From 8b38d699808e9d58d8b251e65e67657f23e72d3b Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 11 Dec 2025 23:07:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/routers/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 4331 -> 4331 bytes .../__pycache__/toolkit.cpython-313.pyc | Bin 4413 -> 6306 bytes api/routers/toolkit.py | 35 +- api/static/js/index.js | 876 +++++++++++++++++- .../__pycache__/categories.cpython-313.pyc | Bin 7630 -> 8360 bytes .../__pycache__/toolkit.cpython-313.pyc | Bin 14159 -> 14632 bytes db/handlers/categories.py | 19 +- db/handlers/toolkit.py | 5 + .../__pycache__/categories.cpython-313.pyc | Bin 1955 -> 1959 bytes db/schemas/categories.py | 4 +- 11 files changed, 902 insertions(+), 38 deletions(-) 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 d8cbcd334ea93bd9c9a4ada64b0f19dd571e2527..fb4c8caf3853237b7b8f385e3c8e6618f529e505 100644 GIT binary patch delta 97 zcmaE@_*#+oGcPX}0}yC2T4yfX$g9H3<;KXsBX)sB^qcEsYhE8t8z|3u@&sOYMuW*W sc#krMPM*gn!6>wOJD(JjlntZU4I!~F%s~1xGXs;*M=FHqu%5j syhj;>C(q-PU=-NAollBM%9>H^hLG47W+45UnSn{@BNLES$cZRZ@(x|K1#<|TY>1P8+4`k zyoym(8}3zAfXm{`m@O{`tad_e)^K7hDUq?v)$a9&SWJpB+*LxS(=U-RGVbE(6Dhhe zNjD})o^InlCTuO*#gPw2;-x%gOi^`7s%~|dr&^+}w5DRo9<#@<@tTH|#*)-n5}mxJ zV_hu&Q9AOrsx{!BWhSQCDJDEN?w<`z{$WK~z+3Q}WE8H!&*4|_4%~zr@Qx9F3D=VI zXuA%#ka8or2)~9KfkH94++_&gL*u|L;#Nt!1oHupgloYo>pYU2!6EQUiYF1mi-tI| z{|&Ed6?tVLu}^fY8_^1Ec%DfE+=BpwBwT{5t(E&E$J#53VYnH*MLg`W;oRS(p8H-z zcKQ)dp9cUh84~vm0YCyHcuLe~jzpE^qEbHDe?~1Y&o#!O>VXLiv;QxZl0&JygbsiZ zQV}VA1S+wdV?;Xf@cd)$NUqGTLYX~;J80CO z5?#W~s(u!^16CyfEr3P<9fFK3FzF9XFyXbo#xC)3_pTf@OHk1B2VohxY1!` z%aFM%Bb}QGK63s9xo#qYf9pTNHzN%)R~QtsUI=*$et`lW?bt}MWpm+bk*Qh6l4xR2 z2w)OG=+BJE9}H&1OmJ$Jb)&sm&Xx!|;aKvul7(8k{j>gzEIbzoFyU}U8Die-%HN~6 zavCYigy*izX4LtbB8QZ8KINk9IMnZAH5b{p7u5&m(a^8sQmq<9-2A@RL|^ zy?4GBs?Y9<9cj8O-UyYZs5?y^c?lwgr$Al4WQ|$lmw0u3N^MT6%?UHFZj1J&OVrVx zw4q_evFu2kUi0i3PA@1Q5G9M`4HA|*zE!52-lWqD2S<44D0I9IwHJ4Zu?PBwl-{zV zwK3l0OAR-AvT{(_zOMi^mX+z{$aG?I1K+E*L!#m_Ep&QP&Y`4p z2wu3zJIA2o4XE|+65}~u>yF+EpY7!JJt9$c2Z8x8z&~&c@a~Y-g>~fdh zgxx1}NOZp%6VNDNzgfrqs=SSz=a%Ry#Rwu7UefSq+!lRGsWrrVd2Lfx)QoAs|6sFxb0-U&Rc-H_aBeYzI8Xz|OgZ23}J8yMlfgaPYcKCO00000ba^^yN(_+>7y{S;kto;%XAdb5lS%>63Jh{(adl;L zbYEn%AOevFEo%Y*0OIUET~)4 z5IK|F2sJVp*9{%f4IP+f(+wrp4JpwLDX1vZ4Ksj7*b^Vu6C=?RBcMjp6D!vfFVPb( zunyA`KeImx;|2pxDHM|z7qOGg7a0mn000010001Uca!iJHxC&gpc~T}E%5^_0TkF0 T7Lzs@FADJj4FME#IRF3v;2lB@ 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 = ` + + `; + + 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 => `