diff --git a/api/routers/__pycache__/toolkit.cpython-313.pyc b/api/routers/__pycache__/toolkit.cpython-313.pyc index db09add..dbc618c 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 c37001d..d8e4253 100644 --- a/api/routers/toolkit.py +++ b/api/routers/toolkit.py @@ -18,6 +18,8 @@ def handleResult(result: dict, response: dict) -> dict: response["message"] = result["errorMessage"] else: response["status"] = "ok" + if "data" in result.keys(): + response["data"] = result["data"] return response @@ -251,3 +253,40 @@ async def quick_action(reqData: dict = Depends(requestDict)): case _: pass return response + + +@router.get("/all", summary="Получение инструментов") +async def get_toolkits(reqData: dict = Depends(requestDict)): + logger.info(f"Получение инструментов") + return {"status": "ok", "data": await ToolkitHandler.getAll()} + + +@router.get("/compatibility", summary="Получение совместимости инструментов") +async def get_compatibility(reqData: dict = Depends(requestDict)): + response = {"status": "error"} + toolkitId = reqData.get("query").get("toolkitId") + toolkitId = int("".join(filter(str.isdigit, toolkitId))) + logger.info(f"Получение совместимости инструмента {toolkitId}") + toolkit = await ToolkitHandler.getCompatibility(toolkitId) + response = handleResult(toolkit, response) + return response + + +@router.post("/compatibility", summary="Управление совместимостью инструментов") +async def compatibility(reqData: dict = Depends(requestDict)): + logger.info(f"Управление совместимостью инструментов") + response = {"status": "error"} + action = reqData.get("body").get("action") + userId = reqData.get("body").get("userId") + data = reqData.get("body").get("data") + match action: + case "add": + toolkit = await ToolkitHandler.addCompatibility(userId, data) + response = handleResult(toolkit, response) + case "delete": + toolkit = await ToolkitHandler.deleteCompatibility(userId, data) + response = handleResult(toolkit, response) + case _: + logger.error(f"Unknown action: {action}") + pass + return response diff --git a/api/static/css/index.css b/api/static/css/index.css index 44c5d53..21048ba 100644 --- a/api/static/css/index.css +++ b/api/static/css/index.css @@ -459,4 +459,8 @@ tr:hover .action-buttons { .modal-footer { padding: 0.75rem 1rem; } +} + +.modal-content-green { + background-color: #f4fbf6; /* светло-зелёный */ } \ No newline at end of file diff --git a/api/static/js/index.js b/api/static/js/index.js index 10ea46a..d8ae7d0 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -1,6 +1,6 @@ import { getCookie } from '/static/js/cookies.js'; import { apiRequest } from '/static/js/api.js'; -import { showInfo } from '/static/js//toast.js'; +import { showInfo } from '/static/js/toast.js'; let accessData; let userData; @@ -3836,14 +3836,16 @@ async function getToolkitStocks(toolkitId) { return resp.data; } -// Функция показа модального окна с деталями инструмента // Функция показа модального окна с деталями инструмента async function showToolkitDetailsModal(toolkitId) { const modalId = 'toolkitDetailsModal'; let modal = document.getElementById(modalId); if (modal) { - modal.hide(); + const modalInstance = bootstrap.Modal.getInstance(modal); + if (modalInstance) { + modalInstance.hide(); + } modal.remove(); } @@ -3930,9 +3932,12 @@ async function showToolkitDetailsModal(toolkitId) { ` : '
'; } - // Переменная для хранения данных об остатках (будет загружена при раскрытии аккордеона) + // Переменные для хранения данных let toolkitStocksData = null; let isStocksLoading = false; + let compatibilityData = null; + let isCompatibilityLoading = false; + let allToolkitsData = null; // Форматирование даты комментария let commentDateInfo = ''; @@ -4028,6 +4033,8 @@ async function showToolkitDetailsModal(toolkitId) { + + ${toolkitData.external_link ? `
@@ -4037,6 +4044,45 @@ async function showToolkitDetailsModal(toolkitId) { ` : ''}
+ + +
+
+

+ +

+
+
+
+
+ Загрузка... +
+

Загрузка данных о совместимости...

+
+
+ +
+
+ + Не удалось загрузить данные о совместимости +
+ ${accessData.tools_edit ? ` + + ` : ''} +
+
+
+
@@ -4140,8 +4186,6 @@ async function showToolkitDetailsModal(toolkitId) { // Показываем результат if (response.status === 'ok') { - // Успешное отображение - // Обновляем список инструментов if (typeof uploadTab === 'function') { await uploadTab('toolkits'); @@ -4370,26 +4414,362 @@ async function showToolkitDetailsModal(toolkitId) { } }; - // Обработчик события раскрытия аккордеона + // Функция для загрузки данных о совместимости + const loadCompatibilityData = async () => { + if (isCompatibilityLoading) return; + + const compatibilityLoading = modal.querySelector('#compatibilityLoading'); + const compatibilityContent = modal.querySelector('#compatibilityContent'); + const compatibilityError = modal.querySelector('#compatibilityError'); + const addCompatibilityBtn = modal.querySelector('#addCompatibilityBtn'); + + try { + isCompatibilityLoading = true; + + // Показываем спиннер, скрываем контент и ошибку + compatibilityLoading.classList.remove('d-none'); + compatibilityContent.classList.add('d-none'); + compatibilityError.classList.add('d-none'); + + // Загружаем данные о совместимости + const response = await apiRequest(`/toolkit/compatibility?toolkitId=${toolkitData.id}`, {}, 'GET'); + + if (response.status === 'ok') { + compatibilityData = response.data; + + // Формируем HTML для совместимости + let compatibilityHtml = ''; + + if (Object.keys(compatibilityData.records || {}).length > 0) { + compatibilityHtml = ` +
+ + + + + + + ${accessData.tools_edit ? '' : ''} + + + + ${Object.entries(compatibilityData.records || {}).map(([recordId, compatibleToolkitId]) => { + const compatibleToolkit = compatibilityData.toolkits[compatibleToolkitId]; + const compatibleCategory = categories[compatibleToolkit?.category_id]; + + return ` + + + + + ${accessData.tools_edit ? ` + + ` : ''} + + `; + }).join('')} + +
ИнструментКатегорияКомментарийДействия
+ ${compatibleToolkit?.title || 'Неизвестный инструмент'} + ${compatibleToolkit ? ` + + ` : ''} + ${compatibleCategory?.title || 'Неизвестно'}${compatibleToolkit?.comment_text || 'Нет комментария'} + +
+
+ `; + } else { + compatibilityHtml = ` +
+ + Нет данных о совместимых инструментах +
+ `; + } + + // Вставляем HTML и показываем контент + compatibilityContent.innerHTML = compatibilityHtml; + compatibilityContent.classList.remove('d-none'); + + // Показываем кнопку добавления, если есть права + if (accessData.tools_edit) { + addCompatibilityBtn.style.display = 'inline-block'; + } + + // Добавляем обработчики для кнопок просмотра инструмента + compatibilityContent.querySelectorAll('.view-toolkit-btn').forEach(button => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + + const compatibleToolkitId = e.currentTarget.dataset.toolkitId; + + const modalInstance = bootstrap.Modal.getInstance(modal); + if (modalInstance) { + modalInstance.hide(); + } + + modal.addEventListener('hidden.bs.modal', async () => { + await showToolkitDetailsModal(compatibleToolkitId); + }, { once: true }); + }); + }); + + // Добавляем обработчики для кнопок удаления + compatibilityContent.querySelectorAll('.delete-compatibility-btn').forEach(button => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const recordId = e.currentTarget.dataset.recordId; + const compatibleToolkitId = e.currentTarget.dataset.toolkitId; + + if (confirm('Вы уверены, что хотите удалить эту связь совместимости?')) { + await deleteCompatibility(recordId, compatibleToolkitId); + } + }); + }); + } else { + throw new Error(response.message || 'Ошибка загрузки данных о совместимости'); + } + } catch (error) { + console.error('Ошибка при загрузке данных о совместимости:', error); + compatibilityError.classList.remove('d-none'); + } finally { + compatibilityLoading.classList.add('d-none'); + isCompatibilityLoading = false; + } + }; + + // Функция для удаления совместимости + const deleteCompatibility = async (recordId, compatibleToolkitId) => { + try { + const response = await apiRequest('/toolkit/compatibility', { + action: 'delete', + userId: userData.id, + data: { + toolkitId: toolkitData.id, + compatibleToolkitId: compatibleToolkitId + } + }, 'POST'); + + if (response.status === 'ok') { + showInfo('Связь совместимости успешно удалена', 'success'); + // Обновляем данные + compatibilityData = null; + await loadCompatibilityData(); + } else { + throw new Error(response.message || 'Ошибка удаления связи'); + } + } catch (error) { + console.error('Ошибка при удалении совместимости:', error); + showInfo(error.message || 'Произошла ошибка при удалении связи', 'danger'); + } + }; + + // Функция для добавления совместимости + const addCompatibility = async (compatibleToolkitId) => { + try { + const response = await apiRequest('/toolkit/compatibility', { + action: 'add', + userId: userData.id, + data: { + toolkitId: toolkitData.id, + compatibleToolkitId: compatibleToolkitId + } + }, 'POST'); + + if (response.status === 'ok') { + showInfo('Связь совместимости успешно добавлена', 'success'); + // Обновляем данные + compatibilityData = null; + await loadCompatibilityData(); + // Закрываем модальное окно добавления + const addModal = bootstrap.Modal.getInstance(document.getElementById(`${toolkitData.id}-add-compatibility-modal`)); + if (addModal) addModal.hide(); + } else { + throw new Error(response.message || 'Ошибка добавления связи'); + } + } catch (error) { + console.error('Ошибка при добавлении совместимости:', error); + showInfo(error.message || 'Произошла ошибка при добавлении связи', 'danger'); + } + }; + + // Функция для показа модального окна добавления совместимости + const showAddCompatibilityModal = async () => { + // Загружаем все инструменты, если еще не загружены + if (!allToolkitsData) { + try { + const response = await apiRequest('/toolkit/all', {}, 'GET'); + if (response.status === 'ok') { + allToolkitsData = response.data; + } + } catch (error) { + console.error('Ошибка загрузки списка инструментов:', error); + showInfo('Не удалось загрузить список инструментов', 'danger'); + return; + } + } + + // Создаем модальное окно + const addModalId = `${toolkitData.id}-add-compatibility-modal`; + let addModal = document.getElementById(addModalId); + + if (addModal) { + addModal.remove(); + } + + addModal = document.createElement('div'); + addModal.className = 'modal fade'; + addModal.id = addModalId; + addModal.tabIndex = -1; + + // Фильтруем инструменты: исключаем текущий и уже совместимые + const compatibleIds = Object.values(compatibilityData?.records || {}); + const filteredToolkits = Object.values(allToolkitsData || {}).filter(toolkit => + toolkit.id !== toolkitData.id && !compatibleIds.includes(toolkit.id) + ); + + addModal.innerHTML = ` + + `; + + document.body.appendChild(addModal); + const bsAddModal = new bootstrap.Modal(addModal); + bsAddModal.show(); + + // Функция поиска инструментов + const searchInput = addModal.querySelector('#compatibilitySearch'); + const toolkitsList = addModal.querySelector('#compatibilityToolkitsList'); + const rows = toolkitsList.querySelectorAll('tr'); + + searchInput.addEventListener('input', function () { + const searchTerm = this.value.toLowerCase(); + + rows.forEach(row => { + const title = row.dataset.toolkitTitle; + const isVisible = title.includes(searchTerm); + row.style.display = isVisible ? '' : 'none'; + }); + }); + + // Обработчики для кнопок выбора + addModal.querySelectorAll('.select-compatibility-btn').forEach(button => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const compatibleToolkitId = e.currentTarget.dataset.toolkitId; + await addCompatibility(compatibleToolkitId); + }); + }); + + // Очистка при закрытии + addModal.addEventListener('hidden.bs.modal', () => { + addModal.remove(); + }); + }; + + // Обработчик события раскрытия аккордеона остатков const stocksCollapse = modal.querySelector('#stocksCollapse'); stocksCollapse.addEventListener('show.bs.collapse', async () => { - // Загружаем данные только если они еще не загружены if (!toolkitStocksData && !isStocksLoading) { await loadToolkitStocks(); } }); - // Обработчик для принудительной перезагрузки данных (например, при повторном открытии аккордеона) - const stocksHeading = modal.querySelector('#stocksHeading'); - stocksHeading.addEventListener('click', async (e) => { - // Если данные уже загружены, можно обновить их при повторном клике - const isExpanded = stocksCollapse.classList.contains('show'); - if (isExpanded && toolkitStocksData) { - // Можно добавить кнопку обновления или обновлять автоматически - // Для простоты пока оставляем как есть + // Обработчик события раскрытия аккордеона совместимости + const compatibilityCollapse = modal.querySelector('#compatibilityCollapse'); + compatibilityCollapse.addEventListener('show.bs.collapse', async () => { + if (!compatibilityData && !isCompatibilityLoading) { + await loadCompatibilityData(); } }); + // Обработчик для кнопки добавления совместимости + const addCompatibilityBtn = modal.querySelector('#addCompatibilityBtn'); + if (addCompatibilityBtn) { + addCompatibilityBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + showAddCompatibilityModal(); + }); + + // Остановка всплытия события при клике на кнопку внутри аккордеона + const compatibilityHeading = modal.querySelector('#compatibilityHeading'); + compatibilityHeading.addEventListener('click', (e) => { + if (addCompatibilityBtn.contains(e.target)) { + e.stopPropagation(); + } + }); + } + // Очистка при закрытии модалки modal.addEventListener('hidden.bs.modal', () => { modal.remove(); diff --git a/db/handlers/__pycache__/orders.cpython-313.pyc b/db/handlers/__pycache__/orders.cpython-313.pyc index 08700df..9bd7096 100644 Binary files a/db/handlers/__pycache__/orders.cpython-313.pyc and b/db/handlers/__pycache__/orders.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/toolkit.cpython-313.pyc b/db/handlers/__pycache__/toolkit.cpython-313.pyc index e54d0d7..0f0bdcf 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/orders.py b/db/handlers/orders.py index d0e9e1b..4f391c1 100644 --- a/db/handlers/orders.py +++ b/db/handlers/orders.py @@ -1,6 +1,7 @@ from datetime import date, datetime, time from sqlalchemy import func, select from db import CRUD +from db.handlers.records import ServiceRecordsHandler from db.schemas.orders import Orders from utils.loggers import logger @@ -10,6 +11,10 @@ class OrdersHandler: async def new(user_id: int, order: str): try: await Orders(customer_id=user_id, customer_comment=order).save() + await ServiceRecordsHandler.add( + user_id, + {f"Добавлен заказ": f"{order}"}, + ) except Exception as e: logger.error(f"Ошибка создания заказа: {str(e)}") return {"errorMessage": f"Ошибка создания заказа: {str(e)}"} @@ -92,6 +97,10 @@ class OrdersHandler: if comment: changeData["executor_comment"] = comment await order.edit(**changeData) + await ServiceRecordsHandler.add( + user_id, + {f"Обновлен заказ": f"{order.customer_comment}"}, + ) except Exception as e: logger.error(f"Ошибка обновления заказа: {str(e)}") return {"errorMessage": f"Ошибка обновления заказа: {str(e)}"} diff --git a/db/handlers/toolkit.py b/db/handlers/toolkit.py index 7d395c9..e30f2b0 100644 --- a/db/handlers/toolkit.py +++ b/db/handlers/toolkit.py @@ -3,7 +3,7 @@ from db.handlers.stock import StockHandler from db.handlers.user import UserHandler from utils import logger, saveImage, safeFilename from db import CRUD -from db.schemas.toolkit import Toolkit +from db.schemas.toolkit import Toolkit, ToolkitCompatibility from sqlalchemy import select from db.handlers.records import ServiceRecordsHandler from utils.image import deleteImage @@ -173,7 +173,7 @@ class ToolkitHandler: @staticmethod async def getAll(): - query = select(Toolkit) + query = select(Toolkit).order_by(Toolkit.id) toolkits = await CRUD.read(query, True) return [toolkit.toDict() for toolkit in toolkits] if toolkits else [] @@ -184,16 +184,15 @@ class ToolkitHandler: if not toolkit: logger.error("Инструмент не найден") return {} + data = toolkit.toDict() if toolkit.comment_user_id: user_data = await UserHandler.get(toolkit.comment_user_id) - data = toolkit.toDict() - data["comment_user_data"] = user_data - logger.info(data) + data["comment_user_data"] = user_data return data @staticmethod async def getSeveral(toolkitIds: list[int]) -> list[dict]: - query = select(Toolkit).where(Toolkit.id.in_(toolkitIds)) + query = select(Toolkit).where(Toolkit.id.in_(toolkitIds)).order_by(Toolkit.id) toolkits = await CRUD.read(query, True) return [toolkit.toDict() for toolkit in toolkits] if toolkits else [] @@ -230,9 +229,6 @@ class ToolkitHandler: @staticmethod async def addComment(toolkitId: int, user_id: int, comment: str): - logger.info(f"Добавление комментария к инструменту {toolkitId}...") - logger.info(f"Комментарий: {comment}") - logger.info(f"Пользователь: {user_id}") query = select(Toolkit).where(Toolkit.id == toolkitId) toolkit = await CRUD.read(query) if not toolkit: @@ -248,6 +244,45 @@ class ToolkitHandler: logger.info(f"Комментарий к инструменту {toolkit.title} успешно добавлен") return {"status": "ok"} + @staticmethod + async def addCompatibility(userId, data): + newCompatibility = await ToolkitCompatibility.add_compatibility( + int(data.get("toolkitId")), int(data.get("compatibleToolkitId")) + ) + if "errorMessage" not in newCompatibility: + await ServiceRecordsHandler.add( + userId, + { + f"Добавлена совместимость": f"{data.get('toolkitId')} - {data.get('compatibleToolkitId')}" + }, + ) + return newCompatibility + + @staticmethod + async def deleteCompatibility(userId, data): + deleteCompatibility = await ToolkitCompatibility.remove_compatibility( + int(data.get("toolkitId")), int(data.get("compatibleToolkitId")) + ) + if "errorMessage" not in deleteCompatibility: + await ServiceRecordsHandler.add( + userId, + { + f"Удалена совместимость": f"{data.get('toolkitId')} - {data.get('compatibleToolkitId')}" + }, + ) + return deleteCompatibility + + @staticmethod + async def getCompatibility(toolkitId: int): + result = await ToolkitCompatibility.get_compatibility(toolkitId) + if "errorMessage" in result: + return result + toolkitsIds = list(result.get("data").values()) + toolkitsList = await ToolkitHandler.getSeveral(toolkitsIds) + toolkitsData = {toolkit["id"]: toolkit for toolkit in toolkitsList} + data = {"records": result.get("data"), "toolkits": toolkitsData} + return {"status": "ok", "data": data} + @staticmethod async def initialize(): from .categories import CategoryHandler diff --git a/db/schemas/__pycache__/toolkit.cpython-313.pyc b/db/schemas/__pycache__/toolkit.cpython-313.pyc index da0a154..500f967 100644 Binary files a/db/schemas/__pycache__/toolkit.cpython-313.pyc and b/db/schemas/__pycache__/toolkit.cpython-313.pyc differ diff --git a/db/schemas/toolkit.py b/db/schemas/toolkit.py index b6ab954..5c0f62f 100644 --- a/db/schemas/toolkit.py +++ b/db/schemas/toolkit.py @@ -1,9 +1,154 @@ from datetime import datetime -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, + Text, + select, +) from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import relationship from db import Base, CRUD import utils +from sqlalchemy import Column, Integer, ForeignKey, UniqueConstraint, CheckConstraint + + +class ToolkitCompatibility(Base): + __tablename__ = "toolkit_compatibility" + + id = Column(Integer, primary_key=True) + toolkit_id = Column( + Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False + ) + compatible_toolkit_id = Column( + Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False + ) + __table_args__ = ( + # Запрещаем дубли (A-B и A-B) + UniqueConstraint( + "toolkit_id", "compatible_toolkit_id", name="uq_toolkit_compatibility" + ), + # Запрещаем связь с самим собой + CheckConstraint( + "toolkit_id <> compatible_toolkit_id", name="ck_toolkit_not_self" + ), + ) + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + def toDict(self): + return { + "id": self.id, + "toolkit_id": self.toolkit_id, + "compatible_toolkit_id": self.compatible_toolkit_id, + } + + @staticmethod + def getUnique( + compatibles: list["ToolkitCompatibility"], originalToolkitId: int + ) -> dict: + unique = [] + uniqueData = {} + for c in compatibles: + if c.toolkit_id not in unique and c.toolkit_id != originalToolkitId: + unique.append(c.toolkit_id) + uniqueData[c.id] = c.toolkit_id + if ( + c.compatible_toolkit_id not in unique + and c.compatible_toolkit_id != originalToolkitId + ): + unique.append(c.compatible_toolkit_id) + uniqueData[c.id] = c.compatible_toolkit_id + return uniqueData + + async def save(self): + await CRUD.create(self) + + @staticmethod + async def add_compatibility(a_id: int, b_id: int): + errorMsg = { + "status": "error", + } + + if a_id == b_id: + utils.logger.error("Невозможно добавить совместимость с самим собой") + errorMsg["errorMessage"] = "Невозможно добавить совместимость с самим собой" + return errorMsg + + toolkit_id, compatible_id = sorted([a_id, b_id]) + try: + + await ToolkitCompatibility( + toolkit_id=toolkit_id, compatible_toolkit_id=compatible_id + ).save() + + return {"status": "ok"} + + except Exception as e: + utils.logger.error(f"Ошибка добавления совместимости: {str(e)}") + errorMsg["errorMessage"] = f"Ошибка добавления совместимости: {str(e)}" + return errorMsg + + @staticmethod + async def remove_compatibility(a_id: int, b_id: int): + errorMsg = { + "status": "error", + } + if a_id == b_id: + utils.logger.error("Невозможно удалить совместимость с самим собой") + errorMsg["errorMessage"] = "Невозможно удалить совместимость с самим собой" + return errorMsg + + toolkit_id, compatible_id = sorted([a_id, b_id]) + + try: + compatibility = await CRUD.read( + select(ToolkitCompatibility).where( + ToolkitCompatibility.toolkit_id == toolkit_id, + ToolkitCompatibility.compatible_toolkit_id == compatible_id, + ) + ) + + if not compatibility: + utils.logger.error("Совместимость не найдена") + errorMsg["errorMessage"] = "Совместимость не найдена" + return errorMsg + + await CRUD.delete(compatibility) + + return {"status": "ok"} + + except Exception as e: + utils.logger.error(f"Ошибка удаления совместимости: {str(e)}") + errorMsg["errorMessage"] = f"Ошибка удаления совместимости: {str(e)}" + return errorMsg + + @staticmethod + async def get_compatibility(toolkit_id: int): + try: + + result = await CRUD.read( + select(ToolkitCompatibility).where( + (ToolkitCompatibility.toolkit_id == toolkit_id) + | (ToolkitCompatibility.compatible_toolkit_id == toolkit_id) + ), + True, + ) + if not result: + return {"status": "ok", "data": {}} + + return { + "status": "ok", + "data": ToolkitCompatibility.getUnique(result, toolkit_id), + } + + except Exception as e: + utils.logger.error(f"Ошибка получения совместимости: {str(e)}") + return {"errorMessage": f"Ошибка получения совместимости: {str(e)}"} class Toolkit(Base):