diff --git a/api/routers/__pycache__/toolkit.cpython-313.pyc b/api/routers/__pycache__/toolkit.cpython-313.pyc index 67dc3d1..324d490 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 9ba3b3b..95fe0e3 100644 --- a/api/routers/toolkit.py +++ b/api/routers/toolkit.py @@ -1,8 +1,12 @@ from fastapi import APIRouter, Depends +from sqlalchemy import select +from db import CRUD +from db.handlers.actions import StocksActions from db.handlers.categories import CategoryHandler from db.handlers.stock import PlacementHandler, StockHandler from db.handlers.toolbox import ToolboxHandler from db.handlers.toolkit import ToolkitHandler +from db.schemas.stock import Placement from utils import requestDict, logger @@ -191,3 +195,41 @@ async def manage_toolkit(reqData: dict = Depends(requestDict)): f"Управление инструментами ({action}) прошло {'успешно' if response.get('status') == 'ok' else 'неуспешно'}" ) return response + + +@router.post("/quick_action", summary="Быстрое действие") +async def quick_action(reqData: dict = Depends(requestDict)): + logger.info(f"Быстрое действие") + response = {"status": "error"} + action = reqData.get("body").get("action") + toolboxId = int(reqData.get("body").get("toolboxId")) + toolkitId = int(reqData.get("body").get("toolkitId")) + userId = reqData.get("body").get("userId") + data = reqData.get("body").get("data") + match action: + case "fill": + toolkit = await StocksActions.registration( + toolkitId, + toolboxId, + userId, + int(data.get("amount")), + float(data.get("price")), + "", + data.get("reason"), + ) + response = handleResult(toolkit, response) + case "move": + try: + placement = await CRUD.read( + select(Placement).where( + Placement.toolkit_id == toolkitId, + Placement.toolbox_id == toolboxId, + ) + ) + await placement.edit(data.get("location")) + response["status"] = "ok" + except Exception as e: + logger.error(e) + case _: + pass + return response diff --git a/api/static/css/index.css b/api/static/css/index.css index d314205..44c5d53 100644 --- a/api/static/css/index.css +++ b/api/static/css/index.css @@ -230,16 +230,19 @@ th[data-sort]:hover { .badge.bg-danger { background-color: #dc3545 !important; } /* Стили для кнопок действий в строке */ +.quick-actions, .action-buttons { opacity: 0; transition: opacity 0.2s; } +tr:hover .quick-actions, tr:hover .action-buttons { opacity: 1; } /* Уменьшаем отступы у кнопок в строках */ +.quick-actions .btn, .action-buttons .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; diff --git a/api/static/js/index.js b/api/static/js/index.js index 20c0d0a..3df0e8d 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -2057,6 +2057,7 @@ async function selectToolbox(toolboxId) { await loadToolboxContent(toolboxId); } + async function loadToolboxContent(toolboxId) { const contentContainer = document.querySelector('.toolbox-content-container'); @@ -2072,6 +2073,8 @@ async function loadToolboxContent(toolboxId) { `; + + try { const resp = await apiRequest(`/stocks/`, { toolboxId }); if (resp.status === 'ok') { @@ -3353,6 +3356,159 @@ function formatPrice(price) { return parseFloat(price).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); } + +function toolkitFillReplace(toolboxId, toolkitId, tabId) { + if (!accessData.tools_registration) return ''; + const manageBtns = ` +
+ + +
+ `; + + // Навешиваем обработчики один раз + if (!document.body.dataset.toolkitHandlers) { + document.body.dataset.toolkitHandlers = 'true'; + + document.addEventListener('click', async (e) => { + const actionBtn = e.target.closest('.toolkit-action-btn'); + if (!actionBtn) return; + + const { action, toolbox, toolkit, tab } = actionBtn.dataset; + + // Удаляем старую модалку если была + document.getElementById('toolkitDynamicModal')?.remove(); + + let modalHtml = ''; + + if (action === 'fill') { + modalHtml = ` + + `; + } + + if (action === 'move') { + modalHtml = ` + + `; + } + + document.body.insertAdjacentHTML('beforeend', modalHtml); + + const modalEl = document.getElementById('toolkitDynamicModal'); + const modal = new bootstrap.Modal(modalEl); + modal.show(); + + modalEl.querySelector('#toolkitModalSubmit').addEventListener('click', async () => { + const form = modalEl.querySelector('#toolkitModalForm'); + + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const data = Object.fromEntries(new FormData(form).entries()); + const userId = userData.id; + + const response = await apiRequest('/toolkit/quick_action', { + action, + toolboxId: toolbox, + toolkitId: toolkit, + data, + userId + }); + + if (response.status === 'ok') { + showInfo('Действие успешно выполнено', 'success'); + if (tab === 'toolbox') { await selectToolbox(toolbox); } + if (tab === 'toolkits') { + await uploadTab(tab); + showInfo('В открытом окне карточки данные не обновятся автоматически', 'info'); + } + } else { + showInfo('Произошла ошибка при выполнении действия', 'danger'); + throw new Error(response.message || 'Произошла ошибка при выполнении действия'); + } + + modal.hide(); + modalEl.remove(); + }, { once: true }); + }); + } + + return manageBtns; +} + // Функция инициализации таблицы async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { let currentPage = 1; @@ -3465,7 +3621,12 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { ${quantityMonitoring ? `${item.indicator?.text || '-'}` : ''} ${formatPrice(item.totalCost)} ₽ - ${toolboxOwn === 'Общий склад' ? `${item.placement}` : ''} + ${toolboxOwn === 'Общий склад' ? ` + + ${item.placement} + ${toolkitFillReplace(item.toolboxId, item.id, 'toolbox')} + + ` : ''} ${item.lastUpdated} `; @@ -3648,6 +3809,7 @@ async function showToolkitDetailsModal(toolkitId) { let modal = document.getElementById(modalId); if (modal) { + modal.hide(); modal.remove(); } @@ -3970,6 +4132,7 @@ async function showToolkitDetailsModal(toolkitId) { ` : ''} + ${value.id ? toolkitFillReplace(value.id, toolkitData.id, 'toolkits') : ''} `).join('')} diff --git a/db/handlers/__pycache__/actions.cpython-313.pyc b/db/handlers/__pycache__/actions.cpython-313.pyc index 862d90f..497a2dc 100644 Binary files a/db/handlers/__pycache__/actions.cpython-313.pyc and b/db/handlers/__pycache__/actions.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/stock.cpython-313.pyc b/db/handlers/__pycache__/stock.cpython-313.pyc index b8b29a1..14f6bb8 100644 Binary files a/db/handlers/__pycache__/stock.cpython-313.pyc and b/db/handlers/__pycache__/stock.cpython-313.pyc differ diff --git a/db/handlers/actions.py b/db/handlers/actions.py index ebc128e..51c8349 100644 --- a/db/handlers/actions.py +++ b/db/handlers/actions.py @@ -17,7 +17,7 @@ class StocksActions: toolbox_id: int, user_id: int, quantity: int, - price: int, + price: float, placement: str, reason: str, ) -> bool: @@ -33,7 +33,7 @@ class StocksActions: logger.error( f"Приход инструмента {toolkit_id} на складе {toolbox_id} не удалось" ) - return False + return {"errorMessage": "Приход инструмента не удалось"} recorded = await StocksRecordsHandler.add( action="Приход", @@ -64,8 +64,12 @@ class StocksActions: logger.error( f"Обновление даты последнего заполнения инструмента {toolkit_id} не удалось" ) - return accepted - return False + return ( + {"errorMessage": "Приход инструмента не удалось"} + if not accepted + else {} + ) + return {"errorMessage": "Приход инструмента не удалось"} async def moving( action: str, diff --git a/db/handlers/stock.py b/db/handlers/stock.py index 2bf3cab..b3e6cb9 100644 --- a/db/handlers/stock.py +++ b/db/handlers/stock.py @@ -103,7 +103,8 @@ class StockHandler: async def add(**kwargs) -> dict: newStock = await Stock(**kwargs).save() if "placement" in kwargs and kwargs["placement"]: - await PlacementHandler.add(**kwargs) + if kwargs["placement"] != "": + await PlacementHandler.add(**kwargs) logger.info( f"Новая запись об инструменте {newStock.toolkit_data.title} на складе {newStock.toolbox_data.title} успешно создана" )