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

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
+477 -14
View File
@@ -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('Ошибка выполнения операции');