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} успешно создана"
)