Управление категориями
This commit is contained in:
@@ -62,6 +62,7 @@ async def post_requests(
|
||||
"toolkits": toolkits,
|
||||
"categories": categories,
|
||||
}
|
||||
# logger.info(resultData)
|
||||
case "jurnal_toolkits":
|
||||
jurnal_toolkits = await StocksRecordsHandler.get()
|
||||
if jurnal_toolkits:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+31
-4
@@ -11,17 +11,17 @@ router = APIRouter()
|
||||
|
||||
@router.post("/", summary="Запрос остатка инструмента")
|
||||
async def toolkit_request(
|
||||
request_data: dict = Depends(requestDict),
|
||||
reqData: dict = Depends(requestDict),
|
||||
):
|
||||
response = {"status": "error", "data": {}}
|
||||
toolkitId = request_data.get("body").get("toolkitId")
|
||||
toolkitId = reqData.get("body").get("toolkitId")
|
||||
logger.info(f"Получение запроса остатка инструмента #{toolkitId}")
|
||||
# logger.info(request_data)
|
||||
stocks = await StockHandler.getByToolkitId(toolkitId)
|
||||
if not stocks:
|
||||
return response
|
||||
userId = request_data.get("body").get("userId")
|
||||
allToolboxes = request_data.get("body").get("allToolboxes")
|
||||
userId = reqData.get("body").get("userId")
|
||||
allToolboxes = reqData.get("body").get("allToolboxes")
|
||||
toolboxes = (
|
||||
await ToolboxHandler.getByOwner(userId)
|
||||
if not allToolboxes
|
||||
@@ -73,3 +73,30 @@ async def fill_toolbox():
|
||||
"placements": [placement.toDict() for placement in placements],
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/categories_batch", summary="Управление категориями")
|
||||
async def categories_batch(reqData: dict = Depends(requestDict)):
|
||||
logger.info(f"Управление категориями")
|
||||
response = {"status": "error"}
|
||||
userId = reqData.get("body").get("userId")
|
||||
changesData = reqData.get("body").get("changes")
|
||||
success = True
|
||||
for newCategoryData in changesData.get("create", []):
|
||||
logger.info(f"Добавление категории: {newCategoryData.get('title')}")
|
||||
result = await CategoryHandler.add(newCategoryData, userId)
|
||||
if not result:
|
||||
success = False
|
||||
for updateCategoryData in changesData.get("update", []):
|
||||
logger.info(f"Обновление категории: {updateCategoryData.get('title')}")
|
||||
result = await CategoryHandler.edit(updateCategoryData, userId)
|
||||
if not result:
|
||||
success = False
|
||||
for deleteCategoryId in changesData.get("delete", []):
|
||||
logger.info(f"Удаление категории: {deleteCategoryId}")
|
||||
result = await CategoryHandler.delete(deleteCategoryId, userId)
|
||||
if not result:
|
||||
success = False
|
||||
if success:
|
||||
response["status"] = "ok"
|
||||
return response
|
||||
|
||||
+848
-28
@@ -240,37 +240,857 @@ function renderSimpleTab(tabId, tabData, title) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderToolkitsTab(tabId, toolsArray, categoriesList) {
|
||||
|
||||
async function manageCategory(categoriesList) {
|
||||
// Удаляем старое модальное окно, если оно существует
|
||||
let modal = document.getElementById('manageCategoryModal');
|
||||
if (modal) modal.remove();
|
||||
|
||||
// Храним изменения
|
||||
const changes = {
|
||||
create: [], // новые категории
|
||||
update: [], // измененные категории
|
||||
delete: [] // id категорий для удаления
|
||||
};
|
||||
|
||||
// Делаем копию списка категорий для работы с дополнительными полями
|
||||
const categories = categoriesList.map(cat => ({
|
||||
...cat,
|
||||
status: 'unchanged', // unchanged, new, edited, deleted
|
||||
originalData: null
|
||||
}));
|
||||
|
||||
// Создаём модальное окно
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'modal fade';
|
||||
modal.id = 'manageCategoryModal';
|
||||
modal.tabIndex = -1;
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-folder me-2"></i>Управление категориями</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div id="manageCategoryError" class="alert alert-danger d-none m-3" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<span id="manageCategoryErrorMessage"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" id="categoriesTable">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 35%;">Название</th>
|
||||
<th style="width: 45%;">Описание</th>
|
||||
<th style="width: 20%;" class="text-center">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="categoriesListBody">
|
||||
<!-- Список категорий будет здесь -->
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addCategoryBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить категорию
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="newCategoryFormRow" class="d-none">
|
||||
<td colspan="3">
|
||||
<div class="p-3 border border-primary rounded">
|
||||
<h6 class="mb-3 text-primary"><i class="bi bi-plus-lg me-1"></i>Новая категория</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-12">
|
||||
<label for="newCategoryTitle" class="form-label required">Название категории</label>
|
||||
<input type="text" class="form-control form-control-sm" id="newCategoryTitle"
|
||||
placeholder="Введите название категории" required minlength="2" maxlength="100">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label for="newCategoryDescription" class="form-label required">Описание категории</label>
|
||||
<textarea class="form-control form-control-sm" id="newCategoryDescription"
|
||||
rows="2" placeholder="Описание категории" required maxlength="500"></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex justify-content-end gap-2 mt-2">
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="cancelNewCategoryBtn">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" id="saveNewCategoryBtn">
|
||||
<i class="bi bi-check-lg me-1"></i>Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Панель изменений -->
|
||||
<div id="changesPanel" class="border-top p-3">
|
||||
<h6 class="mb-3"><i class="bi bi-list-check me-2"></i>Планируемые изменения</h6>
|
||||
|
||||
<div id="addChangesList" class="mb-3">
|
||||
<h6 class="text-success mb-2"><i class="bi bi-plus-circle me-1"></i>Добавляем:</h6>
|
||||
<div id="addChangesItems" class="ps-3">
|
||||
<!-- Новые категории будут здесь -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editChangesList" class="mb-3">
|
||||
<h6 class="text-warning mb-2"><i class="bi bi-pencil-square me-1"></i>Меняем:</h6>
|
||||
<div id="editChangesItems" class="ps-3">
|
||||
<!-- Измененные категории будут здесь -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deleteChangesList" class="mb-3">
|
||||
<h6 class="text-danger mb-2"><i class="bi bi-trash me-1"></i>Удаляем:</h6>
|
||||
<div id="deleteChangesItems" class="ps-3">
|
||||
<!-- Категории для удаления будут здесь -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="noChangesMessage" class="text-muted text-center py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>Нет запланированных изменений
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="saveAllChangesBtn">
|
||||
<span class="spinner-border spinner-border-sm me-1" id="saveChangesSpinner" style="display: none;"></span>
|
||||
<span id="saveChangesText">Сохранить все изменения</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
|
||||
const categoriesListBody = modal.querySelector('#categoriesListBody');
|
||||
const newCategoryFormRow = modal.querySelector('#newCategoryFormRow');
|
||||
const addCategoryBtn = modal.querySelector('#addCategoryBtn');
|
||||
const cancelNewCategoryBtn = modal.querySelector('#cancelNewCategoryBtn');
|
||||
const saveNewCategoryBtn = modal.querySelector('#saveNewCategoryBtn');
|
||||
const saveAllChangesBtn = modal.querySelector('#saveAllChangesBtn');
|
||||
const saveChangesSpinner = modal.querySelector('#saveChangesSpinner');
|
||||
const saveChangesText = modal.querySelector('#saveChangesText');
|
||||
const addChangesItems = modal.querySelector('#addChangesItems');
|
||||
const editChangesItems = modal.querySelector('#editChangesItems');
|
||||
const deleteChangesItems = modal.querySelector('#deleteChangesItems');
|
||||
const noChangesMessage = modal.querySelector('#noChangesMessage');
|
||||
const addChangesList = modal.querySelector('#addChangesList');
|
||||
const editChangesList = modal.querySelector('#editChangesList');
|
||||
const deleteChangesList = modal.querySelector('#deleteChangesList');
|
||||
|
||||
// Функция для обновления панели изменений
|
||||
function updateChangesPanel() {
|
||||
// Очищаем панели
|
||||
addChangesItems.innerHTML = '';
|
||||
editChangesItems.innerHTML = '';
|
||||
deleteChangesItems.innerHTML = '';
|
||||
|
||||
// Собираем изменения
|
||||
const newCategories = categories.filter(cat => cat.status === 'new');
|
||||
const editedCategories = categories.filter(cat => cat.status === 'edited');
|
||||
const deletedCategories = categories.filter(cat => cat.status === 'deleted');
|
||||
|
||||
// Обновляем отображение панелей
|
||||
if (newCategories.length > 0) {
|
||||
addChangesList.classList.remove('d-none');
|
||||
newCategories.forEach((category, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center mb-1';
|
||||
item.innerHTML = `
|
||||
<span>${escapeHtml(category.title)}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelNewCategoryAction(${categories.indexOf(category)})">
|
||||
<i class="bi bi-x"></i> Отменить
|
||||
</button>
|
||||
`;
|
||||
addChangesItems.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
addChangesList.classList.add('d-none');
|
||||
}
|
||||
|
||||
if (editedCategories.length > 0) {
|
||||
editChangesList.classList.remove('d-none');
|
||||
editedCategories.forEach((category, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center mb-1';
|
||||
item.innerHTML = `
|
||||
<span>${escapeHtml(category.title)}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelEditCategoryAction(${categories.indexOf(category)})">
|
||||
<i class="bi bi-x"></i> Отменить
|
||||
</button>
|
||||
`;
|
||||
editChangesItems.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
editChangesList.classList.add('d-none');
|
||||
}
|
||||
|
||||
if (deletedCategories.length > 0) {
|
||||
deleteChangesList.classList.remove('d-none');
|
||||
deletedCategories.forEach((category, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center mb-1';
|
||||
item.innerHTML = `
|
||||
<span>${escapeHtml(category.title)}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelDeleteCategoryAction(${categories.indexOf(category)})">
|
||||
<i class="bi bi-x"></i> Отменить
|
||||
</button>
|
||||
`;
|
||||
deleteChangesItems.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
deleteChangesList.classList.add('d-none');
|
||||
}
|
||||
|
||||
// Показываем/скрываем сообщение "нет изменений"
|
||||
const hasChanges = newCategories.length > 0 || editedCategories.length > 0 || deletedCategories.length > 0;
|
||||
if (hasChanges) {
|
||||
noChangesMessage.classList.add('d-none');
|
||||
} else {
|
||||
noChangesMessage.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для рендеринга списка категорий
|
||||
function renderCategoriesList() {
|
||||
categoriesListBody.innerHTML = '';
|
||||
|
||||
categories.forEach((category, index) => {
|
||||
const status = category.status;
|
||||
|
||||
// Определяем стили в зависимости от статуса
|
||||
let rowClass = '';
|
||||
let badge = '';
|
||||
|
||||
switch (status) {
|
||||
case 'new':
|
||||
rowClass = 'table-success';
|
||||
badge = '<span class="badge bg-success">Новая</span>';
|
||||
break;
|
||||
case 'edited':
|
||||
rowClass = 'table-warning';
|
||||
badge = '<span class="badge bg-warning">Изменена</span>';
|
||||
break;
|
||||
case 'deleted':
|
||||
rowClass = 'table-danger';
|
||||
badge = '<span class="badge bg-danger">Удалена</span>';
|
||||
break;
|
||||
default:
|
||||
rowClass = '';
|
||||
badge = '';
|
||||
}
|
||||
|
||||
// Для удаленных категорий показываем только с кнопкой восстановления
|
||||
if (status === 'new') {
|
||||
// Для новых категорий показываем только кнопку отмены
|
||||
categoriesListBody.innerHTML += `
|
||||
<tr id="category-row-${index}" class="${rowClass}">
|
||||
<td>
|
||||
<div class="fw-semibold">${escapeHtml(category.title)}</div>
|
||||
${badge}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-muted small">${escapeHtml(category.description)}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="cancelNewCategoryAction(${index})" title="Отменить добавление">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
} else if (status === 'deleted') {
|
||||
// Для удаленных категорий показываем кнопку восстановления
|
||||
categoriesListBody.innerHTML += `
|
||||
<tr id="category-row-${index}" class="${rowClass}">
|
||||
<td>
|
||||
<div class="fw-semibold">${escapeHtml(category.title)}</div>
|
||||
${badge}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-muted small">${escapeHtml(category.description)}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="restoreCategory(${index})" title="Восстановить">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
} else {
|
||||
// Для остальных категорий (unchanged, edited) показываем обычные кнопки
|
||||
categoriesListBody.innerHTML += `
|
||||
<tr id="category-row-${index}" class="${rowClass}">
|
||||
<td>
|
||||
<div class="fw-semibold">${escapeHtml(category.title)}</div>
|
||||
${badge}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-muted small">${escapeHtml(category.description)}</div>
|
||||
<div class="text-muted mt-1">
|
||||
<small>
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
${category.created_at ? new Date(category.created_at).toLocaleDateString('ru-RU') : 'Новая'}
|
||||
</small>
|
||||
<small class="ms-3">
|
||||
<i class="bi bi-pencil me-1"></i>
|
||||
${category.updated_at ? new Date(category.updated_at).toLocaleDateString('ru-RU') : 'Новая'}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="editCategory(${index})" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="deleteCategory(${index})" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для экранирования HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Функции для работы с категориями
|
||||
modal.editCategory = function (index) {
|
||||
// Сохраняем оригинальные данные если еще не сохранены
|
||||
if (!categories[index].originalData) {
|
||||
categories[index].originalData = {
|
||||
title: categories[index].title,
|
||||
description: categories[index].description
|
||||
};
|
||||
}
|
||||
|
||||
// Создаем модальное окно для редактирования
|
||||
const editModal = document.createElement('div');
|
||||
editModal.className = 'modal fade';
|
||||
editModal.id = 'editCategoryModal';
|
||||
editModal.innerHTML = `
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-pencil-square me-2"></i>Редактирование категории</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="editCategoryTitle" class="form-label required">Название категории</label>
|
||||
<input type="text" class="form-control" id="editCategoryTitle"
|
||||
value="${escapeHtml(categories[index].title)}" required minlength="2" maxlength="100">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editCategoryDescription" class="form-label required">Описание категории</label>
|
||||
<textarea class="form-control" id="editCategoryDescription"
|
||||
rows="3" required maxlength="500">${escapeHtml(categories[index].description)}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-success" id="saveEditCategoryBtn">
|
||||
Сохранить изменения
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(editModal);
|
||||
const bsEditModal = new bootstrap.Modal(editModal);
|
||||
|
||||
// Обработчик сохранения изменений
|
||||
editModal.querySelector('#saveEditCategoryBtn').addEventListener('click', () => {
|
||||
const titleInput = editModal.querySelector('#editCategoryTitle');
|
||||
const descriptionInput = editModal.querySelector('#editCategoryDescription');
|
||||
|
||||
const title = titleInput.value.trim();
|
||||
const description = descriptionInput.value.trim();
|
||||
|
||||
// Валидация
|
||||
if (!title || title.length < 2) {
|
||||
showInfo('Название категории должно содержать минимум 2 символа', 'error');
|
||||
titleInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description || description.length < 2) {
|
||||
showInfo('Описание категории должно содержать минимум 2 символа', 'error');
|
||||
descriptionInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем уникальность названия среди других категорий
|
||||
const duplicate = categories.find((cat, i) =>
|
||||
i !== index &&
|
||||
cat.id !== categories[index].id &&
|
||||
cat.title.toLowerCase() === title.toLowerCase() &&
|
||||
cat.status !== 'deleted'
|
||||
);
|
||||
|
||||
if (duplicate) {
|
||||
showInfo('Категория с таким названием уже существует', 'error');
|
||||
titleInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем данные категории
|
||||
categories[index].title = title;
|
||||
categories[index].description = description;
|
||||
categories[index].status = 'edited';
|
||||
|
||||
// Добавляем в изменения, если это существующая категория
|
||||
if (categories[index].id) {
|
||||
const updateIndex = changes.update.findIndex(item => item.id === categories[index].id);
|
||||
if (updateIndex === -1) {
|
||||
changes.update.push({
|
||||
id: categories[index].id,
|
||||
title: title,
|
||||
description: description
|
||||
});
|
||||
} else {
|
||||
changes.update[updateIndex] = {
|
||||
id: categories[index].id,
|
||||
title: title,
|
||||
description: description
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
bsEditModal.hide();
|
||||
setTimeout(() => {
|
||||
editModal.remove();
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Изменения сохранены', 'success');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Очистка при закрытии модалки
|
||||
editModal.addEventListener('hidden.bs.modal', () => {
|
||||
setTimeout(() => {
|
||||
if (editModal.parentNode) editModal.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
bsEditModal.show();
|
||||
setTimeout(() => {
|
||||
editModal.querySelector('#editCategoryTitle').focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
modal.cancelEditCategoryAction = function (index) {
|
||||
if (categories[index].originalData) {
|
||||
// Восстанавливаем исходные данные
|
||||
categories[index].title = categories[index].originalData.title;
|
||||
categories[index].description = categories[index].originalData.description;
|
||||
categories[index].status = 'unchanged';
|
||||
categories[index].originalData = null;
|
||||
|
||||
// Удаляем из изменений
|
||||
if (categories[index].id) {
|
||||
const updateIndex = changes.update.findIndex(item => item.id === categories[index].id);
|
||||
if (updateIndex !== -1) {
|
||||
changes.update.splice(updateIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Изменения отменены', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
modal.deleteCategory = function (index) {
|
||||
// Сохраняем оригинальные данные если еще не сохранены
|
||||
if (!categories[index].originalData) {
|
||||
categories[index].originalData = {
|
||||
title: categories[index].title,
|
||||
description: categories[index].description
|
||||
};
|
||||
}
|
||||
|
||||
// Создаем модальное окно подтверждения удаления
|
||||
const deleteModal = document.createElement('div');
|
||||
deleteModal.className = 'modal fade';
|
||||
deleteModal.id = 'deleteCategoryModal';
|
||||
deleteModal.innerHTML = `
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-danger"><i class="bi bi-trash me-2"></i>Удаление категории</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning mb-3">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Внимание!</strong> Категория может быть удалена только если в ней нет инструментов.
|
||||
При попытке удаления категории, используемой в инструментах, вы получите увдомление об успехе,
|
||||
при этом категория не будет удалена!
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Название:</strong> ${escapeHtml(categories[index].title)}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>Описание:</strong> ${escapeHtml(categories[index].description)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteCategoryBtn">
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(deleteModal);
|
||||
const bsDeleteModal = new bootstrap.Modal(deleteModal);
|
||||
|
||||
// Обработчик подтверждения удаления
|
||||
deleteModal.querySelector('#confirmDeleteCategoryBtn').addEventListener('click', () => {
|
||||
categories[index].status = 'deleted';
|
||||
|
||||
// Добавляем в изменения, если это существующая категория
|
||||
if (categories[index].id) {
|
||||
const deleteIndex = changes.delete.indexOf(categories[index].id);
|
||||
if (deleteIndex === -1) {
|
||||
changes.delete.push(categories[index].id);
|
||||
}
|
||||
} else {
|
||||
// Если это новая категория - удаляем из изменений на создание
|
||||
const createIndex = changes.create.findIndex(item =>
|
||||
item.title === categories[index].originalData.title &&
|
||||
item.description === categories[index].originalData.description
|
||||
);
|
||||
if (createIndex !== -1) {
|
||||
changes.create.splice(createIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bsDeleteModal.hide();
|
||||
setTimeout(() => {
|
||||
deleteModal.remove();
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Категория помечена для удаления', 'warning');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Очистка при закрытии модалки
|
||||
deleteModal.addEventListener('hidden.bs.modal', () => {
|
||||
setTimeout(() => {
|
||||
if (deleteModal.parentNode) deleteModal.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
bsDeleteModal.show();
|
||||
};
|
||||
|
||||
modal.cancelDeleteCategoryAction = function (index) {
|
||||
// Восстанавливаем исходные данные
|
||||
categories[index].title = categories[index].originalData.title;
|
||||
categories[index].description = categories[index].originalData.description;
|
||||
categories[index].status = 'unchanged';
|
||||
categories[index].originalData = null;
|
||||
|
||||
// Удаляем из изменений на удаление
|
||||
if (categories[index].id) {
|
||||
const deleteIndex = changes.delete.indexOf(categories[index].id);
|
||||
if (deleteIndex !== -1) {
|
||||
changes.delete.splice(deleteIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Удаление отменено', 'info');
|
||||
};
|
||||
|
||||
modal.restoreCategory = function (index) {
|
||||
// Восстанавливаем категорию
|
||||
categories[index].status = 'unchanged';
|
||||
categories[index].originalData = null;
|
||||
|
||||
// Удаляем из изменений на удаление
|
||||
if (categories[index].id) {
|
||||
const deleteIndex = changes.delete.indexOf(categories[index].id);
|
||||
if (deleteIndex !== -1) {
|
||||
changes.delete.splice(deleteIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Категория восстановлена', 'success');
|
||||
};
|
||||
|
||||
modal.cancelNewCategoryAction = function (index) {
|
||||
// Удаляем новую категорию
|
||||
if (categories[index].status === 'new') {
|
||||
// Удаляем из изменений на создание
|
||||
const createIndex = changes.create.findIndex(item =>
|
||||
item.title === categories[index].title &&
|
||||
item.description === categories[index].description
|
||||
);
|
||||
if (createIndex !== -1) {
|
||||
changes.create.splice(createIndex, 1);
|
||||
}
|
||||
|
||||
// Удаляем из массива категорий
|
||||
categories.splice(index, 1);
|
||||
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Новая категория удалена', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
// Инициализируем список категорий
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
|
||||
// Обработчики событий
|
||||
addCategoryBtn.addEventListener('click', () => {
|
||||
newCategoryFormRow.classList.remove('d-none');
|
||||
addCategoryBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
const titleInput = modal.querySelector('#newCategoryTitle');
|
||||
if (titleInput) titleInput.focus();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
cancelNewCategoryBtn.addEventListener('click', () => {
|
||||
newCategoryFormRow.classList.add('d-none');
|
||||
addCategoryBtn.disabled = false;
|
||||
modal.querySelector('#newCategoryTitle').value = '';
|
||||
modal.querySelector('#newCategoryDescription').value = '';
|
||||
});
|
||||
|
||||
saveNewCategoryBtn.addEventListener('click', () => {
|
||||
const titleInput = modal.querySelector('#newCategoryTitle');
|
||||
const descriptionInput = modal.querySelector('#newCategoryDescription');
|
||||
|
||||
const title = titleInput.value.trim();
|
||||
const description = descriptionInput.value.trim();
|
||||
|
||||
// Валидация
|
||||
if (!title || title.length < 2) {
|
||||
showInfo('Название категории должно содержать минимум 2 символа', 'error');
|
||||
titleInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description || description.length < 2) {
|
||||
showInfo('Описание категории должно содержать минимум 2 символа', 'error');
|
||||
descriptionInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем уникальность названия
|
||||
const duplicate = categories.find(cat =>
|
||||
cat.title.toLowerCase() === title.toLowerCase() &&
|
||||
cat.status !== 'deleted'
|
||||
);
|
||||
|
||||
if (duplicate) {
|
||||
showInfo('Категория с таким названием уже существует', 'error');
|
||||
titleInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем новую категорию
|
||||
const newCategory = {
|
||||
title: title,
|
||||
description: description,
|
||||
status: 'new',
|
||||
originalData: null
|
||||
};
|
||||
|
||||
categories.push(newCategory);
|
||||
changes.create.push({
|
||||
title: title,
|
||||
description: description
|
||||
});
|
||||
|
||||
// Сбрасываем форму
|
||||
titleInput.value = '';
|
||||
descriptionInput.value = '';
|
||||
newCategoryFormRow.classList.add('d-none');
|
||||
addCategoryBtn.disabled = false;
|
||||
|
||||
// Обновляем отображение
|
||||
renderCategoriesList();
|
||||
updateChangesPanel();
|
||||
showInfo('Категория добавлена', 'success');
|
||||
});
|
||||
|
||||
// Сохранение всех изменений
|
||||
saveAllChangesBtn.addEventListener('click', async function () {
|
||||
// Проверяем, есть ли изменения
|
||||
const hasChanges = changes.create.length > 0 ||
|
||||
changes.update.length > 0 ||
|
||||
changes.delete.length > 0;
|
||||
|
||||
if (!hasChanges) {
|
||||
showInfo('Нет изменений для сохранения', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем исходное состояние кнопки
|
||||
const originalText = saveChangesText.textContent;
|
||||
const originalDisabledState = saveAllChangesBtn.disabled;
|
||||
|
||||
// Двойное подтверждение
|
||||
saveChangesText.textContent = 'Нажмите еще раз для подтверждения (10 сек)';
|
||||
saveAllChangesBtn.disabled = true;
|
||||
|
||||
let confirmed = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!confirmed) {
|
||||
// Возвращаем кнопку в исходное состояние
|
||||
saveChangesText.textContent = originalText;
|
||||
saveAllChangesBtn.disabled = originalDisabledState;
|
||||
saveChangesSpinner.style.display = 'none';
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
const confirmHandler = async function () {
|
||||
confirmed = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
saveAllChangesBtn.disabled = true;
|
||||
saveChangesSpinner.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
// Отправляем запрос на сохранение изменений
|
||||
const response = await apiRequest('/toolkit/categories_batch', {
|
||||
changes: changes,
|
||||
userId: userData.id
|
||||
}, 'POST');
|
||||
|
||||
if (response.status === 'ok') {
|
||||
showInfo('Изменения успешно сохранены', 'success');
|
||||
bsModal.hide();
|
||||
|
||||
// Обновляем вкладку инструментов
|
||||
await uploadTab('toolkits');
|
||||
} else {
|
||||
throw new Error(response.message || 'Ошибка при сохранении изменений');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при сохранении изменений:', error);
|
||||
|
||||
// Возвращаем кнопку в исходное состояние
|
||||
saveAllChangesBtn.disabled = false;
|
||||
saveChangesSpinner.style.display = 'none';
|
||||
saveChangesText.textContent = originalText;
|
||||
|
||||
const errorDiv = modal.querySelector('#manageCategoryError');
|
||||
const errorMessage = modal.querySelector('#manageCategoryErrorMessage');
|
||||
if (errorDiv && errorMessage) {
|
||||
errorMessage.textContent = error.message || 'Произошла ошибка при сохранении изменений';
|
||||
errorDiv.classList.remove('d-none');
|
||||
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Сбрасываем состояние подтверждения
|
||||
confirmed = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик для второго клика
|
||||
const secondClickHandler = function () {
|
||||
saveAllChangesBtn.removeEventListener('click', secondClickHandler);
|
||||
confirmHandler();
|
||||
};
|
||||
|
||||
saveAllChangesBtn.addEventListener('click', secondClickHandler);
|
||||
saveAllChangesBtn.disabled = false;
|
||||
saveChangesText.textContent = 'Подтвердите сохранение (10 сек)';
|
||||
});
|
||||
|
||||
// Очистка при закрытии модалки
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
setTimeout(() => {
|
||||
if (modal.parentNode) modal.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Делаем функции глобально доступными для обработчиков onclick
|
||||
window.editCategory = modal.editCategory;
|
||||
window.cancelEditCategoryAction = modal.cancelEditCategoryAction;
|
||||
window.deleteCategory = modal.deleteCategory;
|
||||
window.cancelDeleteCategoryAction = modal.cancelDeleteCategoryAction;
|
||||
window.restoreCategory = modal.restoreCategory;
|
||||
window.cancelNewCategoryAction = modal.cancelNewCategoryAction;
|
||||
|
||||
// Показываем модалку
|
||||
bsModal.show();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderToolkitsTab(tabId, toolsList, categoriesArray) {
|
||||
const tabContent = document.getElementById(`${tabId}-tab-content`);
|
||||
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
|
||||
|
||||
// Преобразуем объект в массив, если передан объект
|
||||
const tools = Array.isArray(toolsArray) ? toolsArray : Object.values(toolsArray);
|
||||
if (accessData.tools_creation) {
|
||||
tabOptionalContent.innerHTML = `
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<button class="btn btn-outline-secondary" id="manageCategoryBtn">
|
||||
<i class="bi bi-gear-wide-connected me-2"></i>Категории
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let uniqueCategories = {};
|
||||
categoriesList.forEach(cat => {
|
||||
uniqueCategories[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
|
||||
const manageCategoryBtn = document.getElementById('manageCategoryBtn');
|
||||
manageCategoryBtn.addEventListener('click', () => manageCategory(categoriesArray));
|
||||
}
|
||||
|
||||
|
||||
let categoriesData = {};
|
||||
categoriesArray.forEach(cat => {
|
||||
categoriesData[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
|
||||
});
|
||||
|
||||
tools.forEach(tool => {
|
||||
tool['category'] = uniqueCategories[tool.category_id]?.title || '';
|
||||
tool['category_desc'] = uniqueCategories[tool.category_id]?.description || '';
|
||||
toolsList.forEach(tool => {
|
||||
tool['category'] = categoriesData[tool.category_id]?.title || '';
|
||||
tool['category_desc'] = categoriesData[tool.category_id]?.description || '';
|
||||
});
|
||||
|
||||
// Сортируем инструменты: сначала по названию категории, затем по названию
|
||||
const sortedTools = [...tools].sort((a, b) => {
|
||||
// Получаем названия категорий
|
||||
const catA = uniqueCategories[a.category_id]?.title || '';
|
||||
const catB = uniqueCategories[b.category_id]?.title || '';
|
||||
|
||||
// Сначала сравниваем категории
|
||||
if (catA < catB) return -1;
|
||||
if (catA > catB) return 1;
|
||||
|
||||
// Если категории одинаковые, сравниваем названия инструментов
|
||||
const titleA = a.title || '';
|
||||
const titleB = b.title || '';
|
||||
return titleA.localeCompare(titleB, 'ru');
|
||||
});
|
||||
toolsList.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
|
||||
categoriesArray.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
|
||||
|
||||
// Создаем HTML структуру
|
||||
tabContent.innerHTML = `
|
||||
@@ -286,7 +1106,7 @@ function renderToolkitsTab(tabId, toolsArray, categoriesList) {
|
||||
data-category="all">
|
||||
Все категории
|
||||
</button>
|
||||
${Object.values(uniqueCategories).map(category => `
|
||||
${categoriesArray.map(category => `
|
||||
<button class="btn filter-btn"
|
||||
data-category="${category.id}">
|
||||
${category.title}
|
||||
@@ -318,12 +1138,13 @@ function renderToolkitsTab(tabId, toolsArray, categoriesList) {
|
||||
`;
|
||||
|
||||
// Рендерим карточки
|
||||
renderToolkitCards(tabId, sortedTools, uniqueCategories);
|
||||
renderToolkitCards(tabId, toolsList, categoriesData);
|
||||
|
||||
// Добавляем обработчики событий для фильтров
|
||||
setupFilters(tabId, tools, uniqueCategories);
|
||||
setupFilters(tabId, toolsList, categoriesData);
|
||||
}
|
||||
|
||||
|
||||
// Функция для рендеринга карточек
|
||||
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all') {
|
||||
const container = document.getElementById(`${tabId}-cards-container`);
|
||||
@@ -404,7 +1225,6 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
|
||||
|
||||
// Функция для настройки фильтров
|
||||
function setupFilters(tabId, tools, categoriesMap) {
|
||||
const container = document.getElementById(`${tabId}-cards-container`);
|
||||
const searchInput = document.getElementById(`${tabId}-search-input`);
|
||||
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
from sqlalchemy import select
|
||||
from db.handlers.toolkit import ToolkitHandler
|
||||
from utils import logger
|
||||
from db import CRUD
|
||||
from db.schemas.categories import Category
|
||||
@@ -32,15 +33,21 @@ class CategoryHandler:
|
||||
)
|
||||
return newCategory.toDict()
|
||||
|
||||
async def edit(categoryId: int, **kwargs):
|
||||
async def edit(categoryData: dict, userId: int):
|
||||
categoryId = categoryData.pop("id", None)
|
||||
if not categoryId:
|
||||
logger.error("Не указан id категории")
|
||||
return {}
|
||||
query = select(Category).where(Category.id == categoryId)
|
||||
category = await CRUD.read(query)
|
||||
if not category:
|
||||
logger.error("Категория не найдена")
|
||||
return {}
|
||||
try:
|
||||
user_id = kwargs.get("user_id", None)
|
||||
editedCategory = await category.edit(**kwargs)
|
||||
logger.info(
|
||||
f"Обновление категории {category.title} -> {categoryData.get('title')}"
|
||||
)
|
||||
editedCategory = await category.edit(**categoryData)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления категории: {str(e)}")
|
||||
return {}
|
||||
@@ -48,7 +55,7 @@ class CategoryHandler:
|
||||
logger.error("Категория не обновлена")
|
||||
return {}
|
||||
await ServiceRecordsHandler.add(
|
||||
user_id, {f"Обновлена категория {category.title}": editedCategory.toDict()}
|
||||
userId, {f"Обновлена категория {category.title}": editedCategory.toDict()}
|
||||
)
|
||||
logger.info(f"Категория {editedCategory.title} успешно обновлена")
|
||||
return editedCategory.toDict()
|
||||
@@ -64,6 +71,10 @@ class CategoryHandler:
|
||||
return [category.toDict() for category in categories] if categories else []
|
||||
|
||||
async def delete(categoryId: int, user_id: int = None):
|
||||
categoryInUse = await ToolkitHandler.checkCatogoryUse(categoryId)
|
||||
if categoryInUse:
|
||||
logger.error("Категория используется в инструментах")
|
||||
return True
|
||||
query = select(Category).where(Category.id == categoryId)
|
||||
category = await CRUD.read(query)
|
||||
if not category:
|
||||
|
||||
@@ -160,6 +160,11 @@ class ToolkitHandler:
|
||||
toolkits = await CRUD.read(query, True)
|
||||
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
|
||||
|
||||
async def checkCatogoryUse(category_id: int):
|
||||
query = select(Toolkit).where(Toolkit.category_id == category_id)
|
||||
toolkit = await CRUD.read(query)
|
||||
return True if toolkit else False
|
||||
|
||||
async def delete(toolkitId: int, user_id: int = None):
|
||||
query = select(Toolkit).where(Toolkit.id == toolkitId)
|
||||
toolkit = await CRUD.read(query)
|
||||
|
||||
Binary file not shown.
@@ -23,5 +23,5 @@ class Category(Base):
|
||||
async def save(self):
|
||||
return await CRUD.create(self, refresh=True)
|
||||
|
||||
async def edit(id: int, **kwargs):
|
||||
return await CRUD.update(Category, id, **kwargs)
|
||||
async def edit(self, **kwargs):
|
||||
return await CRUD.update(Category, self.id, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user