diff --git a/api/routers/__init__.py b/api/routers/__init__.py index 29ecfc6..3f4d6f5 100644 --- a/api/routers/__init__.py +++ b/api/routers/__init__.py @@ -1,3 +1,4 @@ +from datetime import date, datetime, timedelta from fastapi import APIRouter, Depends, Request from fastapi.responses import RedirectResponse @@ -7,6 +8,7 @@ from .user import router as user from .stocks import router as stocks from .toolbox import router as toolbox from .toolkit import router as toolkit +from .records import router as records router = APIRouter() @@ -15,6 +17,7 @@ router.include_router(user, prefix="/user", tags=["user"]) router.include_router(stocks, prefix="/stocks", tags=["stocks"]) router.include_router(toolbox, prefix="/toolbox", tags=["toolbox"]) router.include_router(toolkit, prefix="/toolkit", tags=["toolkit"]) +router.include_router(records, prefix="/records", tags=["records"]) @router.get("/") @@ -50,9 +53,16 @@ async def post_requests( reqData = { "tab": request_data.get("body").get("tabId"), - "userData": request_data.get("body").get("cookiesData").get("userData"), - "accessData": request_data.get("body").get("cookiesData").get("accessData"), } + if "cookiesData" in request_data.get("body"): + if "userData" in request_data.get("body").get("cookiesData"): + reqData["userData"] = ( + request_data.get("body").get("cookiesData").get("userData") + ) + if "accessData" in request_data.get("body").get("cookiesData"): + reqData["accessData"] = ( + request_data.get("body").get("cookiesData").get("accessData") + ) resultData = {"status": "error", "data": {}} logger.info(f"Получение данных для вкладки {reqData.get('tab')}") match reqData.get("tab"): @@ -67,10 +77,23 @@ async def post_requests( resultData["status"] = "ok" resultData["data"] = toolbox case "requests": - requests = await StocksRecordsHandler.get(reqData.get("userData").get("id")) + canDesign = reqData.get("accessData").get( + "refund_request_confirm", False + ) or reqData.get("accessData").get("debit_request_confirm", False) + userId = reqData.get("userData").get("id") + requests = await StocksRecordsHandler.get(userId, canDesign) + users = await UserHandler.getAll() + toolboxes = await ToolboxHandler.getAll() + toolkitsIds = set([request.get("toolkit_id") for request in requests]) + toolkits = await ToolkitHandler.getSeveral(list(toolkitsIds)) if isinstance(requests, list): resultData["status"] = "ok" - resultData["data"] = requests + resultData["data"] = { + "requests": requests, + "users": users, + "toolboxes": toolboxes, + "toolkits": toolkits, + } case "toolkits": toolkits = await ToolkitHandler.getAll() categories = await CategoryHandler.getAll() @@ -80,12 +103,45 @@ async def post_requests( "toolkits": toolkits, "categories": categories, } - # logger.info(resultData) case "jurnal_toolkits": - jurnal_toolkits = await StocksRecordsHandler.get() - if jurnal_toolkits: - resultData["status"] = "ok" - resultData["data"] = jurnal_toolkits + startDate = request_data.get("body").get( + "startDate", date.today() - timedelta(days=7) + ) + if isinstance(startDate, str): + startDate = datetime.strptime(startDate, "%Y-%m-%d").date() + + endDate = request_data.get("body").get("endDate", date.today()) + if isinstance(endDate, str): + endDate = datetime.strptime(endDate, "%Y-%m-%d").date() + + jurnal_toolkits = await StocksRecordsHandler.getLogs(startDate, endDate) + if isinstance(jurnal_toolkits, list): + if len(jurnal_toolkits) == 0: + resultData["status"] = "ok" + resultData["data"] = { + "requests": [], + "users": [], + "toolboxes": [], + "toolkits": [], + "startDate": startDate.strftime("%Y-%m-%d"), + "endDate": endDate.strftime("%Y-%m-%d"), + } + else: + users = await UserHandler.getAll() + toolboxes = await ToolboxHandler.getAll() + toolkitsIds = set( + [request.get("toolkit_id") for request in jurnal_toolkits] + ) + toolkits = await ToolkitHandler.getSeveral(list(toolkitsIds)) + resultData["status"] = "ok" + resultData["data"] = { + "requests": jurnal_toolkits, + "users": users, + "toolboxes": toolboxes, + "toolkits": toolkits, + "startDate": startDate.strftime("%Y-%m-%d"), + "endDate": endDate.strftime("%Y-%m-%d"), + } case "jurnal_service": jurnal_service = await ServiceRecordsHandler.get() if jurnal_service: diff --git a/api/routers/__pycache__/__init__.cpython-313.pyc b/api/routers/__pycache__/__init__.cpython-313.pyc index 61f5e61..8fe3f59 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__/records.cpython-313.pyc b/api/routers/__pycache__/records.cpython-313.pyc new file mode 100644 index 0000000..687ad18 Binary files /dev/null and b/api/routers/__pycache__/records.cpython-313.pyc differ diff --git a/api/routers/__pycache__/stocks.cpython-313.pyc b/api/routers/__pycache__/stocks.cpython-313.pyc index d2f120b..a20337c 100644 Binary files a/api/routers/__pycache__/stocks.cpython-313.pyc and b/api/routers/__pycache__/stocks.cpython-313.pyc differ diff --git a/api/routers/__pycache__/toolbox.cpython-313.pyc b/api/routers/__pycache__/toolbox.cpython-313.pyc index ee76735..4b966de 100644 Binary files a/api/routers/__pycache__/toolbox.cpython-313.pyc and b/api/routers/__pycache__/toolbox.cpython-313.pyc differ diff --git a/api/routers/__pycache__/toolkit.cpython-313.pyc b/api/routers/__pycache__/toolkit.cpython-313.pyc index 8470eef..775dbcb 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/records.py b/api/routers/records.py new file mode 100644 index 0000000..26e5353 --- /dev/null +++ b/api/routers/records.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from db.handlers.actions import StocksActions +from utils import requestDict, logger + + +router = APIRouter() + + +@router.post("/") +async def post_requests( + reqData: dict = Depends(requestDict), +): + logger.info("Получение записи о перемещении инструмента") + request_id = reqData.get("body").get("request_id") + user_id = reqData.get("body").get("user_id") + accepted = reqData.get("body").get("accepted") + if request_id and user_id and accepted is not None: + result = await StocksActions.movingDecision(int(request_id), user_id, accepted) + if result: + return {"status": "ok"} + return {"status": "error"} diff --git a/api/routers/stocks.py b/api/routers/stocks.py index a5cb75c..2cc13e2 100644 --- a/api/routers/stocks.py +++ b/api/routers/stocks.py @@ -15,7 +15,7 @@ router = APIRouter() async def post_requests( request_data: dict = Depends(requestDict), ): - toolboxId = request_data.get("body").get("toolboxId") + toolboxId = int(request_data.get("body").get("toolboxId")) logger.info(f"Получение инструментов для тулбокса {toolboxId}") response = {"status": "error", "data": []} diff --git a/api/routers/toolbox.py b/api/routers/toolbox.py index 1f5ba6d..ada403c 100644 --- a/api/routers/toolbox.py +++ b/api/routers/toolbox.py @@ -54,7 +54,6 @@ async def delete_toolbox(reqDict=Depends(requestDict)): @router.post("/fill", summary="Заполнение ящика") async def fill_toolbox(reqDict=Depends(requestDict)): logger.info(f"Заполнение ящика") - logger.info(reqDict.get("body")) response = {"status": "error"} toolboxId = reqDict.get("body").get("toolboxId") userId = reqDict.get("body").get("userId") @@ -79,5 +78,4 @@ async def fill_toolbox(reqDict=Depends(requestDict)): response["message"] = ( f"Оприходовано {successCount} записей из {len(items)}. Проверьте остатки и повторите попытку" ) - logger.info(response) return response diff --git a/api/routers/toolkit.py b/api/routers/toolkit.py index a1bd68c..7185226 100644 --- a/api/routers/toolkit.py +++ b/api/routers/toolkit.py @@ -25,7 +25,6 @@ async def get_toolkit(reqData: dict = Depends(requestDict)): if toolkitId: toolkit = await ToolkitHandler.get(int(toolkitId)) if toolkit: - # logger.info(toolkit) response["status"] = "ok" response["data"] = toolkit return response diff --git a/api/static/js/index.js b/api/static/js/index.js index 096b881..328c724 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -87,6 +87,10 @@ async function openTab(event, tabId) { event.currentTarget.querySelector('.nav-icon').classList.add('text-primary'); document.getElementById(tabId).classList.add('show', 'active'); + // Сохраняем выбранную вкладку + saveToStorage('tab', { tabId }); + + // Загружаем содержимое вкладки await uploadTab(tabId); } @@ -104,7 +108,7 @@ function prepareTabs() { }, }; - if (accessData.available_own_toolbox) { + if (accessData.available_own_toolbox || accessData.refund_request_confirm || accessData.debit_request_confirm) { tabsData['requests'] = { title: 'Запросы', icon: 'bi-chat-left-text', @@ -112,7 +116,7 @@ function prepareTabs() { }; } - if (accessData.view_services) { + if (accessData.view_requests) { tabsData['jurnal_toolkits'] = { title: 'Журнал перемещений', icon: 'bi-journal-text', @@ -120,7 +124,7 @@ function prepareTabs() { }; } - if (accessData.view_requests) { + if (accessData.view_services) { tabsData['jurnal_service'] = { title: 'Сервисный журнал', icon: 'bi-journal-richtext', @@ -210,6 +214,15 @@ function prepareTabs() { const mainContainer = document.getElementById('mainContent'); mainContainer.insertAdjacentHTML('afterbegin', tabs); + + const activeTabData = loadFromStorage('tab') || null; + if (activeTabData) { + const activeTabId = activeTabData.tabId; + const tabBtn = document.getElementById(`${activeTabId}-tab`); + if (tabBtn) { + openTab({ currentTarget: tabBtn }, activeTabId); + } + } } async function uploadTab(tabId) { @@ -242,13 +255,13 @@ function fillTab(tabId, tabData) { renderToolboxTab(tabData); break; case 'requests': - renderSimpleTab(tabId, tabData, 'Запросы на инструменты'); + renderRequestsTab(tabId, tabData); break; case 'toolkits': renderToolkitsTab(tabId, tabData.toolkits, tabData.categories); break; case 'jurnal_toolkits': - renderSimpleTab(tabId, tabData, 'Журнал перемещений'); + renderJurnalToolkitsTab(tabId, tabData); break; case 'jurnal_service': renderSimpleTab(tabId, tabData, 'Сервисный журнал'); @@ -1235,11 +1248,29 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) { addToolBtn.addEventListener('click', () => manageToolkit()); } else { tabOptionalContent.innerHTML = ` -
-
-
+
+ +
+
+ ${categoriesArray.map(category => ` + + `).join('')} + +
+
+ + +
+
- +
@@ -1617,18 +1648,18 @@ function setupFilters(tabId, tools, categoriesMap, specData) { render(); } -function loadFromStorage(tabId) { +function loadFromStorage(title) { try { - return JSON.parse(localStorage.getItem(`toolboxStotage:${tabId}`)) || {}; + return JSON.parse(localStorage.getItem(`toolboxStotage:${title}`)) || {}; } catch { return {}; } } -function saveToStorage(tabId, storageData) { +function saveToStorage(title, data) { localStorage.setItem( - `toolboxStotage:${tabId}`, - JSON.stringify(storageData) + `toolboxStotage:${title}`, + JSON.stringify(data) ); } @@ -1948,12 +1979,11 @@ function renderToolboxTab(tabData) { // Сортируем список складов по названию tabData.sort((a, b) => a.title.localeCompare(b.title, 'ru')); - const toolboxNav = ` + tabOptionalContent.innerHTML = `
${tabData.map((toolbox, index) => ` -
`; + const toolboxNav = document.getElementById('toolboxNav'); + toolboxNav.addEventListener('click', async (event) => { + const button = event.target.closest('.toolbox-nav-btn'); + if (!button) return; + if (button.dataset.toolboxId) { + try { + await selectToolbox(button.dataset.toolboxId); + } catch (err) { + console.error('Ошибка выбора склада:', err); + } + } + }); + // Создаем контейнер для содержимого склада - const toolboxContent = ` + tabContent.innerHTML = `
@@ -1979,12 +2022,10 @@ function renderToolboxTab(tabData) {
`; - tabOptionalContent.innerHTML = toolboxNav; - if (accessData.manage_toolboxes) { const addToolboxBtn = document.createElement('button'); - addToolboxBtn.className = 'btn btn-outline-success toolbox-nav-btn d-flex align-items-center mb-2'; + addToolboxBtn.className = 'btn btn-outline-success toolbox-nav-btn'; addToolboxBtn.innerHTML = ` Добавить @@ -1996,33 +2037,41 @@ function renderToolboxTab(tabData) { document.getElementById('toolboxNav').appendChild(addToolboxBtn); } - tabContent.innerHTML = toolboxContent; - const choiceToolbox = loadFromStorage('toolbox'); if (choiceToolbox.toolboxId) { - window.selectToolbox(choiceToolbox.toolboxId).then(() => { }).catch(() => { }); + (async () => { + await selectToolbox(choiceToolbox.toolboxId); + })().catch(err => { + console.error('Ошибка выбора склада:', err); + }); } } - // Функция для выбора склада -window.selectToolbox = async function (toolboxId, index) { - // Убираем активный класс со всех кнопок складов - document.querySelectorAll('.toolbox-nav-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - // Добавляем активный класс выбранной кнопке - const selectedBtn = document.querySelector(`.toolbox-nav-btn[data-toolbox-id="${toolboxId}"]`); - if (selectedBtn) { - selectedBtn.classList.add('active'); +async function selectToolbox(toolboxId) { + if (typeof toolboxId === 'string') { + try { + toolboxId = parseInt(toolboxId); + } catch (err) { + console.error('Неверный идентификатор склада:', toolboxId); + return; + } } + document.querySelectorAll('.toolbox-nav-btn') + .forEach(btn => btn.classList.remove('active')); + + const selectedBtn = document.querySelector( + `.toolbox-nav-btn[data-toolbox-id="${toolboxId}"]` + ); + + selectedBtn?.classList.add('active'); saveToStorage('toolbox', { toolboxId }); - // Загружаем содержимое склада + await loadToolboxContent(toolboxId); } async function loadToolboxContent(toolboxId) { + const contentContainer = document.querySelector('.toolbox-content-container'); // Показываем индикатор загрузки @@ -2214,12 +2263,15 @@ async function loadToolboxContent(toolboxId) { Не удалось загрузить содержимое склада
${error.message}

-
`; + + // Добавляем обработчик для кнопки "Попробовать снова" + contentContainer.querySelector('#tryAgainBtn').addEventListener('click', async () => await selectToolbox(toolboxId)); } } @@ -3326,7 +3378,6 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { async function initializePagination() { const totalPages = Math.ceil(filteredData.length / itemsPerPage); const paginationContainer = document.getElementById('toolboxPagination'); - const tbody = document.getElementById('toolboxItemsBody'); // Очищаем текущее содержимое paginationContainer.innerHTML = ''; @@ -3443,6 +3494,7 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { const itemId = e.currentTarget.dataset.id; const selectedItem = data.find(d => d.id == itemId); if (selectedItem) { + selectedItem.skipRefresh = true; await showOperationModal(action, selectedItem); } }); @@ -5107,26 +5159,736 @@ async function actionRequest(operation, quantity, comment, selectedItem) { } } -function formatKey(key) { - const keyMap = { - 'id': 'ID', - 'title': 'Название', - 'description': 'Описание', - 'owner_id': 'ID владельца', - 'monitoring': 'Мониторинг', - 'created_at': 'Дата создания', - 'updated_at': 'Дата обновления' +function renderRequestsTab(tabId, tabData) { + const tabContent = document.getElementById(`${tabId}-tab-content`); + const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); + + const { requests, users, toolboxes, toolkits } = tabData; + + // Собираем списки для фильтров + const initUsers = [...new Set(requests.map(r => r.init_user_id))]; + const userMap = {}; + users.forEach(user => { + userMap[user.id] = user.username; + }); + + const actionTypes = [...new Set(requests.map(r => r.action))]; + + const ownRequestsCount = requests.filter(r => r.init_user_id === userData.id).length; + + // Создаем мапу для toolboxes + const toolboxMap = {}; + toolboxes.forEach(box => { + toolboxMap[box.id] = box.title; + }); + + // Создаем мапу для toolkits + const toolkitMap = {}; + toolkits.forEach(kit => { + toolkitMap[kit.id] = kit.title; + }); + + // Фильтры + let currentFilters = { + user: 'all', + action: 'all', }; - return keyMap[key] || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' '); + + // Рендерим дополнительный контейнер с фильтрами + tabOptionalContent.innerHTML = ` +
+ +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + +
+
+ ${(accessData.refund_request_confirm || accessData.debit_request_confirm) ? ` +
+ + +
+ ` : ''} + ${ownRequestsCount > 0 ? ` + + ` : ''} +
+
+
+ `; + + // Рендерим основной контейнер с таблицей запросов + tabContent.innerHTML = ` +
+
+
+ + + + + + + + + + + + + + + + +
ТипОформилСо складаНа складИнструментКол-воОбоснованиеДействия
+
+ +
+
+ `; + + // Функция для фильтрации запросов + function filterRequests() { + let filtered = requests; + + // Фильтр по пользователю + if (currentFilters.user !== 'all') { + filtered = filtered.filter(r => r.init_user_id == currentFilters.user); + } + + // Фильтр по типу действия + if (currentFilters.action !== 'all') { + filtered = filtered.filter(r => r.action === currentFilters.action); + } + + return filtered; + } + + // Функция для рендеринга строк таблицы + function renderRequestsTable() { + const tbody = document.getElementById(`${tabId}-requests-body`); + const noRequestsDiv = document.getElementById(`${tabId}-no-requests`); + const filteredRequests = filterRequests(); + + if (filteredRequests.length === 0) { + tbody.innerHTML = ''; + noRequestsDiv.style.display = 'block'; + return; + } + + noRequestsDiv.style.display = 'none'; + + tbody.innerHTML = filteredRequests.map(request => { + + // Определяем доступные действия + const actions = []; + + // Кнопка отзыва (только для инициатора и неподтвержденных запросов) + if (request.init_user_id === userData.id && request.accepted === null) { + actions.push(` + + `); + } + + // Кнопки принятия/отклонения (в зависимости от прав) + let canDecide = false; + + // Проверяем права в зависимости от типа запроса + if (request.action === 'Возврат' && accessData.refund_request_confirm) { + canDecide = true; + } else if (request.action === 'Списание' && accessData.debit_request_confirm) { + canDecide = true; + } else if (request.action !== 'Возврат' && request.action !== 'Списание' && + (accessData.refund_request_confirm || accessData.debit_request_confirm)) { + // Для других типов запросов, если есть хотя бы одно из прав + console.warning('Unknown request action', request.action); + canDecide = true; + } + + if (canDecide) { + actions.push(` +
+ + +
+ `); + } + + // Если нет доступных действий + if (actions.length === 0) { + actions.push('Нет действий'); + } + + return ` + + + ${request.action} + ${request.created_at} + + ${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`} + ${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'} + ${request.target_toolbox_id ? (toolboxMap[request.target_toolbox_id] || `Склад ${request.target_toolbox_id}`) : '-'} + ${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'} + + ${request.quantity} + + + ${request.reason || 'Нет обоснования'} + + +
+ ${actions.join('')} +
+ + + `; + }).join(''); + } + + // Функция для показа модального окна подтверждения + function showConfirmationModal(title, message, onConfirm) { + // Проверяем, есть ли уже модальное окно + let modal = document.getElementById('confirmation-modal'); + + if (!modal) { + // Создаем модальное окно + modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.id = 'confirmation-modal'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + } + + // Устанавливаем содержимое + document.getElementById('confirmation-message').innerHTML = message; + + // Очищаем предыдущие обработчики + const confirmBtn = document.getElementById('confirm-action-btn'); + const oldConfirmBtn = confirmBtn.cloneNode(true); + confirmBtn.parentNode.replaceChild(oldConfirmBtn, confirmBtn); + + const newConfirmBtn = document.getElementById('confirm-action-btn'); + + // Устанавливаем новый обработчик + newConfirmBtn.addEventListener('click', function () { + const modalInstance = bootstrap.Modal.getInstance(modal); + modalInstance.hide(); + onConfirm(); + }); + + // Показываем модальное окно + const modalInstance = new bootstrap.Modal(modal); + modalInstance.show(); + } + + async function sendRequestDecision(requestId, accepted, requestResult = null) { + const data = await apiRequest('/records/', { request_id: requestId, user_id: userData.id, accepted: accepted }); + if (data.status == 'ok') { + const requestIndex = requests.findIndex(r => r.id == requestId); + if (requestIndex !== -1) { + requests.splice(requestIndex, 1); + } + // Перерисовываем таблицу + renderRequestsTable(); + + // Показываем уведомление об успехе + requestResult = requestResult === null ? (accepted ? 'Принят' : 'Отклонен') : requestResult; + showInfo(`Запрос успешно ${requestResult}`, 'success'); + } else { + const errorMessage = data.message || 'Ошибка сервера'; + showInfo(errorMessage, 'error'); + throw new Error(errorMessage); + } + } + + // Функция для обработки решения по запросу + function handleRequestDecision(requestId, accepted) { + const action = accepted ? 'принять' : 'отклонить'; + + showConfirmationModal( + `Подтверждение действия`, + `Вы уверены, что хотите ${action} этот запрос?`, + async () => { + await sendRequestDecision(requestId, accepted); + } + ); + } + + // Функция для отзыва запроса + function handleRequestWithdrawal(requestId) { + showConfirmationModal( + 'Отзыв запроса', + 'Вы уверены, что хотите отозвать этот запрос?', + async () => { + await sendRequestDecision(requestId, false, 'Отозван'); + } + ); + } + + // Функция для массовых действий + function handleBulkAction(actionType) { + const filteredRequests = filterRequests(); + + // Фильтруем только те запросы, с которыми можно совершить действие + let applicableRequests = filteredRequests; + + if (actionType === 'accept' || actionType === 'reject') { + // Для принятия/отклонения: только ожидающие решения + applicableRequests = filteredRequests.filter(r => r.accepted === null); + + // Проверяем права для каждого запроса + applicableRequests = applicableRequests.filter(r => { + if (r.action === 'Возврат') { + return accessData.refund_request_confirm; + } else if (r.action === 'Списание') { + return accessData.debit_request_confirm; + } else { + return accessData.refund_request_confirm || accessData.debit_request_confirm; + } + }); + } else if (actionType === 'withdraw') { + // Для отзыва: только мои и ожидающие решения + applicableRequests = filteredRequests.filter(r => + r.init_user_id === userData.id && r.accepted === null + ); + } + + if (applicableRequests.length === 0) { + showInfo('Нет подходящих запросов для этого действия', 'warning'); + return; + } + + const actionName = actionType === 'accept' ? 'принять' : + actionType === 'reject' ? 'отклонить' : 'отозвать'; + + showConfirmationModal( + 'Массовое действие', + `Вы уверены, что хотите ${actionName} все отправленные запросы (${applicableRequests.length})?`, + async () => { + // Отправляем запросы на сервер + const promises = applicableRequests.map(request => + sendRequestDecision(request.id, actionType === 'accept') + ); + await Promise.all(promises); + } + ); + } + + + // Назначаем обработчики событий для фильтров + document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () { + currentFilters.user = this.value; + renderRequestsTable(); + }); + + document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () { + currentFilters.action = this.value; + renderRequestsTable(); + }); + + // Назначаем обработчики для массовых действий + const acceptAllBtn = document.getElementById(`${tabId}-accept-all-btn`); + const rejectAllBtn = document.getElementById(`${tabId}-reject-all-btn`); + const withdrawAllBtn = document.getElementById(`${tabId}-withdraw-all-btn`); + + if (acceptAllBtn) { + acceptAllBtn.addEventListener('click', () => handleBulkAction('accept')); + } + + if (rejectAllBtn) { + rejectAllBtn.addEventListener('click', () => handleBulkAction('reject')); + } + + if (withdrawAllBtn) { + withdrawAllBtn.addEventListener('click', () => handleBulkAction('withdraw')); + } + + // Назначаем делегированные обработчики для действий в таблице + document.getElementById(`${tabId}-requests-body`).addEventListener('click', function (e) { + const target = e.target; + + // Находим ближайшую кнопку или родительскую кнопку + const button = target.closest('.accept-btn, .reject-btn, .withdraw-btn'); + if (!button) return; + + const requestId = button.dataset.requestId; + + if (button.classList.contains('accept-btn')) { + handleRequestDecision(requestId, true); + } else if (button.classList.contains('reject-btn')) { + handleRequestDecision(requestId, false); + } else if (button.classList.contains('withdraw-btn')) { + handleRequestWithdrawal(requestId); + } + }); + + // Первоначальный рендеринг таблицы + renderRequestsTable(); } -function formatValue(value) { - if (value === null || value === undefined) return '—'; - if (typeof value === 'boolean') return value ? 'Да' : 'Нет'; - if (typeof value === 'object') return JSON.stringify(value); - return value.toString(); -} +function renderJurnalToolkitsTab(tabId, tabData) { + const tabContent = document.getElementById(`${tabId}-tab-content`); + const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); + + const { requests, users, toolboxes, toolkits, startDate, endDate } = tabData; + + if (requests.length === 0) { + tabContent.innerHTML = ` + + `; + return; + } + + // Собираем списки для фильтров + const initUsers = [...new Set(requests.map(r => r.init_user_id))]; + const userMap = {}; + users.forEach(user => { + userMap[user.id] = user.username; + }); + + const actionTypes = [...new Set(requests.map(r => r.action))]; + + // Создаем мапу для toolboxes + const toolboxMap = {}; + toolboxes.forEach(box => { + toolboxMap[box.id] = box.title; + }); + + // Создаем мапу для toolkits + const toolkitMap = {}; + toolkits.forEach(kit => { + toolkitMap[kit.id] = kit.title; + }); + + const savedFilters = loadFromStorage(tabId); + // Фильтры + let currentFilters = { + user: savedFilters?.user || 'all', + action: savedFilters?.action || 'all', + status: savedFilters?.status || 'all' + }; + + // Рендерим дополнительный контейнер с фильтрами + tabOptionalContent.innerHTML = ` +
+ +
+
+
+
+ + + + +
+
+
+ +
+
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+ + Дата начала: + + +
+
+ + Дата окончания: + + +
+
+ +
+
+
+
+ `; + + const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`); + filterResetBtn.addEventListener('click', () => { + currentFilters = { + user: 'all', + action: 'all', + status: 'all' + }; + document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; + document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; + document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; + saveToStorage(tabId, currentFilters); + renderRequestsTable(); + }); + + const startDateInput = document.getElementById(`${tabId}-date-from`); + const endDateInput = document.getElementById(`${tabId}-date-to`); + startDateInput.value = startDate; + endDateInput.value = endDate; + + const refreshDateBtn = document.getElementById(`${tabId}-date-update-btn`); + refreshDateBtn.addEventListener('click', async () => { + const newStartDate = startDateInput.value; + const newEndDate = endDateInput.value; + const newDateRequestData = { + tabId: tabId, + startDate: newStartDate, + endDate: newEndDate + }; + if (newStartDate && newEndDate) { + tabContent.innerHTML = ` + + `; + const newPeriodData = await apiRequest('/', newDateRequestData); + if (newPeriodData.status == 'ok') { + renderJurnalToolkitsTab(tabId, newPeriodData.data); + } + } + }); + + // Рендерим основной контейнер с таблицей запросов + tabContent.innerHTML = ` +
+
+
+ + + + + + + + + + + + + + + + +
ТипОформилРешилСо складаНа складИнструментКол-воОбоснование
+
+ +
+
+ `; + + // Функция для фильтрации запросов + function filterRequests() { + let filtered = requests; + + // Фильтр по пользователю + if (currentFilters.user !== 'all') { + filtered = filtered.filter(r => r.init_user_id == currentFilters.user); + document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; + } + + // Фильтр по типу действия + if (currentFilters.action !== 'all') { + filtered = filtered.filter(r => r.action === currentFilters.action); + document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; + } + + // Фильтр по статусу + if (currentFilters.status !== 'all') { + document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; + switch (currentFilters.status) { + case 'accepted': + filtered = filtered.filter(r => r.accepted === true); + break; + case 'rejected': + filtered = filtered.filter(r => r.accepted === false); + break; + } + } + + return filtered; + } + + // Функция для рендеринга строк таблицы + function renderRequestsTable() { + const tbody = document.getElementById(`${tabId}-requests-body`); + const noRequestsDiv = document.getElementById(`${tabId}-no-requests`); + const filteredRequests = filterRequests(); + + if (filteredRequests.length === 0) { + tbody.innerHTML = ''; + noRequestsDiv.style.display = 'block'; + return; + } + + noRequestsDiv.style.display = 'none'; + + tbody.innerHTML = filteredRequests.map(request => { + // Определяем статус запроса + let statusBadge = ''; + if (request.accepted === true) { + statusBadge = 'Принято'; + } else { + statusBadge = 'Отклонено'; + } + + return ` + + + ${request.action} + ${statusBadge} + + ${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}
${request.created_at} + ${userMap[request.decision_user_id] || `Пользователь ${request.decision_user_id}`}
${request.decided_at} + ${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'} + ${request.target_toolbox_id ? (toolboxMap[request.target_toolbox_id] || `Склад ${request.target_toolbox_id}`) : '-'} + ${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'} + + ${request.quantity} + + + ${request.reason || 'Нет обоснования'} + + + `; + }).join(''); + } + + // Назначаем обработчики событий для фильтров + document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () { + currentFilters.user = this.value; + saveToStorage(tabId, currentFilters); + renderRequestsTable(); + }); + + document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () { + currentFilters.action = this.value; + saveToStorage(tabId, currentFilters); + renderRequestsTable(); + }); + + document.getElementById(`${tabId}-status-filter`).addEventListener('change', function () { + currentFilters.status = this.value; + saveToStorage(tabId, currentFilters); + renderRequestsTable(); + }); + + // Первоначальный рендеринг таблицы + renderRequestsTable(); +} document.addEventListener('DOMContentLoaded', async () => { await getCookieData(); diff --git a/api/static/js/user.js b/api/static/js/user.js index ebb923a..44a5832 100644 --- a/api/static/js/user.js +++ b/api/static/js/user.js @@ -120,6 +120,9 @@ class ClientManager { // Очищаем cookie пользователя this.clearUserCookie(); + // Очищаем локальное хранилище + localStorage.clear(); + // Переход на страницу выхода setTimeout(() => { window.location.href = '/user/login'; diff --git a/db/handlers/__pycache__/access.cpython-313.pyc b/db/handlers/__pycache__/access.cpython-313.pyc index b70ed85..96388f2 100644 Binary files a/db/handlers/__pycache__/access.cpython-313.pyc and b/db/handlers/__pycache__/access.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/actions.cpython-313.pyc b/db/handlers/__pycache__/actions.cpython-313.pyc index bda3e2d..862d90f 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__/records.cpython-313.pyc b/db/handlers/__pycache__/records.cpython-313.pyc index ded9ab5..dd10b1e 100644 Binary files a/db/handlers/__pycache__/records.cpython-313.pyc and b/db/handlers/__pycache__/records.cpython-313.pyc differ diff --git a/db/handlers/access.py b/db/handlers/access.py index 037354e..da16f38 100644 --- a/db/handlers/access.py +++ b/db/handlers/access.py @@ -90,21 +90,16 @@ class AccessLevelHandler: "admin": { "title": "Администратор", "description": "Администратор. Полный доступ", - "receiving_edit": True, - "refund_request_edit": True, "refund_request_confirm": True, - "debit_request_edit": True, "debit_request_confirm": True, "tools_creation": True, "tools_registration": True, - "tools_registration_edit": True, "tools_edit": True, "tools_delete": True, "users_creation": True, "users_edit": True, "users_disabling": True, "users_view": True, - "available_own_toolbox": False, "view_all_toolboxes": True, "view_requests": True, "view_services": True, diff --git a/db/handlers/actions.py b/db/handlers/actions.py index cb62598..ebc128e 100644 --- a/db/handlers/actions.py +++ b/db/handlers/actions.py @@ -270,7 +270,7 @@ class StocksActions: if not accept: return accept - totalRecordsIds = [record_id] + totalRecordsIds = [record_id] if not record else [record.id] if len(stocksMovements) > 1: for stock in stocksMovements[1:]: @@ -298,7 +298,7 @@ class StocksActions: ) if not accept: return False - totalRecordsIds.append(recorded) + totalRecordsIds.append(recorded.id) logger.info( f"Записи {', '.join(map(str, totalRecordsIds))} о {movingRecord.action} инструмента успешно приняты {user_id}" diff --git a/db/handlers/records.py b/db/handlers/records.py index 601b6d3..a4129e4 100644 --- a/db/handlers/records.py +++ b/db/handlers/records.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import date, datetime, time, timedelta from sqlalchemy import select @@ -142,39 +142,69 @@ class StocksRecordsHandler: logger.error(f"Ошибка обновления записи: {str(e)}") return False - async def get(user_id: int = None, days: int = 30): + async def getLogs(startDate: date, endDate: date): from db import CRUD try: - if user_id: - userInfo = f"пользователя {user_id} " - decided = "не решенных " - daysLimit = "" - query = select(StocksRecords).where( - StocksRecords.init_user_id == user_id, - StocksRecords.decision_user_id == None, + start_dt = datetime.combine(startDate, time.min) + end_dt = datetime.combine(endDate, time.max) + + query = ( + select(StocksRecords) + .where( + StocksRecords.created_at.between(start_dt, end_dt), + StocksRecords.decision_user_id != None, + ) + .order_by(StocksRecords.created_at.desc()) + ) + + logger.debug("Получение записей за период %s - %s", startDate, endDate) + + records = await CRUD.read(query, True) + + logger.debug( + "%d записей за период %s - %s успешно получены", + len(records), + startDate, + endDate, + ) + + return [record.toDict() for record in records] + + except Exception: + logger.exception("Ошибка получения записей") + return [] + + async def get(user_id: int, manager: bool): + from db import CRUD + + try: + if manager: + query = ( + select(StocksRecords) + .where( + StocksRecords.decision_user_id == None, + ) + .order_by(StocksRecords.created_at.asc()) ) else: - userInfo = "всех пользователей " - decided = "" - daysLimit = f"за последние {days} дн." - query = select(StocksRecords).where( - StocksRecords.created_at > datetime.now() - timedelta(days=days), + query = ( + select(StocksRecords) + .where( + StocksRecords.init_user_id == user_id, + StocksRecords.decision_user_id == None, + ) + .order_by(StocksRecords.created_at.asc()) ) - logger.debug(f"Получение всех {decided}записей {userInfo}{daysLimit}") + logger.debug(f"Получение всех записей без решения") records = await CRUD.read(query, True) - logger.debug( - f"{len(records)} {decided}записей {userInfo}{daysLimit} успешно получены" - ) - if len(records) == 0: - return [] - records.sort(key=lambda x: x.created_at, reverse=True) + logger.debug(f"{len(records)} записей без решения успешно получены") recordsData = [record.toDict() for record in records] logger.debug(recordsData) return recordsData except Exception as e: logger.error(f"Ошибка получения записей: {str(e)}") - return False + return [] async def getById(record_id: int, record: bool = False): from db import CRUD diff --git a/db/schemas/__pycache__/access.cpython-313.pyc b/db/schemas/__pycache__/access.cpython-313.pyc index 6b44e0c..9d5344d 100644 Binary files a/db/schemas/__pycache__/access.cpython-313.pyc and b/db/schemas/__pycache__/access.cpython-313.pyc differ diff --git a/db/schemas/access.py b/db/schemas/access.py index a281ef6..76fe808 100644 --- a/db/schemas/access.py +++ b/db/schemas/access.py @@ -14,14 +14,10 @@ class AccessLevel(Base): updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # permissions - receiving_edit = Column(Boolean, default=False) - refund_request_edit = Column(Boolean, default=False) refund_request_confirm = Column(Boolean, default=False) - debit_request_edit = Column(Boolean, default=False) debit_request_confirm = Column(Boolean, default=False) tools_creation = Column(Boolean, default=False) tools_registration = Column(Boolean, default=False) - tools_registration_edit = Column(Boolean, default=False) tools_edit = Column(Boolean, default=False) tools_delete = Column(Boolean, default=False) users_creation = Column(Boolean, default=False)