diff --git a/api/routers/__init__.py b/api/routers/__init__.py
index fcd30fe..a2bfbfb 100644
--- a/api/routers/__init__.py
+++ b/api/routers/__init__.py
@@ -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:
diff --git a/api/routers/__pycache__/__init__.cpython-313.pyc b/api/routers/__pycache__/__init__.cpython-313.pyc
index d8cbcd3..fb4c8ca 100644
Binary files a/api/routers/__pycache__/__init__.cpython-313.pyc and b/api/routers/__pycache__/__init__.cpython-313.pyc differ
diff --git a/api/routers/__pycache__/toolkit.cpython-313.pyc b/api/routers/__pycache__/toolkit.cpython-313.pyc
index 419d40a..86c890f 100644
Binary files a/api/routers/__pycache__/toolkit.cpython-313.pyc and b/api/routers/__pycache__/toolkit.cpython-313.pyc differ
diff --git a/api/routers/toolkit.py b/api/routers/toolkit.py
index b53d1ae..fd8bf7d 100644
--- a/api/routers/toolkit.py
+++ b/api/routers/toolkit.py
@@ -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
diff --git a/api/static/js/index.js b/api/static/js/index.js
index f8ed674..2cbb16b 100644
--- a/api/static/js/index.js
+++ b/api/static/js/index.js
@@ -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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Название
+ Описание
+ Действия
+
+
+
+
+
+
+
+
+
+ Добавить категорию
+
+
+
+
+
+
+
Новая категория
+
+
+ Название категории
+
+
+
+ Описание категории
+
+
+
+
+
+ Отмена
+
+
+ Сохранить
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Планируемые изменения
+
+
+
+
+
+
+
+
+ Нет запланированных изменений
+
+
+
+
+
+
+ `;
+
+ 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 = `
+ ${escapeHtml(category.title)}
+
+ Отменить
+
+ `;
+ 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 = `
+ ${escapeHtml(category.title)}
+
+ Отменить
+
+ `;
+ 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 = `
+ ${escapeHtml(category.title)}
+
+ Отменить
+
+ `;
+ 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 = 'Новая ';
+ break;
+ case 'edited':
+ rowClass = 'table-warning';
+ badge = 'Изменена ';
+ break;
+ case 'deleted':
+ rowClass = 'table-danger';
+ badge = 'Удалена ';
+ break;
+ default:
+ rowClass = '';
+ badge = '';
+ }
+
+ // Для удаленных категорий показываем только с кнопкой восстановления
+ if (status === 'new') {
+ // Для новых категорий показываем только кнопку отмены
+ categoriesListBody.innerHTML += `
+
+
+ ${escapeHtml(category.title)}
+ ${badge}
+
+
+ ${escapeHtml(category.description)}
+
+
+
+ Отменить
+
+
+
+ `;
+ } else if (status === 'deleted') {
+ // Для удаленных категорий показываем кнопку восстановления
+ categoriesListBody.innerHTML += `
+
+
+ ${escapeHtml(category.title)}
+ ${badge}
+
+
+ ${escapeHtml(category.description)}
+
+
+
+
+
+
+
+ `;
+ } else {
+ // Для остальных категорий (unchanged, edited) показываем обычные кнопки
+ categoriesListBody.innerHTML += `
+
+
+ ${escapeHtml(category.title)}
+ ${badge}
+
+
+ ${escapeHtml(category.description)}
+
+
+
+ ${category.created_at ? new Date(category.created_at).toLocaleDateString('ru-RU') : 'Новая'}
+
+
+
+ ${category.updated_at ? new Date(category.updated_at).toLocaleDateString('ru-RU') : 'Новая'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+ });
+ }
+
+ // Функция для экранирования 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 = `
+
+
+
+
+
+ Название категории
+
+
+
+ Описание категории
+
+
+
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+
+
+ Внимание! Категория может быть удалена только если в ней нет инструментов.
+ При попытке удаления категории, используемой в инструментах, вы получите увдомление об успехе,
+ при этом категория не будет удалена!
+
+
+ Название: ${escapeHtml(categories[index].title)}
+
+
+ Описание: ${escapeHtml(categories[index].description)}
+
+
+
+
+
+ `;
+
+ 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 = `
+
+ `;
- 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">
Все категории
- ${Object.values(uniqueCategories).map(category => `
+ ${categoriesArray.map(category => `
${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`);
diff --git a/db/handlers/__pycache__/categories.cpython-313.pyc b/db/handlers/__pycache__/categories.cpython-313.pyc
index fafb64b..6b9d58a 100644
Binary files a/db/handlers/__pycache__/categories.cpython-313.pyc and b/db/handlers/__pycache__/categories.cpython-313.pyc differ
diff --git a/db/handlers/__pycache__/toolkit.cpython-313.pyc b/db/handlers/__pycache__/toolkit.cpython-313.pyc
index fa66782..15718a0 100644
Binary files a/db/handlers/__pycache__/toolkit.cpython-313.pyc and b/db/handlers/__pycache__/toolkit.cpython-313.pyc differ
diff --git a/db/handlers/categories.py b/db/handlers/categories.py
index 2425ef8..5bb31ec 100644
--- a/db/handlers/categories.py
+++ b/db/handlers/categories.py
@@ -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:
diff --git a/db/handlers/toolkit.py b/db/handlers/toolkit.py
index 3e05ea5..24a9a71 100644
--- a/db/handlers/toolkit.py
+++ b/db/handlers/toolkit.py
@@ -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)
diff --git a/db/schemas/__pycache__/categories.cpython-313.pyc b/db/schemas/__pycache__/categories.cpython-313.pyc
index cd79ed7..f2a075b 100644
Binary files a/db/schemas/__pycache__/categories.cpython-313.pyc and b/db/schemas/__pycache__/categories.cpython-313.pyc differ
diff --git a/db/schemas/categories.py b/db/schemas/categories.py
index 2c00b14..d12106b 100644
--- a/db/schemas/categories.py
+++ b/db/schemas/categories.py
@@ -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)