Добавление общих складов и удаление пустых ни разу не использованных общих складов

This commit is contained in:
2025-12-08 23:35:14 +03:00
parent 2a04f71e0c
commit 307f970d28
17 changed files with 755 additions and 62 deletions
Binary file not shown.
+4
View File
@@ -4,12 +4,16 @@ from db.handlers.categories import CategoryHandler
from utils import render, requestDict, logger from utils import render, requestDict, logger
from .user import router as user from .user import router as user
from .stocks import router as stocks from .stocks import router as stocks
from .toolbox import router as toolbox
from .toolkit import router as toolkit
router = APIRouter() router = APIRouter()
router.include_router(user, prefix="/user", tags=["user"]) router.include_router(user, prefix="/user", tags=["user"])
router.include_router(stocks, prefix="/stocks", tags=["stocks"]) 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.get("/") @router.get("/")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -41
View File
@@ -11,16 +11,17 @@ from utils import requestDict, logger
router = APIRouter() router = APIRouter()
@router.post("/") @router.post("/", summary="Получение инструментов для тулбокса")
async def post_requests( async def post_requests(
request_data: dict = Depends(requestDict), request_data: dict = Depends(requestDict),
): ):
toolboxId = request_data.get("body").get("toolboxId") toolboxId = request_data.get("body").get("toolboxId")
logger.info(f"Получение инструментов для тулбокса {toolboxId}") logger.info(f"Получение инструментов для тулбокса {toolboxId}")
response = {"status": "error", "data": {}} response = {"status": "error", "data": []}
stocksData = await StockHandler.getByToolboxId(toolboxId) stocksData = await StockHandler.getByToolboxId(toolboxId)
if not stocksData: if not stocksData:
response["status"] = "ok"
return response return response
toolkitsIds = set(stock["toolkit_id"] for stock in stocksData) toolkitsIds = set(stock["toolkit_id"] for stock in stocksData)
toolkitsData = await ToolkitHandler.getSeveral(list(toolkitsIds)) toolkitsData = await ToolkitHandler.getSeveral(list(toolkitsIds))
@@ -107,42 +108,3 @@ async def post_requests(
if result: if result:
resonse["status"] = "ok" resonse["status"] = "ok"
return resonse return resonse
@router.post("/toolkit", summary="Запрос остатка инструмента")
async def toolkit_request(
request_data: dict = Depends(requestDict),
):
response = {"status": "error", "data": {}}
logger.info(f"Получение запроса остатка инструмента")
# logger.info(request_data)
toolkitId = request_data.get("body").get("toolkitId")
stocks = await StockHandler.getByToolkitId(toolkitId)
if not stocks:
return response
userId = request_data.get("body").get("userId")
allToolboxes = request_data.get("body").get("allToolboxes")
toolboxes = (
await ToolboxHandler.getByOwner(userId)
if not allToolboxes
else await ToolboxHandler.getAll()
)
if not toolboxes:
return response
toolboxesTitles = {toolbox["id"]: toolbox["title"] for toolbox in toolboxes}
stocksData = {"count": 0, "toolboxes": {}}
for stock in stocks:
toolboxTitle = toolboxesTitles.get(stock["toolbox_id"], None)
if not toolboxTitle:
continue
stocksData["count"] += stock["quantity"]
if toolboxTitle not in stocksData["toolboxes"]:
stocksData["toolboxes"][toolboxTitle] = {
"count": stock["quantity"],
"placement": stock["placement"],
}
else:
stocksData["toolboxes"][toolboxTitle]["count"] += stock["quantity"]
response["status"] = "ok"
response["data"] = stocksData
return response
+38
View File
@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends
from db.handlers.stock import StockHandler
from db.handlers.toolbox import ToolboxHandler
from utils import requestDict, logger
router = APIRouter()
@router.post("/", summary="Добавление ящика")
async def add_toolbox(reqDict=Depends(requestDict)):
logger.info(f"Добавление ящика")
response = {"status": "error"}
userId = reqDict.get("body").get("userId")
toolboxData = reqDict.get("body").get("toolboxData")
result = await ToolboxHandler.add(toolboxData, userId)
if result:
response["status"] = "ok"
logger.info(response)
return response
@router.delete("/", summary="Удаление ящика")
async def delete_toolbox(reqDict=Depends(requestDict)):
toolboxId = reqDict.get("body").get("toolboxId")
logger.info(f"Удаление ящика #{toolboxId}")
response = {"status": "error"}
stocksData = await StockHandler.getByToolboxId(toolboxId, False)
if stocksData:
response["message"] = (
"Через этот склад были проведены операции, удаление невозможно"
)
return response
userId = reqDict.get("body").get("userId")
result = await ToolboxHandler.delete(toolboxId, userId)
if result:
response["status"] = "ok"
return response
+46
View File
@@ -0,0 +1,46 @@
from fastapi import APIRouter, Depends
from db.handlers.stock import StockHandler
from db.handlers.toolbox import ToolboxHandler
from utils import requestDict, logger
router = APIRouter()
@router.post("/", summary="Запрос остатка инструмента")
async def toolkit_request(
request_data: dict = Depends(requestDict),
):
response = {"status": "error", "data": {}}
logger.info(f"Получение запроса остатка инструмента")
# logger.info(request_data)
toolkitId = request_data.get("body").get("toolkitId")
stocks = await StockHandler.getByToolkitId(toolkitId)
if not stocks:
return response
userId = request_data.get("body").get("userId")
allToolboxes = request_data.get("body").get("allToolboxes")
toolboxes = (
await ToolboxHandler.getByOwner(userId)
if not allToolboxes
else await ToolboxHandler.getAll()
)
if not toolboxes:
return response
toolboxesTitles = {toolbox["id"]: toolbox["title"] for toolbox in toolboxes}
stocksData = {"count": 0, "toolboxes": {}}
for stock in stocks:
toolboxTitle = toolboxesTitles.get(stock["toolbox_id"], None)
if not toolboxTitle:
continue
stocksData["count"] += stock["quantity"]
if toolboxTitle not in stocksData["toolboxes"]:
stocksData["toolboxes"][toolboxTitle] = {
"count": stock["quantity"],
"placement": stock["placement"],
}
else:
stocksData["toolboxes"][toolboxTitle]["count"] += stock["quantity"]
response["status"] = "ok"
response["data"] = stocksData
return response
+114
View File
@@ -342,4 +342,118 @@ tr:hover .action-buttons {
.toolkit-card-img { .toolkit-card-img {
height: 150px; height: 150px;
} }
}
/* Стили для модального окна добавления склада */
.modal-content {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px 12px 0 0;
padding: 1.25rem 1.5rem;
}
.modal-title {
color: #495057;
font-weight: 600;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid rgba(0, 0, 0, 0.08);
padding: 1rem 1.5rem;
}
/* Стили для обязательных полей */
.form-label.required::after {
content: " *";
color: #dc3545;
}
/* Кастомные стили для переключателя */
.form-switch .form-check-input {
width: 3em;
height: 1.5em;
cursor: pointer;
transition: all 0.3s ease;
}
.form-switch .form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.form-switch .form-check-input:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-switch .form-check-label {
font-weight: 500;
cursor: pointer;
}
/* Стили для текстовых полей в фокусе */
.form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
transition: all 0.3s ease;
}
/* Стили для валидации */
.form-control.is-valid {
border-color: #198754;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
/* Стили для текста подсказки */
.form-text {
color: #6c757d;
font-size: 0.875em;
margin-top: 0.25rem;
}
/* Стили для кнопок в модальном окне */
.modal-footer .btn {
min-width: 100px;
font-weight: 500;
}
/* Адаптивность для мобильных */
@media (max-width: 576px) {
.modal-dialog {
margin: 0.5rem;
}
.modal-content {
border-radius: 8px;
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 0.75rem 1rem;
}
} }
+1 -3
View File
@@ -1,8 +1,6 @@
// api.js // api.js
export async function apiRequest(url, payload = {}, method = 'POST') { export async function apiRequest(url, payload = {}, method = 'POST') {
const finalUrl = new URL(url, window.location.origin).toString(); const res = await fetch(url, {
const res = await fetch(finalUrl, {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+477 -14
View File
@@ -1,5 +1,6 @@
import { getCookie } from '/static/js/cookies.js'; import { getCookie } from '/static/js/cookies.js';
import { apiRequest } from '/static/js/api.js'; import { apiRequest } from '/static/js/api.js';
import { showInfo } from '/static/js//toast.js';
let accessData; let accessData;
let userData; let userData;
@@ -112,9 +113,9 @@ function prepareTabs() {
<div class="tab-pane fade p-0" <div class="tab-pane fade p-0"
id="${tabId}" id="${tabId}"
role="tabpanel"> role="tabpanel">
<div class="card border-0 shadow-sm mb-1"> <div class="card border-0 shadow-sm mb-1">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -128,19 +129,19 @@ function prepareTabs() {
</div> </div>
</div> </div>
<div id="${tabId}-tab-optional-content" class="px-4"></div> <div id="${tabId}-tab-optional-content" class="px-4 mt-3" style="max-width: 65%;"></div>
</div> </div>
<div id="${tabId}-tab-content" class="px-4">
<div class="text-center py-5"> <div id="${tabId}-tab-content" class="px-4">
<div class="spinner-border text-primary mb-3" role="status"> <div class="text-center py-5">
<span class="visually-hidden">Загрузка...</span> <div class="spinner-border text-primary mb-3" role="status">
</div> <span class="visually-hidden">Загрузка...</span>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
`).join('')} `).join('')}
</div> </div>
@@ -462,6 +463,249 @@ function setupFilters(tabId, tools, categoriesMap) {
} }
} }
function addToolbox() {
// Проверяем, существует ли уже модальное окно
let modal = document.getElementById('addToolboxModal');
if (modal) {
modal.remove();
}
// Создаем модальное окно
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'addToolboxModal';
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Добавить новый склад</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form id="addToolboxForm" novalidate>
<div class="modal-body">
<div class="mb-3">
<label for="toolboxTitle" class="form-label required">Название склада</label>
<input type="text" class="form-control" id="toolboxTitle"
required minlength="3" maxlength="100">
<div class="invalid-feedback">
Пожалуйста, введите название склада (не менее 3 символов)
</div>
</div>
<div class="mb-3">
<label for="toolboxDescription" class="form-label required">Описание склада</label>
<textarea class="form-control" id="toolboxDescription"
rows="2" minlength="3" maxlength="500"></textarea>
<div class="invalid-feedback">
Пожалуйста, введите описание (не менее 3 символов)
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch d-flex flex-column flex-md-row align-items-md-center justify-content-left"">
<input class="form-check-input me-2" type="checkbox"
role="switch" id="toolboxMonitoring">
<label class="form-check-label" for="toolboxMonitoring">
Отслеживание остатков
</label>
</div>
<div class="form-text">
Включите для отслеживания минимальных остатков инструментов
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="submit" class="btn btn-primary" id="submitToolboxBtn">
<span class="spinner-border spinner-border-sm me-1"
id="submitToolboxSpinner" style="display: none;"></span>
Добавить склад
</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
// Инициализация модального окна
const bsModal = new bootstrap.Modal(modal);
// Получаем элементы формы
const form = modal.querySelector('#addToolboxForm');
const titleInput = modal.querySelector('#toolboxTitle');
const descriptionInput = modal.querySelector('#toolboxDescription');
const submitBtn = modal.querySelector('#submitToolboxBtn');
const spinner = modal.querySelector('#submitToolboxSpinner');
// Функция валидации формы
function validateForm() {
let isValid = true;
// Валидация названия
if (titleInput.value.length < 3) {
titleInput.classList.add('is-invalid');
isValid = false;
} else {
titleInput.classList.remove('is-invalid');
titleInput.classList.add('is-valid');
}
// Валидация описания (необязательно, но если заполнено - проверяем)
if (descriptionInput.value.length > 0 && descriptionInput.value.length < 3) {
descriptionInput.classList.add('is-invalid');
isValid = false;
} else if (descriptionInput.value.length >= 3) {
descriptionInput.classList.remove('is-invalid');
descriptionInput.classList.add('is-valid');
} else {
descriptionInput.classList.remove('is-invalid', 'is-valid');
}
submitBtn.disabled = !isValid;
return isValid;
}
// Слушатели событий для валидации в реальном времени
titleInput.addEventListener('input', function () {
if (this.value.length >= 3) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
if (this.value.length > 0) {
this.classList.add('is-invalid');
} else {
this.classList.remove('is-invalid');
}
}
validateForm();
});
descriptionInput.addEventListener('input', function () {
if (this.value.length === 0) {
this.classList.remove('is-invalid', 'is-valid');
} else if (this.value.length >= 3) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.add('is-invalid');
this.classList.remove('is-valid');
}
validateForm();
});
// Обработчик отправки формы
form.addEventListener('submit', async function (e) {
e.preventDefault();
if (!validateForm()) {
return;
}
// Показываем индикатор загрузки и отключаем кнопку
submitBtn.disabled = true;
spinner.style.display = 'inline-block';
// Собираем данные
const toolboxData = {
title: titleInput.value.trim(),
description: descriptionInput.value.trim(),
monitoring: modal.querySelector('#toolboxMonitoring').checked
};
const userId = userData.id;
try {
// Отправка данных (замените на ваш реальный endpoint)
const response = await apiRequest("/toolbox/", { toolboxData, userId });
if (response.status !== 'ok') {
throw new Error('Ошибка при добавлении склада');
}
// Успешная отправка
bsModal.hide();
// Показываем уведомление об успехе
showInfo('Склад успешно добавлен', 'success');
// Здесь можно добавить обновление списка складов
await uploadTab('toolbox');
} catch (error) {
console.error('Ошибка при добавлении склада:', error);
// Возвращаем кнопку в исходное состояние
submitBtn.disabled = false;
spinner.style.display = 'none';
// Показываем сообщение об ошибке
showInfo('Ошибка при добавлении склада. Попробуйте еще раз.', 'error');
// Можно добавить более детальное сообщение об ошибке
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger mt-3';
errorDiv.innerHTML = `
<strong>Ошибка!</strong> Не удалось добавить склад.
Проверьте соединение и попробуйте еще раз.
`;
const modalBody = modal.querySelector('.modal-body');
if (!modalBody.querySelector('.alert')) {
modalBody.appendChild(errorDiv);
// Убираем сообщение через 5 секунд
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.remove();
}
}, 5000);
}
}
});
// Очистка при закрытии модального окна
modal.addEventListener('hidden.bs.modal', () => {
// Сбрасываем форму
form.reset();
// Убираем стили валидации
titleInput.classList.remove('is-valid', 'is-invalid');
descriptionInput.classList.remove('is-valid', 'is-invalid');
// Включаем кнопку
submitBtn.disabled = false;
spinner.style.display = 'none';
// Убираем сообщения об ошибках
const alerts = modal.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
// Удаляем модальное окно из DOM
setTimeout(() => {
if (modal.parentNode) {
modal.remove();
}
}, 300);
});
// Показываем модальное окно
bsModal.show();
// Фокусируемся на первом поле
setTimeout(() => {
titleInput.focus();
}, 500);
}
function renderToolboxTab(tabData) { function renderToolboxTab(tabData) {
currentToolboxData = tabData; currentToolboxData = tabData;
const tabContent = document.getElementById(`toolbox-tab-content`); const tabContent = document.getElementById(`toolbox-tab-content`);
@@ -479,10 +723,11 @@ function renderToolboxTab(tabData) {
} }
// Создаем навигацию по складам // Создаем навигацию по складам
const toolboxNav = ` const toolboxNav = `
<div class="d-flex flex-wrap gap-2" id="toolboxNav"> <div class="d-flex flex-wrap gap-2" id="toolboxNav">
${tabData.map((toolbox, index) => ` ${tabData.map((toolbox, index) => `
<button class="btn btn-outline-primary toolbox-nav-btn d-flex align-items-center" <button class="btn btn-outline-primary toolbox-nav-btn d-flex align-items-center mb-2"
onclick="selectToolbox(${toolbox.id}, ${index})" onclick="selectToolbox(${toolbox.id}, ${index})"
data-toolbox-id="${toolbox.id}"> data-toolbox-id="${toolbox.id}">
<i class="bi bi-box-seam me-2"></i> <i class="bi bi-box-seam me-2"></i>
@@ -511,6 +756,22 @@ function renderToolboxTab(tabData) {
`; `;
tabOptionalContent.innerHTML = toolboxNav; 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.innerHTML = `
<i class="bi bi-plus-square-fill fs-5 me-2"></i>
<span>Добавить</span>
`;
addToolboxBtn.addEventListener('click', function (e) {
e.preventDefault();
addToolbox();
});
document.getElementById('toolboxNav').appendChild(addToolboxBtn);
}
tabContent.innerHTML = toolboxContent; tabContent.innerHTML = toolboxContent;
} }
@@ -547,12 +808,37 @@ async function loadToolboxContent(toolboxId) {
try { try {
const resp = await apiRequest(`/stocks/`, { toolboxId }); const resp = await apiRequest(`/stocks/`, { toolboxId });
if (resp.status === 'ok') { if (resp.status === 'ok') {
const toolboxData = resp.data; const toolboxData = resp.data;
const toolboxInfo = currentToolboxData.find(t => t.id === toolboxId);
if (toolboxData.length === 0) {
contentContainer.innerHTML = `
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<i class="bi bi-box display-1 text-muted mb-3"></i>
<h4 class="text-muted mb-2">Склад пуст</h4>
<p class="text-muted">В этом складе нет инструментов</p>
<div class="mt-3 d-flex justify-content-end">
<button class="btn btn-danger" id="deleteToolbox">
<i class="bi bi-trash me-2"></i>
Удалить склад
</button>
</div>
</div>
`;
if (!toolboxInfo.owner_id && accessData.manage_toolboxes) {
document.getElementById('deleteToolbox').addEventListener('click', function (e) {
e.preventDefault();
deleteToolbox(toolboxId);
});
} else {
document.getElementById('deleteToolbox').remove();
}
return;
}
// Находим информацию о выбранном складе // Находим информацию о выбранном складе
const toolboxInfo = currentToolboxData.find(t => t.id === toolboxId);
const toolboxOwn = toolboxInfo.owner_id === userData.id ? 'Мой склад' : toolboxInfo.owner_id ? 'Склад сотрудника' : 'Общий склад'; const toolboxOwn = toolboxInfo.owner_id === userData.id ? 'Мой склад' : toolboxInfo.owner_id ? 'Склад сотрудника' : 'Общий склад';
const quantityMonitoring = toolboxInfo.monitoring && accessData.view_all_toolboxes; const quantityMonitoring = toolboxInfo.monitoring && accessData.view_all_toolboxes;
@@ -649,6 +935,182 @@ async function loadToolboxContent(toolboxId) {
} }
} }
async function deleteToolbox(toolboxId) {
// Находим информацию о складе
const toolboxInfo = currentToolboxData.find(t => t.id === toolboxId);
if (!toolboxInfo) {
showInfo('Склад не найден', 'error');
return;
}
// Проверяем, существует ли уже модальное окно
let modal = document.getElementById('deleteToolboxModal');
if (modal) {
modal.remove();
}
// Создаем модальное окно подтверждения
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'deleteToolboxModal';
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
<div class="modal-content">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title text-danger mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Удаление склада
</h5>
<button type="button" class="btn-close mb-2" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body pt-0">
<div class="alert alert-warning mt-3" role="alert">
<div class="d-flex">
<i class="bi bi-exclamation-triangle-fill fs-4 me-2"></i>
<div>
<h6 class="alert-heading mb-2">Внимание! Это действие необратимо</h6>
<p class="mb-0">Вы собираетесь удалить склад. Это действие нельзя отменить.</p>
</div>
</div>
</div>
<div class="mb-4">
<p class="mb-2">Вы уверены, что хотите удалить следующий склад?</p>
<div class="card border">
<div class="card-body">
<h6 class="card-title mb-2">${toolboxInfo.title}</h6>
${toolboxInfo.description ? `<p class="text-muted small mb-1">${toolboxInfo.description}</p>` : ''}
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-calendar me-1"></i>
${toolboxInfo.created_at ? `Создан: ${toolboxInfo.created_at}` : ''}
</small>
<small class="text-muted">
<i class="bi bi-clock me-1"></i>
${toolboxInfo.updated_at ? `Обновлен: ${toolboxInfo.updated_at}` : ''}
</small>
</div>
</div>
</div>
</div>
<div class="alert alert-info" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle me-2 mt-1"></i>
<div>
<p class="mb-1">
<strong>Важно:</strong> Если по складу были операции движения инструментов, удаление будет невозможно.
</p>
<p class="mb-0 small">
В случае проблем с удалением обратитесь к администратору системы.
</p>
</div>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="confirmDeleteCheckbox">
<label class="form-check-label" for="confirmDeleteCheckbox">
Я понимаю последствия и хочу удалить этот склад
</label>
</div>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn" disabled>
<span class="spinner-border spinner-border-sm me-1"
id="deleteSpinner" style="display: none;"></span>
Удалить склад
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Инициализация модального окна
const bsModal = new bootstrap.Modal(modal);
// Получаем элементы
const confirmCheckbox = modal.querySelector('#confirmDeleteCheckbox');
const confirmBtn = modal.querySelector('#confirmDeleteBtn');
const deleteSpinner = modal.querySelector('#deleteSpinner');
// Активация кнопки при подтверждении
confirmCheckbox.addEventListener('change', function () {
confirmBtn.disabled = !this.checked;
});
// Обработчик кнопки удаления
confirmBtn.addEventListener('click', async function () {
if (!confirmCheckbox.checked) return;
// Показываем индикатор загрузки и отключаем кнопку
confirmBtn.disabled = true;
deleteSpinner.style.display = 'inline-block';
try {
// Отправляем запрос на удаление
const userId = userData.id;
const resp = await apiRequest('/toolbox/', { toolboxId, userId }, 'DELETE');
// Проверяем успешность запроса
if (resp.status == 'ok') {
// Успешное удаление
bsModal.hide();
showInfo('Склад успешно удален', 'success');
await uploadTab('toolbox');
} else {
// Обработка ошибок от сервера
let errorMessage = 'Не удалось удалить склад';
if (resp.message) {
errorMessage += ': ' + resp.message;
}
// Показываем конкретное сообщение об ошибке
showInfo(errorMessage, 'error');
// Возвращаем кнопку в исходное состояние
confirmBtn.disabled = false;
deleteSpinner.style.display = 'none';
}
} catch (error) {
console.error('Ошибка при удалении склада:', error);
// Возвращаем кнопку в исходное состояние
confirmBtn.disabled = false;
deleteSpinner.style.display = 'none';
// Показываем общее сообщение об ошибке
showInfo('Произошла ошибка при удалении склада. Попробуйте еще раз.', 'error');
}
});
// Очистка при закрытии модального окна
modal.addEventListener('hidden.bs.modal', () => {
// Удаляем модальное окно из DOM
setTimeout(() => {
if (modal.parentNode) {
modal.remove();
}
}, 300);
});
// Показываем модальное окно
bsModal.show();
}
// Функция обработки данных склада // Функция обработки данных склада
function processToolboxData(toolboxData, toolboxId, quantityMonitoring) { function processToolboxData(toolboxData, toolboxId, quantityMonitoring) {
const { stocks, toolkits, categories } = toolboxData; const { stocks, toolkits, categories } = toolboxData;
@@ -1009,7 +1471,7 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) {
async function getToolkitStocks(toolkitId) { async function getToolkitStocks(toolkitId) {
const userId = userData.id; const userId = userData.id;
const allToolboxes = accessData.view_all_toolboxes; const allToolboxes = accessData.view_all_toolboxes;
const resp = await apiRequest('/stocks/toolkit', { toolkitId, userId, allToolboxes }); const resp = await apiRequest('/toolkit/', { toolkitId, userId, allToolboxes });
return resp.data; return resp.data;
} }
@@ -1272,6 +1734,7 @@ async function showOperationModal(operation, selectedItem) {
if (success) { if (success) {
bsModal.hide(); bsModal.hide();
showInfo(`Запрос на ${operationTitles[operation]} успешно создан`, 'success');
await loadToolboxContent(selectedItem.toolboxId); await loadToolboxContent(selectedItem.toolboxId);
} else { } else {
showError('Ошибка выполнения операции'); showError('Ошибка выполнения операции');
+68
View File
@@ -0,0 +1,68 @@
// Вспомогательная функция для показа уведомлений с использованием Bootstrap Toasts
export function showInfo(message, type = 'info') {
// Создаем контейнер для тостов если его нет
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.cssText = `
z-index: 99999;
max-width: 350px;
`;
document.body.appendChild(toastContainer);
}
// Создаем уникальный ID для тоста
const toastId = 'toast-' + Date.now();
// Определяем классы в зависимости от типа
const typeConfig = {
'success': { bgClass: 'bg-success', icon: 'bi-check-circle', delay: 5000 },
'error': { bgClass: 'bg-danger', icon: 'bi-exclamation-circle', delay: 10000 },
'info': { bgClass: 'bg-info', icon: 'bi-info-circle', delay: 3000 },
'warning': { bgClass: 'bg-warning', icon: 'bi-exclamation-triangle', delay: 8000 }
};
const config = typeConfig[type] || typeConfig.info;
// Создаем тост
const toast = document.createElement('div');
toast.className = `toast ${config.bgClass} text-white`;
toast.id = toastId;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="toast-header ${config.bgClass} text-white">
<i class="bi ${config.icon} me-2"></i>
<strong class="me-auto">Уведомление</strong>
<small>Только что</small>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
toastContainer.appendChild(toast);
// Инициализируем и показываем тост
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: config.delay
});
bsToast.show();
// Удаляем тост после скрытия
toast.addEventListener('hidden.bs.toast', function () {
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
});
}
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -59,7 +59,7 @@ class UserHandler:
return {} return {}
if userAccessLevel.get("available_own_toolbox"): if userAccessLevel.get("available_own_toolbox"):
newToolboxData = { newToolboxData = {
"title": f"Т{newUser.username}", "title": newUser.username,
"description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность", "description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность",
"owner_id": newUser.id, "owner_id": newUser.id,
} }
@@ -110,7 +110,7 @@ class UserHandler:
if not user.available_own_toolbox: if not user.available_own_toolbox:
if editedUser.available_own_toolbox: if editedUser.available_own_toolbox:
newToolboxData = { newToolboxData = {
"title": f"Тулбокс {editedUser.username}", "title": editedUser.username,
"description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность", "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
"owner_id": editedUser.id, "owner_id": editedUser.id,
} }
+2 -2
View File
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer from db.initialize import DatabaseInitializer
try: try:
force = True force = False
reNewDB = True reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB) await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
except Exception as e: except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True) logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)