Добавление общих складов и удаление пустых ни разу не использованных общих складов
This commit is contained in:
Binary file not shown.
@@ -4,12 +4,16 @@ from db.handlers.categories import CategoryHandler
|
||||
from utils import render, requestDict, logger
|
||||
from .user import router as user
|
||||
from .stocks import router as stocks
|
||||
from .toolbox import router as toolbox
|
||||
from .toolkit import router as toolkit
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
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.get("/")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-41
@@ -11,16 +11,17 @@ from utils import requestDict, logger
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
@router.post("/", summary="Получение инструментов для тулбокса")
|
||||
async def post_requests(
|
||||
request_data: dict = Depends(requestDict),
|
||||
):
|
||||
toolboxId = request_data.get("body").get("toolboxId")
|
||||
logger.info(f"Получение инструментов для тулбокса {toolboxId}")
|
||||
response = {"status": "error", "data": {}}
|
||||
response = {"status": "error", "data": []}
|
||||
|
||||
stocksData = await StockHandler.getByToolboxId(toolboxId)
|
||||
if not stocksData:
|
||||
response["status"] = "ok"
|
||||
return response
|
||||
toolkitsIds = set(stock["toolkit_id"] for stock in stocksData)
|
||||
toolkitsData = await ToolkitHandler.getSeveral(list(toolkitsIds))
|
||||
@@ -107,42 +108,3 @@ async def post_requests(
|
||||
if result:
|
||||
resonse["status"] = "ok"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -342,4 +342,118 @@ tr:hover .action-buttons {
|
||||
.toolkit-card-img {
|
||||
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,8 +1,6 @@
|
||||
// api.js
|
||||
export async function apiRequest(url, payload = {}, method = 'POST') {
|
||||
const finalUrl = new URL(url, window.location.origin).toString();
|
||||
|
||||
const res = await fetch(finalUrl, {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+477
-14
@@ -1,5 +1,6 @@
|
||||
import { getCookie } from '/static/js/cookies.js';
|
||||
import { apiRequest } from '/static/js/api.js';
|
||||
import { showInfo } from '/static/js//toast.js';
|
||||
|
||||
let accessData;
|
||||
let userData;
|
||||
@@ -112,9 +113,9 @@ function prepareTabs() {
|
||||
<div class="tab-pane fade p-0"
|
||||
id="${tabId}"
|
||||
role="tabpanel">
|
||||
|
||||
<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="d-flex align-items-center">
|
||||
@@ -128,19 +129,19 @@ function prepareTabs() {
|
||||
</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 id="${tabId}-tab-content" class="px-4">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
|
||||
<div id="${tabId}-tab-content" class="px-4">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</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) {
|
||||
currentToolboxData = tabData;
|
||||
const tabContent = document.getElementById(`toolbox-tab-content`);
|
||||
@@ -479,10 +723,11 @@ function renderToolboxTab(tabData) {
|
||||
}
|
||||
|
||||
// Создаем навигацию по складам
|
||||
|
||||
const toolboxNav = `
|
||||
<div class="d-flex flex-wrap gap-2" id="toolboxNav">
|
||||
${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})"
|
||||
data-toolbox-id="${toolbox.id}">
|
||||
<i class="bi bi-box-seam me-2"></i>
|
||||
@@ -511,6 +756,22 @@ 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.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;
|
||||
}
|
||||
|
||||
@@ -547,12 +808,37 @@ async function loadToolboxContent(toolboxId) {
|
||||
|
||||
try {
|
||||
const resp = await apiRequest(`/stocks/`, { toolboxId });
|
||||
|
||||
if (resp.status === 'ok') {
|
||||
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 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) {
|
||||
const { stocks, toolkits, categories } = toolboxData;
|
||||
@@ -1009,7 +1471,7 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) {
|
||||
async function getToolkitStocks(toolkitId) {
|
||||
const userId = userData.id;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1272,6 +1734,7 @@ async function showOperationModal(operation, selectedItem) {
|
||||
|
||||
if (success) {
|
||||
bsModal.hide();
|
||||
showInfo(`Запрос на ${operationTitles[operation]} успешно создан`, 'success');
|
||||
await loadToolboxContent(selectedItem.toolboxId);
|
||||
} else {
|
||||
showError('Ошибка выполнения операции');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user