`;
} else {
- imagesDiv = `
+ imagesDiv = images[0] ? `
';
}
- const description = item.description || item.toolkitData?.description || 'Нет описания';
- const specifications = item.toolkitData?.specifications || item.specifications || {};
- const external_link = item.external_link || item.toolkitData?.external_link || null;
- const category_desc = item.categoryData?.description || item.category_desc || '';
- const toolkitStocks = await getToolkitStocks(item.id);
+ // Переменная для хранения данных об остатках (будет загружена при раскрытии аккордеона)
+ let toolkitStocksData = null;
+ let isStocksLoading = false;
modal.innerHTML = `
@@ -3341,48 +3445,64 @@ async function showToolkitDetailsModal(item) {
${imagesDiv}
Описание:
-
${description}
+
${toolkitData.description}
Категория:
-
${item.category} - ${category_desc}
+
${categoryData.title} - ${categoryData.description}
- ${specifications ? `
+ ${Object.keys(toolkitData.specifications).length > 0 ? `
Характеристики:
-
- ${Object.entries(specifications).map(([key, value]) => `
-
- | ${key}: |
- ${value} |
-
- `).join('')}
-
+
+
+
+ ${Object.entries(toolkitData.specifications).map(([key, value]) => `
+
+ | ${key}: |
+ ${value} |
+
+ `).join('')}
+
+
+
` : ''}
- ${toolkitStocks ? `
-
Остатки на складах: ${toolkitStocks.count} шт.
-
- ${Object.entries(toolkitStocks.toolboxes).map(([key, value]) => `
-
- | ${key}: |
- ${value.count} шт.${value.id && accessData.available_own_toolbox ? `
-
- ` : ''}
- |
- ${value.placement || ''} |
-
- `).join('')}
-
- ` : ''}
+
+
+
+
+
+
+
+
+ Загрузка...
+
+
Загрузка данных об остатках...
+
+
+
+
+
+
+ Не удалось загрузить данные об остатках
+
+
+
+
+
- ${external_link ? `
-
- Внешняя ссылка
-
+ ${toolkitData.external_link ? `
+
` : ''}
@@ -3394,40 +3514,1198 @@ async function showToolkitDetailsModal(item) {
`;
- // Добавляем обработчики для кнопок в строке
- modal.querySelectorAll('button[data-action]').forEach(button => {
- button.addEventListener('click', async (e) => {
- e.stopPropagation();
- const action = e.currentTarget.dataset.action;
- const id = e.currentTarget.dataset.id;
- const toolboxId = e.currentTarget.dataset.toolbox_id;
- const available = e.currentTarget.dataset.available;
- const totalQuantity = available;
- const title = item.title;
- const totalCost = e.currentTarget.dataset.totalcost;
- const skipRefresh = true;
- const selectedItem = { id, toolboxId, available, totalQuantity, title, totalCost, skipRefresh };
- await showOperationModal(action, selectedItem);
- modal.querySelector('button[data-bs-dismiss="modal"]').click();
- });
- });
+ if (!toolkitData.hidden) {
+ if (accessData.tools_edit) {
+ const footer = modal.querySelector('.modal-footer');
+ const editButton = document.createElement('button');
+ editButton.className = 'btn btn-outline-primary';
+ editButton.textContent = 'Редактировать';
+ footer.prepend(editButton);
+
+ editButton.addEventListener('click', async () => {
+ try {
+ modal.querySelector('.btn-close').click();
+ await manageToolkit(toolkitData, categories, 'update');
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ }
+
+ if (accessData.tools_creation) {
+ const footer = modal.querySelector('.modal-footer');
+ const dubleButton = document.createElement('button');
+ dubleButton.className = 'btn btn-outline-success';
+ dubleButton.textContent = 'Скопировать';
+ footer.prepend(dubleButton);
+
+ dubleButton.addEventListener('click', async () => {
+ try {
+ modal.querySelector('.btn-close').click();
+ await manageToolkit(toolkitData, categories, 'copy');
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ }
+
+ if (accessData.tools_delete) {
+ const footer = modal.querySelector('.modal-footer');
+ const deleteButton = document.createElement('button');
+ deleteButton.className = 'btn btn-outline-danger';
+ deleteButton.textContent = 'Удалить';
+ footer.prepend(deleteButton);
+
+ deleteButton.addEventListener('click', async () => {
+ try {
+ modal.querySelector('.btn-close').click();
+ await deleteToolkit(toolkitData);
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ }
+ } else {
+ if (accessData.tools_delete) {
+ const footer = modal.querySelector('.modal-footer');
+ const showButton = document.createElement('button');
+ showButton.className = 'btn btn-outline-danger';
+ showButton.textContent = 'Показывать';
+ footer.prepend(showButton);
+
+ showButton.addEventListener('click', async () => {
+ try {
+ // Подготавливаем данные для отправки
+ const formData = { toolkitId: toolkitData.id, userId: userData.id, hidden: false };
+
+ setTimeout(() => {
+ modal.querySelector('.btn-close').click();
+ }, 300);
+
+ // Отправляем запрос на отображение
+ const response = await apiRequest('/toolkit/hide', formData, 'POST');
+
+ // Показываем результат
+ if (response.status === 'ok') {
+ // Успешное отображение
+
+ // Обновляем список инструментов
+ if (typeof uploadTab === 'function') {
+ await uploadTab('toolkits');
+ }
+ showInfo(response.message || 'Инструмент успешно отображен', 'success');
+
+ } else {
+ // Ошибка при отображении
+ showInfo(response.message || 'Произошла ошибка при отображении инструмента', 'danger');
+ }
+ } catch (error) {
+ console.error('Ошибка при скрытии инструмента:', error);
+ }
+ });
+ }
+ }
+
+ // Добавляем модальное окно в DOM
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
+ // Функция для загрузки данных об остатках
+ const loadToolkitStocks = async () => {
+ if (isStocksLoading) return;
+
+ const stocksLoading = modal.querySelector('#stocksLoading');
+ const stocksContent = modal.querySelector('#stocksContent');
+ const stocksError = modal.querySelector('#stocksError');
+
+ try {
+ isStocksLoading = true;
+
+ // Показываем спиннер, скрываем контент и ошибку
+ stocksLoading.classList.remove('d-none');
+ stocksContent.classList.add('d-none');
+ stocksError.classList.add('d-none');
+
+ // Загружаем данные
+ toolkitStocksData = await getToolkitStocks(toolkitData.id);
+
+ if (toolkitStocksData) {
+ // Формируем HTML для остатков
+ let stocksHtml = '';
+
+ if (toolkitStocksData.count > 0) {
+ stocksHtml = `
+
Общее количество: ${toolkitStocksData.count} шт.
+
+
+
+
+ | Склад |
+ Количество |
+ Расположение |
+
+
+
+ ${Object.entries(toolkitStocksData.toolboxes || {}).map(([key, value]) => `
+
+ | ${key} |
+ ${value.count} шт. |
+
+ ${value.placement || ''}
+ ${!toolkitData.hidden && value.id && accessData.available_own_toolbox ? `
+
+ ` : ''}
+ |
+
+ `).join('')}
+
+
+
+ `;
+ } else {
+ stocksHtml = `
+
+
+ На складах отсутствуют остатки этого инструмента
+
+ `;
+ }
+
+ // Вставляем HTML и показываем контент
+ stocksContent.innerHTML = stocksHtml;
+ stocksContent.classList.remove('d-none');
+
+ // Добавляем обработчики для кнопок получения
+ stocksContent.querySelectorAll('.get-stock-btn').forEach(button => {
+ button.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const action = 'get';
+ const id = e.currentTarget.dataset.id;
+ const toolboxId = e.currentTarget.dataset.toolboxId;
+ const available = e.currentTarget.dataset.available;
+ const totalQuantity = available;
+ const title = toolkitData.title;
+ const totalCost = e.currentTarget.dataset.totalcost;
+ const skipRefresh = true;
+ const selectedItem = {
+ id,
+ toolboxId,
+ available,
+ totalQuantity,
+ title,
+ totalCost,
+ skipRefresh
+ };
+ await showOperationModal(action, selectedItem);
+ modal.querySelector('button[data-bs-dismiss="modal"]').click();
+ });
+ });
+ } else {
+ throw new Error('Нет данных об остатках');
+ }
+ } catch (error) {
+ console.error('Ошибка при загрузке остатков:', error);
+ stocksError.classList.remove('d-none');
+ } finally {
+ stocksLoading.classList.add('d-none');
+ isStocksLoading = false;
+ }
+ };
+
+ // Обработчик события раскрытия аккордеона
+ const stocksCollapse = modal.querySelector('#stocksCollapse');
+ stocksCollapse.addEventListener('show.bs.collapse', async () => {
+ // Загружаем данные только если они еще не загружены
+ if (!toolkitStocksData && !isStocksLoading) {
+ await loadToolkitStocks();
+ }
+ });
+
+ // Обработчик для принудительной перезагрузки данных (например, при повторном открытии аккордеона)
+ const stocksHeading = modal.querySelector('#stocksHeading');
+ stocksHeading.addEventListener('click', async (e) => {
+ // Если данные уже загружены, можно обновить их при повторном клике
+ const isExpanded = stocksCollapse.classList.contains('show');
+ if (isExpanded && toolkitStocksData) {
+ // Можно добавить кнопку обновления или обновлять автоматически
+ // Для простоты пока оставляем как есть
+ }
+ });
+
+ // Очистка при закрытии модалки
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
- // После создания модального окна добавьте инициализацию lightbox
- lightbox.option({
- 'resizeDuration': 200,
- 'wrapAround': true,
- 'albumLabel': "Изображение %1 из %2",
- 'fadeDuration': 300,
- 'imageFadeDuration': 300
+ // Инициализация lightbox
+ setTimeout(() => {
+ lightbox.option({
+ 'resizeDuration': 200,
+ 'wrapAround': true,
+ 'albumLabel': "Изображение %1 из %2",
+ 'fadeDuration': 300,
+ 'imageFadeDuration': 300
+ });
+ }, 100);
+}
+
+// Функция создания/редактирования инструмента
+async function manageToolkit(toolkitData = null, categories = null, action = 'create') {
+
+ if (!toolkitData && action !== 'create') {
+ showInfo('Произошла ошибка', 'error');
+ return;
+ }
+
+ if (!categories) {
+ try {
+ const categoriesResponse = await apiRequest('/toolkit/categories', {}, 'GET');
+ if (categoriesResponse.status === 'ok') {
+ categories = categoriesResponse.data;
+ } else {
+ showInfo('Произошла ошибка', 'error');
+ return;
+ }
+ } catch (error) {
+ console.error('Ошибка загрузки категорий:', error);
+ }
+ }
+
+ if (action === 'copy') {
+ toolkitData.title += ' (копия)';
+ }
+
+ // Удаляем старое модальное окно, если оно существует
+ let modal = document.getElementById('manageToolkitModal');
+ if (modal) modal.remove();
+
+ // Проверяем режим (создание или редактирование)
+ const isEditMode = !!toolkitData;
+ const modalTitle = isEditMode ? 'Редактирование инструмента' : 'Создание нового инструмента';
+ const submitButtonText = isEditMode ? 'Сохранить изменения' : 'Создать инструмент';
+
+ // Данные инструмента по умолчанию
+ const defaultToolkitData = {
+ title: '',
+ category_id: '',
+ description: '',
+ external_link: '',
+ quantity_min: null,
+ quantity_min_extra: null,
+ specifications: {},
+ image: {
+ main: '',
+ additional: []
+ }
+ };
+
+ // Объединяем данные
+ const data = isEditMode ? {
+ ...defaultToolkitData,
+ ...toolkitData,
+ // Обеспечиваем правильную структуру изображений
+ image: toolkitData.image ? {
+ main: typeof toolkitData.image === 'string' ? toolkitData.image : toolkitData.image.main,
+ additional: toolkitData.images || toolkitData.image.additional || []
+ } : defaultToolkitData.image
+ } : defaultToolkitData;
+
+ // Состояние изображений - теперь храним объекты с метаданными
+ let mainImageFile = null;
+ let mainImagePreview = data.image.main;
+
+ // Для дополнительных изображений храним объекты с информацией о типе
+ let additionalImages = [];
+
+ // Инициализируем существующие изображения
+ if (data.image.additional && Array.isArray(data.image.additional)) {
+ additionalImages = data.image.additional.map(url => ({
+ preview: url,
+ originalUrl: url,
+ isNew: false,
+ isFile: false,
+ file: null
+ }));
+ }
+
+ // Состояние характеристик
+ let specifications = { ...data.specifications };
+
+ // Создаём модальное окно
+ modal = document.createElement('div');
+ modal.className = 'modal fade';
+ modal.id = 'manageToolkitModal';
+ modal.tabIndex = -1;
+ modal.setAttribute('aria-hidden', 'true');
+
+ modal.innerHTML = `
+
+ `;
+
+ document.body.appendChild(modal);
+ const bsModal = new bootstrap.Modal(modal);
+
+ // Получаем элементы DOM
+ const mainImageDropZone = modal.querySelector('#mainImageDropZone');
+ const mainImageInput = modal.querySelector('#mainImageInput');
+ const mainImageContent = modal.querySelector('#mainImageContent');
+ const removeMainImageBtn = modal.querySelector('#removeMainImageBtn');
+ const additionalImagesContainer = modal.querySelector('#additionalImagesContainer');
+ const addAdditionalImageBtn = modal.querySelector('#addAdditionalImageBtn');
+ const additionalImagesDropZone = modal.querySelector('#additionalImagesDropZone');
+ const additionalImagesInput = modal.querySelector('#additionalImagesInput');
+ const specificationsList = modal.querySelector('#specificationsList');
+ const addSpecBtn = modal.querySelector('#addSpecBtn');
+ const submitBtn = modal.querySelector('#submitToolkitBtn');
+ const spinner = modal.querySelector('#submitToolkitSpinner');
+ const submitText = modal.querySelector('#submitToolkitText');
+ const errorDiv = modal.querySelector('#manageToolkitError');
+ const errorMessage = modal.querySelector('#manageToolkitErrorMessage');
+
+ // Функция для отображения ошибок
+ function showError(message) {
+ errorMessage.textContent = message;
+ errorDiv.classList.remove('d-none');
+ errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+
+ // Функция для скрытия ошибок
+ function hideError() {
+ errorDiv.classList.add('d-none');
+ }
+
+ // Функция для преобразования файла в base64
+ function fileToBase64(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = error => reject(error);
+ });
+ }
+
+ // Функция для обновления превью основного изображения
+ function updateMainImagePreview() {
+ if (mainImagePreview) {
+ mainImageContent.innerHTML = `
+
+

+
+
+
+ ${mainImageFile ? 'Готово к загрузке' : 'Основное изображение загружено'}
+
+ `;
+
+ // Добавляем обработчик для кнопки удаления
+ const newRemoveBtn = mainImageContent.querySelector('#removeMainImageBtn');
+ newRemoveBtn.addEventListener('click', removeMainImage);
+ } else {
+ mainImageContent.innerHTML = `
+
+
+
Перетащите изображение сюда
+
или кликните для выбора файла
+
JPG, PNG до 5MB
+
+ `;
+ }
+ }
+
+ // Функция для удаления основного изображения
+ function removeMainImage() {
+ mainImageFile = null;
+ mainImagePreview = '';
+ mainImagePreview = data.image.main; // Возвращаем оригинальное изображение если было
+ updateMainImagePreview();
+ }
+
+ // Обработчики для основного изображения
+ mainImageDropZone.addEventListener('click', () => mainImageInput.click());
+ mainImageDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ mainImageDropZone.style.backgroundColor = '#f8f9fa';
+ });
+ mainImageDropZone.addEventListener('dragleave', () => {
+ mainImageDropZone.style.backgroundColor = '';
+ });
+ mainImageDropZone.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ mainImageDropZone.style.backgroundColor = '';
+
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ const file = files[0];
+ if (file.type.startsWith('image/')) {
+ mainImageFile = file;
+ mainImagePreview = URL.createObjectURL(file);
+ updateMainImagePreview();
+ } else {
+ showError('Пожалуйста, выберите файл изображения');
+ }
+ }
+ });
+
+ mainImageInput.addEventListener('change', async (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ if (file.size > 5 * 1024 * 1024) {
+ showError('Размер файла не должен превышать 5MB');
+ return;
+ }
+ mainImageFile = file;
+ mainImagePreview = URL.createObjectURL(file);
+ updateMainImagePreview();
+ }
+ e.target.value = '';
+ });
+
+ // Функция для обновления превью дополнительных изображений
+ function updateAdditionalImages() {
+ additionalImagesContainer.innerHTML = '';
+
+ if (additionalImages.length > 0) {
+ additionalImagesDropZone.classList.add('d-none');
+
+ additionalImages.forEach((image, index) => {
+ const isNewFile = image.isFile && image.isNew;
+ const imgElement = document.createElement('div');
+ imgElement.className = 'mb-3';
+ imgElement.innerHTML = `
+
+
+
+

+
+
+
+ ${isNewFile ? 'Готово к загрузке' : 'Изображение загружено'}
+ ${isNewFile && image.file ? `
${image.file.name}
` : ''}
+
+
+
+
+
+
+
+ `;
+ additionalImagesContainer.appendChild(imgElement);
+ });
+ } else {
+ additionalImagesDropZone.classList.remove('d-none');
+ }
+ }
+
+ // Функция для удаления дополнительного изображения
+ window.removeAdditionalImage = function (index) {
+ // Освобождаем blob URL если это новый файл
+ const image = additionalImages[index];
+ if (image.isFile && image.isNew && image.preview.startsWith('blob:')) {
+ URL.revokeObjectURL(image.preview);
+ }
+
+ additionalImages.splice(index, 1);
+ updateAdditionalImages();
+ };
+
+ // Обработчики для дополнительных изображений
+ addAdditionalImageBtn.addEventListener('click', () => additionalImagesInput.click());
+
+ additionalImagesDropZone.addEventListener('click', () => additionalImagesInput.click());
+ additionalImagesDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ additionalImagesDropZone.style.backgroundColor = '#f8f9fa';
+ });
+ additionalImagesDropZone.addEventListener('dragleave', () => {
+ additionalImagesDropZone.style.backgroundColor = '';
+ });
+ additionalImagesDropZone.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ additionalImagesDropZone.style.backgroundColor = '';
+
+ const files = Array.from(e.dataTransfer.files);
+ const imageFiles = files.filter(file => file.type.startsWith('image/'));
+
+ if (imageFiles.length > 0) {
+ for (const file of imageFiles) {
+ if (file.size > 5 * 1024 * 1024) {
+ showError(`Файл ${file.name} превышает 5MB`);
+ continue;
+ }
+ additionalImages.push({
+ preview: URL.createObjectURL(file),
+ originalUrl: null,
+ isNew: true,
+ isFile: true,
+ file: file
+ });
+ }
+ updateAdditionalImages();
+ } else {
+ showError('Пожалуйста, выберите файлы изображений');
+ }
+ });
+
+ additionalImagesInput.addEventListener('change', async (e) => {
+ const files = Array.from(e.target.files);
+ const imageFiles = files.filter(file => file.type.startsWith('image/'));
+
+ if (imageFiles.length > 0) {
+ for (const file of imageFiles) {
+ if (file.size > 5 * 1024 * 1024) {
+ showError(`Файл ${file.name} превышает 5MB`);
+ continue;
+ }
+ additionalImages.push({
+ preview: URL.createObjectURL(file),
+ originalUrl: null,
+ isNew: true,
+ isFile: true,
+ file: file
+ });
+ }
+ updateAdditionalImages();
+ }
+ e.target.value = '';
+ });
+
+ // Функция для обновления списка характеристик
+ function updateSpecificationsList() {
+ specificationsList.innerHTML = '';
+
+ if (Object.keys(specifications).length === 0) {
+ specificationsList.innerHTML = `
+
+
+ Характеристики не добавлены
+
+ `;
+ return;
+ }
+
+ const table = document.createElement('table');
+ table.className = 'table table-sm';
+ table.innerHTML = `
+
+
+ | Характеристика |
+ Значение |
+ Действия |
+
+
+
+ ${Object.entries(specifications).map(([key, value], index) => `
+
+ | ${key} |
+ ${value} |
+
+
+
+
+
+ |
+
+ `).join('')}
+
+ `;
+ specificationsList.appendChild(table);
+ }
+
+ // Функция для удаления характеристики
+ window.removeSpecification = function (key) {
+ delete specifications[key];
+ updateSpecificationsList();
+ showInfo('Характеристика удалена', 'success');
+ };
+
+ // Функция для редактирования характеристики
+ window.editSpecification = function (oldKey, oldValue) {
+ addSpecificationModal(oldKey, oldValue);
+ };
+
+ // Функция для добавления/редактирования характеристики
+ function addSpecificationModal(oldKey = null, oldValue = null) {
+ const isEditMode = oldKey !== null;
+ const modalTitle = isEditMode ? 'Редактирование характеристики' : 'Добавление характеристики';
+ const saveButtonText = isEditMode ? 'Сохранить изменения' : 'Добавить характеристику';
+
+ const specModal = document.createElement('div');
+ specModal.className = 'modal fade';
+ specModal.id = 'addSpecModal';
+ specModal.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(specModal);
+ const bsSpecModal = new bootstrap.Modal(specModal);
+
+ // Функция выбора предопределенной характеристики
+ window.selectPredefinedSpec = function (name) {
+ document.getElementById('specName').value = name;
+ };
+
+ // Обработчик сохранения
+ specModal.querySelector('#saveSpecBtn').addEventListener('click', () => {
+ const name = document.getElementById('specName').value.trim();
+ const value = document.getElementById('specValue').value.trim();
+
+ if (!name) {
+ showError('Введите название характеристики');
+ return;
+ }
+
+ if (!value) {
+ showError('Введите значение характеристики');
+ return;
+ }
+
+ // Если это режим редактирования и название изменилось, удаляем старую характеристику
+ if (isEditMode && oldKey !== name) {
+ delete specifications[oldKey];
+ }
+
+ // Добавляем/обновляем характеристику
+ specifications[name] = value;
+ updateSpecificationsList();
+
+ bsSpecModal.hide();
+ setTimeout(() => {
+ specModal.remove();
+ showInfo(isEditMode ? 'Характеристика обновлена' : 'Характеристика добавлена', 'success');
+ }, 300);
+ });
+
+ // Очистка при закрытии
+ specModal.addEventListener('hidden.bs.modal', () => {
+ setTimeout(() => {
+ if (specModal.parentNode) specModal.remove();
+ }, 300);
+ });
+
+ bsSpecModal.show();
+ setTimeout(() => document.getElementById('specName').focus(), 100);
+ }
+
+ // Обработчик для добавления характеристики
+ addSpecBtn.addEventListener('click', () => addSpecificationModal());
+
+ // Инициализируем списки
+ updateMainImagePreview();
+ updateAdditionalImages();
+ updateSpecificationsList();
+
+ // Обработчик отправки формы
+ modal.querySelector('#manageToolkitForm').addEventListener('submit', async function (e) {
+ e.preventDefault();
+
+ // Собираем данные
+ const formData = {
+ title: document.getElementById('toolkitTitle').value.trim(),
+ category_id: document.getElementById('toolkitCategory').value,
+ description: document.getElementById('toolkitDescription').value.trim(),
+ external_link: document.getElementById('toolkitExternalLink').value.trim(),
+ specifications: specifications,
+ };
+
+ // Добавляем поля количества
+ const quantityMin = document.getElementById('toolkitQuantityMin').value.trim();
+ const quantityMinExtra = document.getElementById('toolkitQuantityMinExtra').value.trim();
+
+ if (quantityMin) {
+ formData.quantity_min = parseInt(quantityMin);
+ }
+
+ if (quantityMinExtra) {
+ formData.quantity_min_extra = parseInt(quantityMinExtra);
+ }
+
+ if (toolkitData?.id) {
+ formData.id = toolkitData.id;
+ }
+
+ // Валидация
+ if (!formData.title) {
+ showError('Введите название инструмента');
+ return;
+ }
+
+ if (!formData.category_id) {
+ showError('Выберите категорию');
+ return;
+ }
+
+ // Проверка значений количества
+ if (quantityMin && isNaN(parseInt(quantityMin))) {
+ showError('Низкое количество должно быть числом');
+ return;
+ }
+
+ if (quantityMinExtra && isNaN(parseInt(quantityMinExtra))) {
+ showError('Критическое количество должно быть числом');
+ return;
+ }
+
+ // Подготавливаем изображения
+ const imageData = {
+ main: mainImageFile ? await fileToBase64(mainImageFile) : data.image.main,
+ additional: []
+ };
+
+ // Обрабатываем дополнительные изображения
+ for (const image of additionalImages) {
+ if (image.isFile && image.isNew && image.file) {
+ // Новый файл - конвертируем в base64
+ imageData.additional.push(await fileToBase64(image.file));
+ } else if (!image.isFile && image.originalUrl) {
+ // Существующее изображение - отправляем оригинальный URL
+ imageData.additional.push(image.originalUrl);
+ }
+ }
+
+ formData.image = imageData;
+
+ // Показываем спиннер
+ submitBtn.disabled = true;
+ spinner.style.display = 'inline-block';
+ hideError();
+
+ try {
+ // Отправляем запрос
+ const userId = userData.id;
+ const response = await apiRequest('/toolkit/manage', { action, formData, userId }, 'POST');
+
+ if (response.status === 'ok') {
+ showInfo(isEditMode ? 'Инструмент обновлен' : 'Инструмент создан', 'success');
+ bsModal.hide();
+
+ // Обновляем список инструментов
+ if (typeof uploadTab === 'function') {
+ await uploadTab('toolkits');
+ }
+ } else {
+ throw new Error(response.message || 'Ошибка сохранения');
+ }
+ } catch (error) {
+ console.error('Ошибка при сохранении инструмента:', error);
+ showError(error.message || 'Произошла ошибка при сохранении инструмента');
+
+ // Возвращаем кнопку в исходное состояние
+ submitBtn.disabled = false;
+ spinner.style.display = 'none';
+ }
+ });
+
+ // Очистка при закрытии модалки
+ modal.addEventListener('hidden.bs.modal', () => {
+ // Освобождаем URL объекты
+ if (mainImagePreview && mainImagePreview.startsWith('blob:')) {
+ URL.revokeObjectURL(mainImagePreview);
+ }
+
+ // Освобождаем blob URL для дополнительных изображений
+ additionalImages.forEach(image => {
+ if (image.isFile && image.isNew && image.preview.startsWith('blob:')) {
+ URL.revokeObjectURL(image.preview);
+ }
+ });
+
+ setTimeout(() => {
+ if (modal.parentNode) modal.remove();
+ }, 300);
+ });
+
+ // Показываем модалку
+ bsModal.show();
+
+ return new Promise((resolve) => {
+ modal.addEventListener('hidden.bs.modal', () => {
+ resolve(null);
+ });
+ });
+}
+
+async function deleteToolkit(toolkitData) {
+ // Создаем модальное окно подтверждения удаления
+ const modalHTML = `
+
+ `;
+
+ // Добавляем модальное окно в DOM
+ const modalContainer = document.createElement('div');
+ modalContainer.innerHTML = modalHTML;
+ document.body.appendChild(modalContainer);
+
+ const modal = new bootstrap.Modal(document.getElementById('deleteToolkitModal'));
+ const confirmBtn = document.getElementById('confirmDeleteBtn');
+ const hideBtn = document.getElementById('confirmHideBtn');
+ const resultMessage = document.getElementById('deleteResultMessage');
+
+ modal.show();
+
+ hideBtn.addEventListener('click', async () => {
+ hideBtn.disabled = true;
+ hideBtn.innerHTML = '
Скрытие...';
+
+ try {
+ // Подготавливаем данные для отправки
+ const formData = { toolkitId: toolkitData.id, userId: userData.id, hidden: true };
+
+ // Отправляем запрос на скрытие
+ const response = await apiRequest('/toolkit/hide', formData, 'POST');
+
+ // Показываем результат
+ if (response.status === 'ok') {
+ // Успешное скрытие
+ resultMessage.className = 'alert alert-success mt-3';
+ resultMessage.innerHTML = `
${response.message || 'Инструмент успешно скрыт'}`;
+ resultMessage.classList.remove('d-none');
+
+ // Показываем общее уведомление
+ showInfo(response.message || 'Инструмент успешно скрыт', 'success');
+
+ setTimeout(() => {
+ modal.hide();
+ }, 1000);
+
+ // Обновляем список инструментов
+ if (typeof uploadTab === 'function') {
+ await uploadTab('toolkits');
+ }
+ } else {
+ // Ошибка при скрытии
+ resultMessage.className = 'alert alert-danger mt-3';
+ resultMessage.innerHTML = `
${response.message || 'Произошла ошибка при скрытии инструмента'}`;
+ resultMessage.classList.remove('d-none');
+ }
+ } catch (error) {
+ console.error('Ошибка при скрытии инструмента:', error);
+ resultMessage.className = 'alert alert-danger mt-3';
+ resultMessage.innerHTML = `
Произошла ошибка при скрытии инструмента`;
+ resultMessage.classList.remove('d-none');
+ hideBtn.disabled = false;
+ hideBtn.innerHTML = '
Скрыть';
+ }
+ });
+
+ // Обработчик подтверждения удаления
+ confirmBtn.addEventListener('click', async () => {
+ // Блокируем кнопку и показываем индикатор загрузки
+ confirmBtn.disabled = true;
+ confirmBtn.innerHTML = '
Удаление...';
+
+ try {
+ // Подготавливаем данные для отправки
+ const formData = { id: toolkitData.id };
+ const userId = userData.id;
+
+ // Отправляем запрос на удаление
+ const response = await apiRequest('/toolkit/manage', {
+ action: 'delete',
+ formData,
+ userId
+ }, 'POST');
+
+ // Показываем результат
+ if (response.status === 'ok') {
+ // Успешное удаление
+ resultMessage.className = 'alert alert-success mt-3';
+ resultMessage.innerHTML = `
${response.message || 'Инструмент успешно удален'}`;
+ resultMessage.classList.remove('d-none');
+
+ if (typeof uploadTab === 'function') {
+ await uploadTab('toolkits');
+ }
+
+ // Показываем общее уведомление
+ showInfo(response.message || 'Инструмент успешно удален', 'success');
+
+ // Закрываем модальное окно через 2 секунды
+ setTimeout(() => {
+ modal.hide();
+ }, 2000);
+ } else {
+ // Ошибка удаления (возможно, есть движения)
+ resultMessage.className = 'alert alert-danger mt-3';
+ resultMessage.innerHTML = `
${response.message || 'Не удалось удалить инструмент'}`;
+ resultMessage.classList.remove('d-none');
+
+ // Разблокируем кнопку и возвращаем исходный текст
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = 'Удалить';
+
+ // Если есть конкретное сообщение о движениях
+ if (response.message && response.message.includes('движени')) {
+ resultMessage.innerHTML += '
Удаление этого инструмента невозможно, так как были операции движения. Его можно только скрыть.';
+ }
+
+ // Показываем общее уведомление
+ showInfo(response.message || 'Не удалось удалить инструмент', 'danger');
+ }
+ } catch (error) {
+ console.error('Ошибка при удалении инструмента:', error);
+
+ resultMessage.className = 'alert alert-danger mt-3';
+ resultMessage.innerHTML = `
Произошла ошибка при удалении: ${error.message}`;
+ resultMessage.classList.remove('d-none');
+
+ // Разблокируем кнопку и возвращаем исходный текст
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = 'Удалить';
+ }
+ });
+
+ // Очистка при закрытии модального окна
+ document.getElementById('deleteToolkitModal').addEventListener('hidden.bs.modal', () => {
+ document.body.removeChild(modalContainer);
});
}
diff --git a/db/handlers/__pycache__/stock.cpython-313.pyc b/db/handlers/__pycache__/stock.cpython-313.pyc
index b89ba55..b8b29a1 100644
Binary files a/db/handlers/__pycache__/stock.cpython-313.pyc and b/db/handlers/__pycache__/stock.cpython-313.pyc differ
diff --git a/db/handlers/__pycache__/toolkit.cpython-313.pyc b/db/handlers/__pycache__/toolkit.cpython-313.pyc
index 15718a0..047be3c 100644
Binary files a/db/handlers/__pycache__/toolkit.cpython-313.pyc and b/db/handlers/__pycache__/toolkit.cpython-313.pyc differ
diff --git a/db/handlers/stock.py b/db/handlers/stock.py
index a0439e1..2bf3cab 100644
--- a/db/handlers/stock.py
+++ b/db/handlers/stock.py
@@ -138,6 +138,13 @@ class StockHandler:
stocks = await CRUD.read(query, True)
return await filterQuantity(stocks, filtered)
+ async def checkToolkitExists(toolkitId: int) -> bool:
+ from db import CRUD
+
+ query = select(Stock).where(Stock.toolkit_id == toolkitId)
+ stocks = await CRUD.read(query)
+ return True if stocks else False
+
async def getByToolboxIdAndToolkitId(
toolboxId: int, toolkitId: int, filtered: bool = True
) -> list[dict]:
diff --git a/db/handlers/toolkit.py b/db/handlers/toolkit.py
index 24a9a71..ab5b8cd 100644
--- a/db/handlers/toolkit.py
+++ b/db/handlers/toolkit.py
@@ -1,15 +1,24 @@
from datetime import datetime
-from utils import logger, saveImage, safeFilename, deleteImage
+from db.handlers.stock import StockHandler
+from utils import logger, saveImage, safeFilename
from db import CRUD
from db.schemas.toolkit import Toolkit
from sqlalchemy import select
from db.handlers.records import ServiceRecordsHandler
+from utils.image import deleteImage
def handleToolkitImage(imageData, title: str):
+ import base64
+
title = safeFilename(title)
- fileName = f"tools/{title}.png"
- if not saveImage(imageData, fileName):
+ fileName = f"static/images/tools/{title}.png"
+ if imageData.startswith("data:image"):
+ header, encoded = imageData.split(",", 1)
+ else:
+ encoded = imageData
+ file_bytes = base64.b64decode(encoded)
+ if not saveImage(file_bytes, fileName):
return None
return fileName
@@ -19,22 +28,20 @@ class ToolkitHandler:
title = toolkitData.get("title", None)
if not title:
logger.error("Не указано название инструмента")
- return {}
+ return {"errorMessage": "Не указано название инструмента"}
query = select(Toolkit).where(Toolkit.title == title)
toolkit = await CRUD.read(query)
if toolkit:
logger.error("Инструмент с таким названием уже существует")
- return {}
+ return {"errorMessage": "Инструмент с таким названием уже существует"}
try:
imageDict = {"main": "static/images/tools/default.png", "additional": []}
if "image" in toolkitData:
imageData = toolkitData.pop("image")
mainImage = imageData.get("main")
- if isinstance(mainImage, str) and mainImage.startswith(
- "static/images/"
- ):
+ if mainImage.startswith("static/images/"):
imageDict["main"] = mainImage
else:
imageFileName = handleToolkitImage(mainImage, title)
@@ -43,9 +50,7 @@ class ToolkitHandler:
additionalImages = imageData.get("additional", [])
if len(additionalImages) > 0:
for image in additionalImages:
- if isinstance(image, str) and image.startswith(
- "static/images/"
- ):
+ if image.startswith("static/images/"):
imageDict["additional"].append(image)
else:
imageFileName = handleToolkitImage(image, title)
@@ -55,18 +60,22 @@ class ToolkitHandler:
newToolkit = await Toolkit(**toolkitData).save()
except Exception as e:
logger.error(f"Ошибка сохранения инструмента: {str(e)}")
- return {}
+ return {"errorMessage": f"Ошибка сохранения инструмента: {str(e)}"}
if not newToolkit:
logger.error("Инструмент не сохранен")
- return {}
+ return {"errorMessage": "Инструмент не сохранен"}
logger.info(f"Инструмент {newToolkit.title} успешно создан")
await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData})
- return newToolkit
+ return newToolkit.toDict()
async def updateMovindDate(toolkitId: int):
- editedToolkit = await ToolkitHandler.edit(toolkitId, moved_at=datetime.now())
+ toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
+ if not toolkit:
+ logger.error("Инструмент не найден")
+ return False
+ editedToolkit = await toolkit.edit(moved_at=datetime.now())
if not editedToolkit:
logger.error("Инструмент не обновлен")
return False
@@ -74,65 +83,78 @@ class ToolkitHandler:
async def updateRefillDate(toolkitId: int):
logger.info(f"Обновление даты пополнения инструмента {toolkitId}...")
- editedToolkit = await ToolkitHandler.edit(toolkitId, refilled_at=datetime.now())
+ toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
+ if not toolkit:
+ logger.error("Инструмент не найден")
+ return False
+ editedToolkit = await toolkit.edit(refilled_at=datetime.now())
if not editedToolkit:
logger.error("Инструмент не обновлен")
return False
return True
- async def edit(toolkitId: int, **kwargs):
+ async def hideToolkit(userId: int, toolkitId: int, hidden: bool = True):
+ logger.info(
+ f"{'Скрытие' if hidden else 'Отображение'} инструмента {toolkitId}..."
+ )
+ return await ToolkitHandler.edit(userId, id=toolkitId, hidden=hidden)
+
+ async def edit(user_id: int, **kwargs):
+ title = kwargs.get("title", None)
+ toolkitId = kwargs.pop("id")
+ if title:
+ query = select(Toolkit).where(Toolkit.title == title)
+ toolkit = await CRUD.read(query)
+ if toolkit:
+ if toolkit.id != toolkitId:
+ logger.error("Инструмент с таким названием уже существует")
+ return {
+ "errorMessage": "Инструмент с таким названием уже существует"
+ }
+
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
if not toolkit:
logger.error("Инструмент не найден")
- return {}
+ return {"errorMessage": "Инструмент не найден"}
try:
if "image" in kwargs:
title = kwargs.get("title", toolkit.title)
imageData = kwargs.pop("image")
+
imageDict = {"main": "", "additional": []}
- existImagesList = [toolkit.image.get("main")]
- existImagesList.extend(toolkit.image.get("additional"))
+ if imageData.get("main").startswith("static/images/"):
+ imageDict["main"] = imageData.get("main")
+ else:
+ imageFileName = handleToolkitImage(imageData.get("main"), title)
+ if imageFileName:
+ imageDict["main"] = imageFileName
+ deleteImage(toolkit.image.get("main"))
- newImagesList = [imageData.get("main")]
- newImagesList.extend(imageData.get("additional"))
-
- for existImage in existImagesList:
- if existImage not in newImagesList:
- deleteImage(existImage)
-
- if toolkit.image.get("main") != imageData.get("main"):
- if imageData.get("main") in existImagesList:
- imageDict["main"] = imageData.get("main")
+ for image in imageData.get("additional"):
+ if image.startswith("static/images/"):
+ imageDict["additional"].append(image)
else:
- imageFileName = handleToolkitImage(imageData.get("main"), title)
- if imageFileName:
- imageDict["main"] = imageFileName
- else:
- imageDict["main"] = "images/tools/default.png"
-
- imageDict["additional"].extend(imageData.get("additional"))
-
- uploadList = imageData.get("upload", [])
- if len(uploadList) > 0:
- for image in uploadList:
imageFileName = handleToolkitImage(image, title)
if imageFileName:
imageDict["additional"].append(imageFileName)
+ for existImage in toolkit.image.get("additional"):
+ if existImage not in imageDict.get("additional"):
+ deleteImage(existImage)
+
kwargs["image"] = imageDict
- user_id = kwargs.pop("user_id", None)
logger.debug(f"Обновление инструмента {toolkit.title}...")
editedToolkit = await toolkit.edit(**kwargs)
except Exception as e:
logger.error(f"Ошибка обновления инструмента: {str(e)}")
- return {}
+ return {"errorMessage": f"Ошибка обновления инструмента: {str(e)}"}
if not editedToolkit:
logger.error("Инструмент не обновлен")
- return {}
+ return {"errorMessage": "Инструмент не обновлен"}
logger.info(
f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}"
@@ -166,24 +188,28 @@ class ToolkitHandler:
return True if toolkit else False
async def delete(toolkitId: int, user_id: int = None):
+ movements = await StockHandler.checkToolkitExists(toolkitId)
+ if movements:
+ logger.error("По инструменту было движение")
+ return {"errorMessage": "По инструменту было движение"}
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
if not toolkit:
logger.error("Инструмент не найден")
- return False
+ return {"errorMessage": "Инструмент не найден"}
try:
toolkitTitle = toolkit.title
result = await CRUD.delete(toolkit)
except Exception as e:
logger.error(f"Ошибка удаления инструмента: {str(e)}")
- return False
+ return {"errorMessage": f"Ошибка удаления инструмента: {str(e)}"}
logger.info(
f"Инструмент {toolkitTitle} {'успешно удален' if result else 'не удален'}"
)
await ServiceRecordsHandler.add(
user_id, {"Удален инструмент": f"Название: {toolkitTitle}"}
)
- return result
+ return {"status": "ok"} if result else {"errorMessage": "Инструмент не удален"}
async def initialize():
from .categories import CategoryHandler
diff --git a/db/schemas/__pycache__/toolkit.cpython-313.pyc b/db/schemas/__pycache__/toolkit.cpython-313.pyc
index 4ffb76e..d8a885e 100644
Binary files a/db/schemas/__pycache__/toolkit.cpython-313.pyc and b/db/schemas/__pycache__/toolkit.cpython-313.pyc differ
diff --git a/db/schemas/toolkit.py b/db/schemas/toolkit.py
index d3c9842..7e29222 100644
--- a/db/schemas/toolkit.py
+++ b/db/schemas/toolkit.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
+from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship
from db import Base, CRUD
@@ -25,6 +25,7 @@ class Toolkit(Base):
quantity_min = Column(Integer, nullable=True)
quantity_min_extra = Column(Integer, nullable=True)
external_link = Column(String, nullable=True)
+ hidden = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
refilled_at = Column(DateTime, default=datetime.now)
diff --git a/main.py b/main.py
index 598c13e..5e2761f 100644
--- a/main.py
+++ b/main.py
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer
try:
- force = False
- reNewDB = False
+ force = True
+ reNewDB = True
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
diff --git a/utils/__pycache__/image.cpython-313.pyc b/utils/__pycache__/image.cpython-313.pyc
index 67d6f85..12deb4c 100644
Binary files a/utils/__pycache__/image.cpython-313.pyc and b/utils/__pycache__/image.cpython-313.pyc differ
diff --git a/utils/image.py b/utils/image.py
index 14d9c6b..e7f2a55 100644
--- a/utils/image.py
+++ b/utils/image.py
@@ -32,7 +32,7 @@ def saveImage(file_bytes: bytes, file_name: str) -> bool:
if not target_path.lower().endswith(".png"):
target_path += ".png"
- logger.info(f"[ImageSave] Saving image to {target_path}")
+ logger.debug(f"[ImageSave] Saving image to {target_path}")
img.save(target_path, "PNG")
return True
@@ -47,10 +47,10 @@ def deleteImage(fileName: str):
try:
import os
- file_name = f"api/{file_name}"
- logger.info(f"Удаляем изображение {fileName}")
- os.remove(f"static/images/{fileName}")
- logger.info(f"Изображение {fileName} успешно удалено")
+ fileName = f"api/{fileName}"
+ logger.debug(f"Удаляем изображение {fileName}")
+ os.remove(fileName)
+ logger.debug(f"Изображение {fileName} успешно удалено")
return True
except Exception as e:
logger.error(f"Ошибка удаления изображения: {str(e)}")