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; let currentToolboxData = null; // Список предопределенных характеристик с разделами const predefinedSpecs = { "Универсальные": [ "Диаметр", "Длина", "Черновая", "Чистовая", "Материал инструмента", "Покрытие (TiN, TiAlN, AlTiN)", "Тип хвостовика", "Назначение", "По стали", "По нержавейке", "По алюминию", "Твёрдый сплав", "HSS" ], "Фрезеровка": [ "Кол-во перьев", "Тип фрезы (концевая, торцевая, черновая)", "Угол спирали", "Геометрия зубьев" ], "Токарка": [ "Пластины", "Форма пластины (C, D, V, W, T)", "Радиус", "Наружная", "Внутренняя", "Резьбовая", "Шаг", "Тип державки", "Направление (правое/левое)", "Система крепления" ], "Сверла": [ "Угол заточки (118°, 135°)", "Тип хвостовика (цилиндрический, Морзе)", "Глубокое сверление" ], "Инструмент для ЧПУ": [ "Тип инструмента (фреза, сверло, развертка, гравёр, фасочник)", "Тип обработки (2D, 3D, контурная, карманная)", "Ступенчатая геометрия", "Тип крепления (ER, Weldon, HSK, BT, ISO)", "Максимальные обороты", "Максимальная подача", "Допуск биения", "Тип спирали (правосторонняя, левосторонняя)", "Длина режущей части", "Рабочая часть" ] }; async function getCookieData() { accessData = await getCookie('toolbox_access'); userData = await getCookie('toolbox_user'); } async function openTab(event, tabId) { // Убираем активный класс со всех вкладок и кнопок document.querySelectorAll('.tab-nav-btn').forEach(btn => { btn.classList.remove('active'); btn.querySelector('.nav-icon').classList.remove('text-primary'); btn.querySelector('.nav-icon').classList.add('text-muted'); }); document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.remove('show', 'active'); }); // Добавляем активный класс выбранной вкладке и кнопке event.currentTarget.classList.add('active'); event.currentTarget.querySelector('.nav-icon').classList.remove('text-muted'); event.currentTarget.querySelector('.nav-icon').classList.add('text-primary'); document.getElementById(tabId).classList.add('show', 'active'); // Сохраняем выбранную вкладку saveToStorage('tab', { tabId }); // Загружаем содержимое вкладки await uploadTab(tabId); } function prepareTabs() { let tabsData = { 'toolbox': { title: 'Склад', icon: 'bi-box-seam', description: 'Управление остатками инструмента на складе' }, 'toolkits': { title: 'Инструменты', icon: 'bi-tools', description: 'Каталог инструментов' }, }; if (accessData.available_own_toolbox || accessData.refund_request_confirm || accessData.debit_request_confirm) { tabsData['requests'] = { title: 'Запросы', icon: 'bi-chat-left-text', description: 'Управление запросами на инструменты' }; } if (accessData.view_requests) { tabsData['jurnal_toolkits'] = { title: 'Журнал перемещений', icon: 'bi-journal-text', description: 'Журнал перемещений инструментов' }; } if (accessData.view_services) { tabsData['jurnal_service'] = { title: 'Сервисный журнал', icon: 'bi-journal-richtext', description: 'Журнал сервисных запросов' }; } if (accessData.users_view) { tabsData['users'] = { title: 'Пользователи', icon: 'bi-people', description: 'Управление пользователями' }; } const tabs = `
${Object.entries(tabsData).map(([tabId, tabData]) => `

${tabData.title}

${tabData.description}

Загрузка...
`).join('')}
`; const mainContainer = document.getElementById('mainContent'); mainContainer.insertAdjacentHTML('afterbegin', tabs); const activeTabData = loadFromStorage('tab') || null; if (activeTabData) { const activeTabId = activeTabData.tabId; const tabBtn = document.getElementById(`${activeTabId}-tab`); if (tabBtn) { openTab({ currentTarget: tabBtn }, activeTabId); } } } async function uploadTab(tabId) { const cookiesData = { userData, accessData }; try { const resp = await apiRequest('/', { tabId, cookiesData }); if (resp.status == 'ok') { fillTab(tabId, resp.data); } else { throw new Error(resp.message || 'Ошибка загрузки данных'); } } catch (error) { console.error('Error loading tab:', error); const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = ` `; } } function fillTab(tabId, tabData) { try { switch (tabId) { case 'toolbox': renderToolboxTab(tabData); break; case 'requests': renderRequestsTab(tabId, tabData); break; case 'toolkits': renderToolkitsTab(tabId, tabData.toolkits, tabData.categories); break; case 'jurnal_toolkits': renderJurnalToolkitsTab(tabId, tabData); break; case 'jurnal_service': renderJurnalServicesTab(tabId, tabData); break; case 'users': renderSimpleTab(tabId, tabData, 'Пользователи системы'); break; } } catch (error) { console.error('Error filling tab:', error); const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = ` `; } } function renderSimpleTab(tabId, tabData, title) { const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = `
${title}
${Object.entries(tabData).map(([key, value]) => `
${key}

${typeof value === 'object' ? JSON.stringify(value, null, 4) : value}

`).join('')}
`; } async function manageCategory(categoriesList) { // Удаляем старое модальное окно, если оно существует let modal = document.getElementById('manageCategoryModal'); if (modal) modal.remove(); // Храним изменения const changes = { create: [], // новые категории update: [], // измененные категории delete: [] // id категорий для удаления }; // Делаем копию списка категорий для работы с дополнительными полями const categories = categoriesList.map(cat => ({ ...cat, status: 'unchanged', // unchanged, new, edited, deleted originalData: null })); // Создаём модальное окно modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'manageCategoryModal'; modal.tabIndex = -1; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` `; document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); const categoriesListBody = modal.querySelector('#categoriesListBody'); const newCategoryFormRow = modal.querySelector('#newCategoryFormRow'); const addCategoryBtn = modal.querySelector('#addCategoryBtn'); const cancelNewCategoryBtn = modal.querySelector('#cancelNewCategoryBtn'); const saveNewCategoryBtn = modal.querySelector('#saveNewCategoryBtn'); const saveAllChangesBtn = modal.querySelector('#saveAllChangesBtn'); const saveChangesSpinner = modal.querySelector('#saveChangesSpinner'); const saveChangesText = modal.querySelector('#saveChangesText'); const addChangesItems = modal.querySelector('#addChangesItems'); const editChangesItems = modal.querySelector('#editChangesItems'); const deleteChangesItems = modal.querySelector('#deleteChangesItems'); const noChangesMessage = modal.querySelector('#noChangesMessage'); const addChangesList = modal.querySelector('#addChangesList'); const editChangesList = modal.querySelector('#editChangesList'); const deleteChangesList = modal.querySelector('#deleteChangesList'); // Функция для обновления панели изменений function updateChangesPanel() { // Очищаем панели addChangesItems.innerHTML = ''; editChangesItems.innerHTML = ''; deleteChangesItems.innerHTML = ''; // Собираем изменения const newCategories = categories.filter(cat => cat.status === 'new'); const editedCategories = categories.filter(cat => cat.status === 'edited'); const deletedCategories = categories.filter(cat => cat.status === 'deleted'); // Обновляем отображение панелей if (newCategories.length > 0) { addChangesList.classList.remove('d-none'); newCategories.forEach((category, index) => { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center mb-1'; item.innerHTML = ` ${escapeHtml(category.title)} `; addChangesItems.appendChild(item); }); } else { addChangesList.classList.add('d-none'); } if (editedCategories.length > 0) { editChangesList.classList.remove('d-none'); editedCategories.forEach((category, index) => { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center mb-1'; item.innerHTML = ` ${escapeHtml(category.title)} `; editChangesItems.appendChild(item); }); } else { editChangesList.classList.add('d-none'); } if (deletedCategories.length > 0) { deleteChangesList.classList.remove('d-none'); deletedCategories.forEach((category, index) => { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center mb-1'; item.innerHTML = ` ${escapeHtml(category.title)} `; deleteChangesItems.appendChild(item); }); } else { deleteChangesList.classList.add('d-none'); } // Показываем/скрываем сообщение "нет изменений" const hasChanges = newCategories.length > 0 || editedCategories.length > 0 || deletedCategories.length > 0; if (hasChanges) { noChangesMessage.classList.add('d-none'); } else { noChangesMessage.classList.remove('d-none'); } } // Функция для рендеринга списка категорий function renderCategoriesList() { categoriesListBody.innerHTML = ''; categories.forEach((category, index) => { const status = category.status; // Определяем стили в зависимости от статуса let rowClass = ''; let badge = ''; switch (status) { case 'new': rowClass = 'table-success'; badge = 'Новая'; break; case 'edited': rowClass = 'table-warning'; badge = 'Изменена'; break; case 'deleted': rowClass = 'table-danger'; badge = 'Удалена'; break; default: rowClass = ''; badge = ''; } // Для удаленных категорий показываем только с кнопкой восстановления if (status === 'new') { // Для новых категорий показываем только кнопку отмены categoriesListBody.innerHTML += `
${escapeHtml(category.title)}
${badge}
${escapeHtml(category.description)}
`; } else if (status === 'deleted') { // Для удаленных категорий показываем кнопку восстановления categoriesListBody.innerHTML += `
${escapeHtml(category.title)}
${badge}
${escapeHtml(category.description)}
`; } else { // Для остальных категорий (unchanged, edited) показываем обычные кнопки categoriesListBody.innerHTML += `
${escapeHtml(category.title)}
${badge}
${escapeHtml(category.description)}
${category.created_at ? new Date(category.created_at).toLocaleDateString('ru-RU') : 'Новая'} ${category.updated_at ? new Date(category.updated_at).toLocaleDateString('ru-RU') : 'Новая'}
`; } }); } // Функция для экранирования HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Функции для работы с категориями modal.editCategory = function (index) { // Сохраняем оригинальные данные если еще не сохранены if (!categories[index].originalData) { categories[index].originalData = { title: categories[index].title, description: categories[index].description }; } // Создаем модальное окно для редактирования const editModal = document.createElement('div'); editModal.className = 'modal fade'; editModal.id = 'editCategoryModal'; editModal.innerHTML = ` `; document.body.appendChild(editModal); const bsEditModal = new bootstrap.Modal(editModal); // Обработчик сохранения изменений editModal.querySelector('#saveEditCategoryBtn').addEventListener('click', () => { const titleInput = editModal.querySelector('#editCategoryTitle'); const descriptionInput = editModal.querySelector('#editCategoryDescription'); const title = titleInput.value.trim(); const description = descriptionInput.value.trim(); // Валидация if (!title || title.length < 2) { showInfo('Название категории должно содержать минимум 2 символа', 'error'); titleInput.focus(); return; } if (!description || description.length < 2) { showInfo('Описание категории должно содержать минимум 2 символа', 'error'); descriptionInput.focus(); return; } // Проверяем уникальность названия среди других категорий const duplicate = categories.find((cat, i) => i !== index && cat.id !== categories[index].id && cat.title.toLowerCase() === title.toLowerCase() && cat.status !== 'deleted' ); if (duplicate) { showInfo('Категория с таким названием уже существует', 'error'); titleInput.focus(); return; } // Обновляем данные категории categories[index].title = title; categories[index].description = description; categories[index].status = 'edited'; // Добавляем в изменения, если это существующая категория if (categories[index].id) { const updateIndex = changes.update.findIndex(item => item.id === categories[index].id); if (updateIndex === -1) { changes.update.push({ id: categories[index].id, title: title, description: description }); } else { changes.update[updateIndex] = { id: categories[index].id, title: title, description: description }; } } bsEditModal.hide(); setTimeout(() => { editModal.remove(); renderCategoriesList(); updateChangesPanel(); showInfo('Изменения сохранены', 'success'); }, 300); }); // Очистка при закрытии модалки editModal.addEventListener('hidden.bs.modal', () => { setTimeout(() => { if (editModal.parentNode) editModal.remove(); }, 300); }); bsEditModal.show(); setTimeout(() => { editModal.querySelector('#editCategoryTitle').focus(); }, 100); }; modal.cancelEditCategoryAction = function (index) { if (categories[index].originalData) { // Восстанавливаем исходные данные categories[index].title = categories[index].originalData.title; categories[index].description = categories[index].originalData.description; categories[index].status = 'unchanged'; categories[index].originalData = null; // Удаляем из изменений if (categories[index].id) { const updateIndex = changes.update.findIndex(item => item.id === categories[index].id); if (updateIndex !== -1) { changes.update.splice(updateIndex, 1); } } renderCategoriesList(); updateChangesPanel(); showInfo('Изменения отменены', 'info'); } }; modal.deleteCategory = function (index) { // Сохраняем оригинальные данные если еще не сохранены if (!categories[index].originalData) { categories[index].originalData = { title: categories[index].title, description: categories[index].description }; } // Создаем модальное окно подтверждения удаления const deleteModal = document.createElement('div'); deleteModal.className = 'modal fade'; deleteModal.id = 'deleteCategoryModal'; deleteModal.innerHTML = ` `; document.body.appendChild(deleteModal); const bsDeleteModal = new bootstrap.Modal(deleteModal); // Обработчик подтверждения удаления deleteModal.querySelector('#confirmDeleteCategoryBtn').addEventListener('click', () => { categories[index].status = 'deleted'; // Добавляем в изменения, если это существующая категория if (categories[index].id) { const deleteIndex = changes.delete.indexOf(categories[index].id); if (deleteIndex === -1) { changes.delete.push(categories[index].id); } } else { // Если это новая категория - удаляем из изменений на создание const createIndex = changes.create.findIndex(item => item.title === categories[index].originalData.title && item.description === categories[index].originalData.description ); if (createIndex !== -1) { changes.create.splice(createIndex, 1); } } bsDeleteModal.hide(); setTimeout(() => { deleteModal.remove(); renderCategoriesList(); updateChangesPanel(); showInfo('Категория помечена для удаления', 'warning'); }, 300); }); // Очистка при закрытии модалки deleteModal.addEventListener('hidden.bs.modal', () => { setTimeout(() => { if (deleteModal.parentNode) deleteModal.remove(); }, 300); }); bsDeleteModal.show(); }; modal.cancelDeleteCategoryAction = function (index) { // Восстанавливаем исходные данные categories[index].title = categories[index].originalData.title; categories[index].description = categories[index].originalData.description; categories[index].status = 'unchanged'; categories[index].originalData = null; // Удаляем из изменений на удаление if (categories[index].id) { const deleteIndex = changes.delete.indexOf(categories[index].id); if (deleteIndex !== -1) { changes.delete.splice(deleteIndex, 1); } } renderCategoriesList(); updateChangesPanel(); showInfo('Удаление отменено', 'info'); }; modal.restoreCategory = function (index) { // Восстанавливаем категорию categories[index].status = 'unchanged'; categories[index].originalData = null; // Удаляем из изменений на удаление if (categories[index].id) { const deleteIndex = changes.delete.indexOf(categories[index].id); if (deleteIndex !== -1) { changes.delete.splice(deleteIndex, 1); } } renderCategoriesList(); updateChangesPanel(); showInfo('Категория восстановлена', 'success'); }; modal.cancelNewCategoryAction = function (index) { // Удаляем новую категорию if (categories[index].status === 'new') { // Удаляем из изменений на создание const createIndex = changes.create.findIndex(item => item.title === categories[index].title && item.description === categories[index].description ); if (createIndex !== -1) { changes.create.splice(createIndex, 1); } // Удаляем из массива категорий categories.splice(index, 1); renderCategoriesList(); updateChangesPanel(); showInfo('Новая категория удалена', 'info'); } }; // Инициализируем список категорий renderCategoriesList(); updateChangesPanel(); // Обработчики событий addCategoryBtn.addEventListener('click', () => { newCategoryFormRow.classList.remove('d-none'); addCategoryBtn.disabled = true; setTimeout(() => { const titleInput = modal.querySelector('#newCategoryTitle'); if (titleInput) titleInput.focus(); }, 10); }); cancelNewCategoryBtn.addEventListener('click', () => { newCategoryFormRow.classList.add('d-none'); addCategoryBtn.disabled = false; modal.querySelector('#newCategoryTitle').value = ''; modal.querySelector('#newCategoryDescription').value = ''; }); saveNewCategoryBtn.addEventListener('click', () => { const titleInput = modal.querySelector('#newCategoryTitle'); const descriptionInput = modal.querySelector('#newCategoryDescription'); const title = titleInput.value.trim(); const description = descriptionInput.value.trim(); // Валидация if (!title || title.length < 2) { showInfo('Название категории должно содержать минимум 2 символа', 'error'); titleInput.focus(); return; } if (!description || description.length < 2) { showInfo('Описание категории должно содержать минимум 2 символа', 'error'); descriptionInput.focus(); return; } // Проверяем уникальность названия const duplicate = categories.find(cat => cat.title.toLowerCase() === title.toLowerCase() && cat.status !== 'deleted' ); if (duplicate) { showInfo('Категория с таким названием уже существует', 'error'); titleInput.focus(); return; } // Добавляем новую категорию const newCategory = { title: title, description: description, status: 'new', originalData: null }; categories.push(newCategory); changes.create.push({ title: title, description: description }); // Сбрасываем форму titleInput.value = ''; descriptionInput.value = ''; newCategoryFormRow.classList.add('d-none'); addCategoryBtn.disabled = false; // Обновляем отображение renderCategoriesList(); updateChangesPanel(); showInfo('Категория добавлена', 'success'); }); // Сохранение всех изменений saveAllChangesBtn.addEventListener('click', async function () { // Проверяем, есть ли изменения const hasChanges = changes.create.length > 0 || changes.update.length > 0 || changes.delete.length > 0; if (!hasChanges) { showInfo('Нет изменений для сохранения', 'info'); return; } // Сохраняем исходное состояние кнопки const originalText = saveChangesText.textContent; const originalDisabledState = saveAllChangesBtn.disabled; // Двойное подтверждение saveChangesText.textContent = 'Нажмите еще раз для подтверждения (10 сек)'; saveAllChangesBtn.disabled = true; let confirmed = false; const timeout = setTimeout(() => { if (!confirmed) { // Возвращаем кнопку в исходное состояние saveChangesText.textContent = originalText; saveAllChangesBtn.disabled = originalDisabledState; saveChangesSpinner.style.display = 'none'; } }, 10000); const confirmHandler = async function () { confirmed = true; clearTimeout(timeout); saveAllChangesBtn.disabled = true; saveChangesSpinner.style.display = 'inline-block'; try { // Отправляем запрос на сохранение изменений const response = await apiRequest('/toolkit/categories_batch', { changes: changes, userId: userData.id }, 'POST'); if (response.status === 'ok') { showInfo('Изменения успешно сохранены', 'success'); bsModal.hide(); // Обновляем вкладку инструментов await uploadTab('toolkits'); } else { throw new Error(response.message || 'Ошибка при сохранении изменений'); } } catch (error) { console.error('Ошибка при сохранении изменений:', error); // Возвращаем кнопку в исходное состояние saveAllChangesBtn.disabled = false; saveChangesSpinner.style.display = 'none'; saveChangesText.textContent = originalText; const errorDiv = modal.querySelector('#manageCategoryError'); const errorMessage = modal.querySelector('#manageCategoryErrorMessage'); if (errorDiv && errorMessage) { errorMessage.textContent = error.message || 'Произошла ошибка при сохранении изменений'; errorDiv.classList.remove('d-none'); errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Сбрасываем состояние подтверждения confirmed = false; } }; // Обработчик для второго клика const secondClickHandler = function () { saveAllChangesBtn.removeEventListener('click', secondClickHandler); confirmHandler(); }; saveAllChangesBtn.addEventListener('click', secondClickHandler); saveAllChangesBtn.disabled = false; saveChangesText.textContent = 'Подтвердите сохранение (10 сек)'; }); // Очистка при закрытии модалки modal.addEventListener('hidden.bs.modal', () => { setTimeout(() => { if (modal.parentNode) modal.remove(); }, 300); }); // Делаем функции глобально доступными для обработчиков onclick window.editCategory = modal.editCategory; window.cancelEditCategoryAction = modal.cancelEditCategoryAction; window.deleteCategory = modal.deleteCategory; window.cancelDeleteCategoryAction = modal.cancelDeleteCategoryAction; window.restoreCategory = modal.restoreCategory; window.cancelNewCategoryAction = modal.cancelNewCategoryAction; // Показываем модалку bsModal.show(); return new Promise((resolve) => { modal.addEventListener('hidden.bs.modal', () => { resolve(null); }); }); } function renderToolkitsTab(tabId, toolsList, categoriesArray) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const hiddenToolCount = toolsList.filter(tool => tool.hidden).length; let categoriesData = {}; categoriesArray.forEach(cat => { categoriesData[cat.id] = { id: cat.id, title: cat.title, description: cat.description }; }); let specData = {} toolsList.forEach(tool => { tool['category'] = categoriesData[tool.category_id]?.title || ''; tool['category_desc'] = categoriesData[tool.category_id]?.description || ''; Object.entries(tool.specifications || {}).forEach(([name, value]) => { if (specData[name]) { if (!specData[name].includes(value)) { specData[name].push(value); } } else { specData[name] = [value]; } }); }); function smartCompare(a, b, locale = 'ru') { const normalizeNumber = (v) => { if (typeof v === 'number') return v; if (typeof v === 'string') { const n = v.replace(',', '.').trim(); if (!isNaN(n) && n !== '') return Number(n); } return null; }; const numA = normalizeNumber(a); const numB = normalizeNumber(b); // Оба — числа if (numA !== null && numB !== null) { return numA - numB; } // Один число, другой строка → число выше if (numA !== null) return -1; if (numB !== null) return 1; // Оба строки return String(a).localeCompare(String(b), locale, { numeric: true, sensitivity: 'base' }); } // Сортируем ключи и значения specData const sortedSpecData = Object.fromEntries( Object.entries(specData) .sort(([keyA], [keyB]) => smartCompare(keyA, keyB)) .map(([key, values]) => [ key, [...values].sort((a, b) => smartCompare(a, b)) ]) ); toolsList.sort((a, b) => a.title.localeCompare(b.title, 'ru')); categoriesArray.sort((a, b) => a.title.localeCompare(b.title, 'ru')); if (accessData.tools_creation) { tabOptionalContent.innerHTML = `
${categoriesArray.map(category => ` `).join('')}
`; const manageCategoryBtn = document.getElementById('manageCategoryBtn'); manageCategoryBtn.addEventListener('click', () => manageCategory(categoriesArray)); const addToolBtn = document.getElementById('addToolBtn'); addToolBtn.addEventListener('click', () => manageToolkit()); } else { tabOptionalContent.innerHTML = `
${categoriesArray.map(category => ` `).join('')}
`; } // Создаем HTML структуру с двумя выпадающими списками tabContent.innerHTML = `
`; // Рендерим карточки renderToolkitCards(tabId, toolsList, categoriesData); // Добавляем обработчики событий для фильтров setupFilters(tabId, toolsList, categoriesData, sortedSpecData); } // Функция для рендеринга карточек function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all', showHiddenTools = false, specParam = '', specValue = '') { const container = document.getElementById(`${tabId}-cards-container`); // Фильтруем инструменты const filteredTools = tools.filter(tool => { // Фильтр по категории if (categoryFilter !== 'all' && tool.category_id !== parseInt(categoryFilter)) { return false; } // Фильтр по скрытым инструментам if (!showHiddenTools && tool.hidden) { return false; } // Фильтр по поисковому запросу if (filterText) { const searchLower = filterText.toLowerCase(); const titleMatch = tool.title.toLowerCase().includes(searchLower); const descriptionMatch = tool.description.toLowerCase().includes(searchLower); if (!titleMatch && !descriptionMatch) { return false; } } // Фильтр по спецификациям if (specParam && specValue) { if (!tool.specifications || tool.specifications[specParam] !== specValue) { return false; } } return true; }); // Рендерим карточки if (filteredTools.length === 0) { container.innerHTML = `
Ничего не найдено. Попробуйте изменить параметры фильтрации.
`; return; } container.innerHTML = filteredTools.map(tool => { const categoryName = categoriesMap[tool.category_id]?.title || `Категория ${tool.category_id}`; const description = tool.description || 'Описание отсутствует'; const imageUrl = tool.image?.main || 'static/images/tools/default.png'; return `
${tool.title || 'Инструмент'} ${categoryName} ${tool.hidden ? ` ` : ''}
${tool.title || 'Без названия'}

${description}

${tool.quantity_min && accessData.view_all_toolboxes ? ` Мин: ${tool.quantity_min} ${tool.quantity_min_extra ? `( ${tool.quantity_min_extra})` : ''} ` : ''}
`; }).join(''); const cards = container.querySelectorAll('.toolkit-card'); let activeModal = null; cards.forEach(card => { card.addEventListener('click', async event => { if (activeModal) return; const toolId = event.currentTarget.dataset.toolid; activeModal = true; await showToolkitDetailsModal(toolId); activeModal = null; }); }); } // Функция для настройки фильтров function setupFilters(tabId, tools, categoriesMap, specData) { const searchInput = document.getElementById(`${tabId}-search-input`); const filterButtons = document.querySelectorAll(`#${tabId}-tab-optional-content .filter-btn`); const showHiddenToolsCheckbox = document.getElementById('showHiddenTools'); // Новые элементы для фильтрации по спецификациям const paramSelect = document.getElementById(`${tabId}-param-select`); const valueSelect = document.getElementById(`${tabId}-value-select`); const findSpecBtn = document.getElementById(`${tabId}-find-spec-btn`); const resetSpecBtn = document.getElementById(`${tabId}-reset-spec-btn`); const clearSearchBtn = document.getElementById(`${tabId}-clear-search`); const savedFilters = loadFromStorage(tabId); const currentFilter = { category: savedFilters.category || 'all', search: savedFilters.search || '', showHidden: savedFilters.showHidden ?? false, specParam: savedFilters.specParam || '', specValue: savedFilters.specValue || '' }; /* ---------- Восстановление UI ---------- */ if (searchInput) { searchInput.value = currentFilter.search; } if (showHiddenToolsCheckbox) { showHiddenToolsCheckbox.checked = currentFilter.showHidden; } filterButtons.forEach(btn => { btn.classList.toggle( 'active', btn.dataset.category === currentFilter.category ); }); // Восстановление выбранного параметра if (paramSelect && currentFilter.specParam) { paramSelect.value = currentFilter.specParam; updateValueSelect(currentFilter.specParam); // Восстановление выбранного значения после обновления списка значений setTimeout(() => { if (valueSelect && currentFilter.specValue) { valueSelect.value = currentFilter.specValue; } }, 0); } const render = () => { renderToolkitCards( tabId, tools, categoriesMap, currentFilter.search, currentFilter.category, currentFilter.showHidden, currentFilter.specParam, currentFilter.specValue ); saveToStorage(tabId, currentFilter); }; /* ---------- Обновление списка значений при выборе параметра ---------- */ function updateValueSelect(selectedParam) { if (valueSelect) { valueSelect.innerHTML = ''; findSpecBtn.disabled = true; if (selectedParam && specData[selectedParam]) { valueSelect.disabled = false; specData[selectedParam].forEach(value => { const option = document.createElement('option'); option.value = value; option.textContent = value; valueSelect.appendChild(option); }); } else { valueSelect.disabled = true; } } } /* ---------- Сброс фильтра спецификаций ---------- */ function resetSpecFilter() { if (paramSelect) { paramSelect.value = ''; } if (valueSelect) { valueSelect.innerHTML = ''; valueSelect.disabled = true; } currentFilter.specParam = ''; currentFilter.specValue = ''; render(); } /* ---------- Обработчик выбора параметра ---------- */ if (paramSelect) { paramSelect.addEventListener('change', function () { const selectedParam = this.value; updateValueSelect(selectedParam); // Сбрасываем выбранное значение при изменении параметра if (valueSelect) { valueSelect.value = ''; } }); } /* ---------- Обработчик выбора значения ---------- */ if (valueSelect) { valueSelect.addEventListener('change', function () { findSpecBtn.disabled = !this.value; }); } /* ---------- Обработчик кнопки "Найти" для спецификаций ---------- */ if (findSpecBtn) { findSpecBtn.addEventListener('click', function () { currentFilter.specParam = paramSelect ? paramSelect.value : ''; currentFilter.specValue = valueSelect && !valueSelect.disabled ? valueSelect.value : ''; render(); }); } /* ---------- Обработчик кнопки "Сброс" для спецификаций ---------- */ if (resetSpecBtn) { resetSpecBtn.addEventListener('click', resetSpecFilter); findSpecBtn.disabled = true; } /* ---------- Чекбокс ---------- */ if (showHiddenToolsCheckbox) { showHiddenToolsCheckbox.addEventListener('change', () => { currentFilter.showHidden = showHiddenToolsCheckbox.checked; render(); }); } /* ---------- Категории ---------- */ filterButtons.forEach(button => { button.addEventListener('click', function () { filterButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); currentFilter.category = this.dataset.category; render(); }); }); /* ---------- Поиск ---------- */ if (searchInput) { let searchTimeout; searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { currentFilter.search = this.value.trim(); render(); }, 300); }); // Кнопка очистки поиска if (clearSearchBtn) { clearSearchBtn.addEventListener('click', () => { searchInput.value = ''; currentFilter.search = ''; clearSearchBtn.classList.add('d-none'); render(); }); searchInput.addEventListener('input', function () { clearSearchBtn.classList.toggle('d-none', !this.value); }); clearSearchBtn.classList.toggle('d-none', !searchInput.value); } } /* ---------- Первый рендер ---------- */ render(); } function loadFromStorage(title) { try { return JSON.parse(localStorage.getItem(`toolboxStotage:${title}`)) || {}; } catch { return {}; } } function saveToStorage(title, data) { localStorage.setItem( `toolboxStotage:${title}`, JSON.stringify(data) ); } function addToolbox(editData = null) { // Проверяем, существует ли уже модальное окно 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 = ` `; // Если редактирование if (editData) { modal.querySelector('#toolboxTitle').value = editData.title; modal.querySelector('#toolboxDescription').value = editData.description; modal.querySelector('#toolboxMonitoring').checked = editData.monitoring; modal.querySelector('#addToolboxModalLabel').textContent = 'Редактировать склад'; modal.querySelector('#submitToolboxText').textContent = 'Сохранить'; if (editData.owner_id) { modal.querySelector('#toolboxMonitoringContainer').classList.add('d-none'); } } // Добавляем модальное окно в DOM 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; let editToolboxData = {} if (editData) { Object.keys(toolboxData).forEach(key => { if (toolboxData[key] !== editData[key]) { editToolboxData[key] = toolboxData[key]; } }); } if (Object.keys(editToolboxData).length === 0 && editData) { showInfo('Новые данные склада совпадают с текущими', 'warning'); // Возвращаем кнопку в исходное состояние submitBtn.disabled = false; spinner.style.display = 'none'; return; } try { // Отправка данных let method = 'POST' let sendData = { toolboxData, userId } if (editData) { method = 'PUT'; const toolboxId = editData.id; sendData = { toolboxId, userId, editToolboxData }; } const response = await apiRequest("/toolbox/", sendData, method); if (response.status !== 'ok') { if (!editData) { throw new Error('Ошибка при добавлении склада'); } else { throw new Error('Ошибка при обновлении склада'); } } // Успешная отправка bsModal.hide(); // Показываем уведомление об успехе const successMessageText = editData ? 'Склад успешно обновлен' : 'Склад успешно добавлен'; showInfo(successMessageText, 'success'); // Здесь можно добавить обновление списка складов await uploadTab('toolbox'); } catch (error) { console.error('Ошибка при добавлении (обновлении) склада:', error); // Возвращаем кнопку в исходное состояние submitBtn.disabled = false; spinner.style.display = 'none'; // Показываем сообщение об ошибке const errorMessageText = editData ? 'Ошибка при обновлении склада' : 'Ошибка при добавлении склада'; showInfo(errorMessageText, 'error'); // Можно добавить более детальное сообщение об ошибке const errorDiv = document.createElement('div'); errorDiv.className = 'alert alert-danger mt-3'; errorDiv.innerHTML = !editData ? ` Ошибка! Не удалось добавить склад. Проверьте соединение и попробуйте еще раз. ` : ` Ошибка! Не удалось обновить склад. Проверьте соединение и попробуйте еще раз. `; 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`); const tabOptionalContent = document.getElementById(`toolbox-tab-optional-content`); if (!tabData || tabData.length === 0) { tabContent.innerHTML = `

Нет доступных складов

У вас нет доступа ни к одному складу

`; return; } // Создаем навигацию по складам // Сортируем список складов по названию tabData.sort((a, b) => a.title.localeCompare(b.title, 'ru')); tabOptionalContent.innerHTML = `
${tabData.map((toolbox, index) => ` `).join('')}
`; const toolboxNav = document.getElementById('toolboxNav'); toolboxNav.addEventListener('click', async (event) => { const button = event.target.closest('.toolbox-nav-btn'); if (!button) return; if (button.dataset.toolboxId) { try { await selectToolbox(button.dataset.toolboxId); } catch (err) { console.error('Ошибка выбора склада:', err); } } }); // Создаем контейнер для содержимого склада tabContent.innerHTML = `

Выберите склад для просмотра

Для отображения содержимого склада нажмите на одну из кнопок выше

`; if (accessData.manage_toolboxes) { const addToolboxBtn = document.createElement('button'); addToolboxBtn.className = 'btn btn-outline-success toolbox-nav-btn'; addToolboxBtn.innerHTML = ` Добавить `; addToolboxBtn.addEventListener('click', function (e) { e.preventDefault(); addToolbox(); }); document.getElementById('toolboxNav').appendChild(addToolboxBtn); } const choiceToolbox = loadFromStorage('toolbox'); if (choiceToolbox.toolboxId) { (async () => { await selectToolbox(choiceToolbox.toolboxId); })().catch(err => { console.error('Ошибка выбора склада:', err); }); } } // Функция для выбора склада async function selectToolbox(toolboxId) { if (typeof toolboxId === 'string') { try { toolboxId = parseInt(toolboxId); } catch (err) { console.error('Неверный идентификатор склада:', toolboxId); return; } } document.querySelectorAll('.toolbox-nav-btn') .forEach(btn => btn.classList.remove('active')); const selectedBtn = document.querySelector( `.toolbox-nav-btn[data-toolbox-id="${toolboxId}"]` ); selectedBtn?.classList.add('active'); saveToStorage('toolbox', { toolboxId }); await loadToolboxContent(toolboxId); } async function loadToolboxContent(toolboxId) { const contentContainer = document.querySelector('.toolbox-content-container'); // Показываем индикатор загрузки contentContainer.innerHTML = `
Загрузка...

Загрузка содержимого склада

Пожалуйста, подождите...

`; try { const resp = await apiRequest(`/stocks/`, { toolboxId }); if (resp.status === 'ok') { const toolboxData = resp.data; const toolboxInfo = currentToolboxData.find(t => t.id === toolboxId); const toolboxOwn = toolboxInfo.owner_id === userData.id ? 'Мой склад' : toolboxInfo.owner_id ? 'Склад сотрудника' : 'Общий склад'; function handleEditBtn() { if (accessData.manage_toolboxes) { contentContainer.querySelector('#editToolbox').addEventListener('click', () => { addToolbox(toolboxInfo); }); } else { contentContainer.querySelector('#editToolbox').remove(); } } function handleFillBtn() { if (accessData.tools_registration && !toolboxInfo.owner_id) { contentContainer.querySelector('#fillToolbox').addEventListener('click', async () => { await fillToolbox(toolboxInfo); }); } else { contentContainer.querySelector('#fillToolbox').remove(); } } if (toolboxData.length === 0) { contentContainer.innerHTML = `
${toolboxInfo?.title || 'Склад'}

${toolboxInfo?.description || 'Описание отсутствует'}

${toolboxOwn}

Склад пуст

В этом складе нет инструментов

`; if (!toolboxInfo.owner_id && accessData.manage_toolboxes) { document.getElementById('deleteToolbox').addEventListener('click', function (e) { e.preventDefault(); deleteToolbox(toolboxId); }); } else { document.getElementById('deleteToolbox').remove(); } handleEditBtn(); handleFillBtn() return; } // Находим информацию о выбранном складе const quantityMonitoring = toolboxInfo.monitoring && accessData.view_all_toolboxes; // Обрабатываем данные в единый список const processedData = processToolboxData(toolboxData, toolboxId, quantityMonitoring); const totalQuantity = processedData.reduce((sum, item) => sum + item.totalQuantity, 0); const totalCost = formatPrice(processedData.reduce((sum, item) => sum + item.totalCost, 0)); const notEnough = processedData.reduce((sum, item) => sum + (item.indicator?.text !== 'Достаточно' ? 1 : 0), 0); // console.log(processedData); // Отображаем содержимое склада contentContainer.innerHTML = `
${toolboxInfo?.title || 'Склад'}

${toolboxInfo?.description || 'Описание отсутствует'}

${toolboxOwn}
${notEnough > 0 && quantityMonitoring ? `
` : ''}
Количество позиций: ${processedData.length} Количество инструментов: ${totalQuantity} Общая стоимость: ${totalCost}
${quantityMonitoring ? '' : ''} ${toolboxOwn === 'Общий склад' ? `` : ''}
Название Категория Количество Статус Стоимость Расположение Последнее изменение
`; handleEditBtn(); handleFillBtn() // Инициализация таблицы с данными await initializeToolboxTable(processedData, toolboxOwn, quantityMonitoring); } else { throw new Error(resp.message || 'Ошибка загрузки данных склада'); } } catch (error) { console.error('Error loading toolbox content:', error); contentContainer.innerHTML = `

Ошибка загрузки

Не удалось загрузить содержимое склада
${error.message}

`; // Добавляем обработчик для кнопки "Попробовать снова" contentContainer.querySelector('#tryAgainBtn').addEventListener('click', async () => await selectToolbox(toolboxId)); } } async function fillToolbox(toolboxInfo) { const allToolkitsData = await apiRequest('/toolkit/fill_prepare'); if (allToolkitsData.status !== 'ok') { showInfo('Ошибка загрузки данных инструментов', 'error'); return; } // Удаляем старое модальное окно let modal = document.getElementById('fillToolboxModal'); if (modal) modal.remove(); // ============================== // Подготовка данных // ============================== const { toolkits, categories, placements } = allToolkitsData.data; const placementMap = {}; placements .filter(p => p.toolbox_id === toolboxInfo.id) .forEach(p => placementMap[p.toolkit_id] = p.placement); // lower -> category const categoriesByLower = Object.fromEntries(categories.map(c => [c.title.toLowerCase().trim(), c])); const toolkitsMap = Object.fromEntries(toolkits.map(t => [t.id, t])); const toolkitsByCategory = {}; toolkits.forEach(t => { if (!toolkitsByCategory[t.category_id]) toolkitsByCategory[t.category_id] = []; toolkitsByCategory[t.category_id].push(t); }); // lower title -> toolkit within category const toolkitsLowerByCategory = {}; Object.entries(toolkitsByCategory).forEach(([catId, list]) => { const map = {}; list.forEach(t => map[t.title.toLowerCase().trim()] = t); toolkitsLowerByCategory[catId] = map; }); // ============================== // Вспомогательные функции // ============================== const normalize = s => (s || '').toLowerCase().trim(); // exact match by lower title const findCategoryByExact = title => { if (!title) return null; return categoriesByLower[title.toLowerCase().trim()] || null; }; const findToolkitByExact = (categoryId, title) => { if (!categoryId || !title) return null; const map = toolkitsLowerByCategory[categoryId] || {}; return map[title.toLowerCase().trim()] || null; }; // format cost const formatCost = value => { if (typeof value !== 'number') value = parseFloat(value) || 0; return formatPrice(value); }; // debounce function debounce(fn, delay = 200) { let t; return function (...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), delay); }; } // Inject minimal styles for autocomplete dropdown (only once) (function injectStyles() { if (document.getElementById('fillToolboxAutocompleteStyles')) return; const style = document.createElement('style'); style.id = 'fillToolboxAutocompleteStyles'; style.textContent = ` .ft-autocomplete { position: relative; } .ft-autocomplete-list { position: absolute; z-index: 2000; left: 0; right: 0; max-height: 220px; overflow: auto; background: white; border: 1px solid #dee2e6; border-radius: 0.25rem; box-shadow: 0 .25rem .5rem rgba(0,0,0,.08); } .ft-autocomplete-item { padding: .375rem .5rem; cursor: pointer; } .ft-autocomplete-item:hover, .ft-autocomplete-item.active { background: #7abb92ff; } .ft-autocomplete-empty { padding: .375rem .5rem; color: #6c757d; } `; document.head.appendChild(style); })(); // create suggestions list element for an input (returns container and helper functions) function createAutocomplete(forInput) { // wrapper ft-autocomplete should exist around input let wrapper = forInput.closest('.ft-autocomplete'); if (!wrapper) { wrapper = document.createElement('div'); wrapper.className = 'ft-autocomplete'; forInput.parentNode.insertBefore(wrapper, forInput); wrapper.appendChild(forInput); } let list = wrapper.querySelector('.ft-autocomplete-list'); if (!list) { list = document.createElement('div'); list.className = 'ft-autocomplete-list'; list.style.display = 'none'; wrapper.appendChild(list); } function show(items) { list.innerHTML = ''; if (!items || items.length === 0) { const empty = document.createElement('div'); empty.className = 'ft-autocomplete-empty'; empty.textContent = 'Ничего не найдено'; list.appendChild(empty); list.style.display = 'block'; return; } items.forEach(it => { const div = document.createElement('div'); div.className = 'ft-autocomplete-item'; div.textContent = it.title; div.dataset.valueId = it.id; div.addEventListener('mousedown', (e) => { // mousedown чтобы сработало до blur e.preventDefault(); if (typeof wrapper._onSelect === 'function') wrapper._onSelect(it); hide(); }); list.appendChild(div); }); list.style.display = 'block'; } function hide() { list.innerHTML = ''; list.style.display = 'none'; } function onSelect(fn) { wrapper._onSelect = fn; } return { wrapper, list, show, hide, onSelect }; } // ============================== // Создание строки таблицы // ============================== function createRow(rowIndex = 0) { const rowId = `row-${Date.now()}-${rowIndex}`; return `
`; } // ============================== // Создаём модалку // ============================== modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'fillToolboxModal'; modal.tabIndex = -1; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` `; document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); const rowsContainer = modal.querySelector('#fillToolboxRows'); rowsContainer.innerHTML = createRow(0); // ============================== // Автокомплит + логика строки // ============================== // Закрытие всех автокомплитов при клике вне document.addEventListener('click', (e) => { const openLists = document.querySelectorAll('.ft-autocomplete-list'); openLists.forEach(l => { if (!l.parentElement.contains(e.target)) l.style.display = 'none'; }); }); // Список для хранения ссылок на автокомплиты const autocompleteInstances = new Map(); function setupRow(row) { const rowId = row.id; const categoryInput = row.querySelector('.category-input'); const categoryIdInput = row.querySelector('.category-id'); const toolkitInput = row.querySelector('.toolkit-input'); const toolkitIdInput = row.querySelector('.toolkit-id'); const qty = row.querySelector('.quantity'); const price = row.querySelector('.price'); const placement = row.querySelector('.placement'); const costInput = row.querySelector('.cost'); // create autocompletes const catAC = createAutocomplete(categoryInput); const toolAC = createAutocomplete(toolkitInput); // Сохраняем ссылки для возможной очистки autocompleteInstances.set(rowId, { catAC, toolAC }); // CATEGORY behavior const showCategorySuggestions = debounce(() => { const q = normalize(categoryInput.value); let matches; if (!q) { // Если поле пустое - показываем все категории matches = categories .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); } else { // Если есть текст - фильтруем по вхождению matches = categories .filter(c => normalize(c.title).includes(q)) .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); } catAC.show(matches); }, 200); catAC.onSelect((cat) => { categoryIdInput.value = cat.id; categoryInput.value = cat.title; categoryInput.classList.remove('is-invalid'); categoryInput.classList.add('is-valid'); toolkitInput.disabled = false; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; // Фокус на поле инструмента после выбора категории setTimeout(() => toolkitInput.focus(), 10); }); categoryInput.addEventListener('focus', function () { // При фокусе на поле показываем все категории, если поле пустое if (!categoryInput.value.trim()) { showCategorySuggestions(); } }); categoryInput.addEventListener('input', function () { categoryIdInput.value = ''; categoryInput.classList.remove('is-valid', 'is-invalid'); toolkitInput.disabled = true; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; showCategorySuggestions(); }); categoryInput.addEventListener('blur', function () { // Небольшая задержка для обработки клика по автокомплиту setTimeout(() => { const v = categoryInput.value.trim(); if (!v) return; const matched = findCategoryByExact(v); if (matched) { categoryIdInput.value = matched.id; categoryInput.value = matched.title; categoryInput.classList.remove('is-invalid'); categoryInput.classList.add('is-valid'); toolkitInput.disabled = false; toolkitInput.focus(); } else { categoryInput.classList.add('is-invalid'); categoryIdInput.value = ''; toolkitInput.disabled = true; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; } }, 150); }); // TOOLKIT behavior const showToolkitSuggestions = debounce(() => { const catId = categoryIdInput.value; if (!catId) { toolAC.hide(); return; } const q = normalize(toolkitInput.value); const pool = toolkitsByCategory[catId] || []; let matches; if (!q) { // Если поле пустое - показываем все инструменты в категории matches = pool .sort((a, b) => a.title.localeCompare(b.title)) .map(t => ({ id: t.id, title: t.title })); } else { // Если есть текст - фильтруем по вхождению matches = pool .filter(t => normalize(t.title).includes(q)) .sort((a, b) => a.title.localeCompare(b.title)) .map(t => ({ id: t.id, title: t.title })); } toolAC.show(matches); }, 200); toolAC.onSelect((tool) => { toolkitIdInput.value = tool.id; toolkitInput.value = tool.title; toolkitInput.classList.remove('is-invalid'); toolkitInput.classList.add('is-valid'); qty.disabled = false; price.disabled = false; placement.disabled = false; placement.value = placementMap[tool.id] || ''; // Фокус на поле количества после выбора инструмента setTimeout(() => qty.focus(), 10); }); toolkitInput.addEventListener('focus', function () { // При фокусе на поле показываем все инструменты в категории, если поле пустое const catId = categoryIdInput.value; if (catId && !toolkitInput.value.trim()) { showToolkitSuggestions(); } }); toolkitInput.addEventListener('input', function () { toolkitIdInput.value = ''; toolkitInput.classList.remove('is-valid', 'is-invalid'); qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; showToolkitSuggestions(); }); toolkitInput.addEventListener('blur', function () { setTimeout(() => { const v = toolkitInput.value.trim(); const catId = categoryIdInput.value; if (!v || !catId) return; const matched = findToolkitByExact(catId, v); if (matched) { toolkitIdInput.value = matched.id; toolkitInput.value = matched.title; toolkitInput.classList.remove('is-invalid'); toolkitInput.classList.add('is-valid'); qty.disabled = false; price.disabled = false; placement.disabled = false; placement.value = placementMap[matched.id] || ''; qty.focus(); } else { toolkitInput.classList.add('is-invalid'); toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; } }, 150); }); // recalc cost function recalc() { const q = parseFloat(qty.value) || 0; const p = parseFloat(price.value.toString().replace(',', '.')) || 0; const c = q * p; costInput.value = formatCost(c) + ' ₽'; updateTotals(); } qty.addEventListener('input', recalc); price.addEventListener('input', recalc); // Фокус на следующее поле при нажатии Enter const handleEnterKey = (currentField, nextField) => { if (nextField && !nextField.disabled) { nextField.focus(); if (nextField === toolkitInput && !nextField.value.trim()) { // Если переходим к пустому полю инструмента, показываем все варианты showToolkitSuggestions(); } } }; categoryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(categoryInput, toolkitInput); } }); toolkitInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(toolkitInput, qty); } }); qty.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(qty, price); } }); price.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(price, placement); } }); placement.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); // При Enter в последнем поле - добавить новую строку modal.querySelector('#addRowBtn').click(); } }); // remove row row.querySelector('.remove-row').addEventListener('click', () => { if (rowsContainer.children.length <= 1) return; // Очищаем автокомплиты const instances = autocompleteInstances.get(row.id); if (instances) { instances.catAC.hide(); instances.toolAC.hide(); autocompleteInstances.delete(row.id); } row.remove(); updateTotals(); // Фокус на первую строку после удаления const firstRow = rowsContainer.querySelector('tr'); if (firstRow) { const firstInput = firstRow.querySelector('.category-input'); if (firstInput) firstInput.focus(); } // Отключаем кнопку удаления, если осталась одна строка if (rowsContainer.children.length === 1) { const btn = rowsContainer.querySelector('.remove-row'); if (btn) btn.disabled = true; } }); } // Инициализируем только первую строку const firstRow = rowsContainer.querySelector('tr'); if (firstRow) setupRow(firstRow); // add row button modal.querySelector('#addRowBtn').addEventListener('click', () => { const newRowHTML = createRow(rowsContainer.children.length); rowsContainer.insertAdjacentHTML('beforeend', newRowHTML); // Настройка новой строки const newRow = rowsContainer.lastElementChild; setupRow(newRow); // Включаем кнопки удаления const removeButtons = rowsContainer.querySelectorAll('.remove-row'); if (removeButtons.length > 1) removeButtons.forEach(b => b.disabled = false); // Фокус на поле категории в новой строке setTimeout(() => { const categoryInput = newRow.querySelector('.category-input'); if (categoryInput) categoryInput.focus(); }, 10); }); // totals function updateTotals() { let totalQty = 0; let totalCost = 0; let filled = 0; rowsContainer.querySelectorAll('tr').forEach(r => { const qty = parseInt(r.querySelector('.quantity')?.value) || 0; const cost = parseFloat((r.querySelector('.cost')?.value || '').replace(/[^0-9.,]/g, '').replace(',', '.')) || 0; if (qty && cost) { totalQty += qty; totalCost += cost; filled++; } }); modal.querySelector('#totalQuantity').textContent = totalQty; modal.querySelector('#totalCost').textContent = formatCost(totalCost) + ' ₽'; modal.querySelector('#totalRowsCount').textContent = filled + ' позиций'; } // form submit with validation + double confirmation like у тебя modal.querySelector('#fillToolboxForm').addEventListener('submit', async function (e) { e.preventDefault(); const reasonInput = modal.querySelector('#fillReason'); const reason = reasonInput.value.trim(); if (reason.length < 10) { showInfo('Укажите обоснование (минимум 10 символов)', 'error'); return; } const rows = rowsContainer.querySelectorAll('tr'); const items = []; let isValid = true; let errorMessage = ''; rows.forEach((row, index) => { if (!isValid) return; const catId = row.querySelector('.category-id').value; const toolId = row.querySelector('.toolkit-id').value; const qty = row.querySelector('.quantity').value; const price = row.querySelector('.price').value; // Strict: category and toolkit must match exact list entries if (!catId) { isValid = false; errorMessage = `Выберите категорию из списка в строке ${index + 1}`; row.querySelector('.category-input').classList.add('is-invalid'); row.querySelector('.category-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (!toolId) { isValid = false; errorMessage = `Выберите инструмент из списка в строке ${index + 1}`; row.querySelector('.toolkit-input').classList.add('is-invalid'); row.querySelector('.toolkit-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (!qty || !price) { isValid = false; errorMessage = `Заполните все обязательные поля в строке ${index + 1}`; if (!qty) row.querySelector('.quantity').classList.add('is-invalid'); if (!price) row.querySelector('.price').classList.add('is-invalid'); row.querySelector('.quantity').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } items.push({ toolkit_id: parseInt(toolId), quantity: parseInt(qty), price: parseFloat(price.toString().replace(',', '.')), placement: row.querySelector('.placement').value || null }); }); if (!isValid) { document.getElementById('fillToolboxErrorMessage').textContent = errorMessage; document.getElementById('fillToolboxError').classList.remove('d-none'); document.getElementById('fillToolboxError').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (items.length === 0) { document.getElementById('fillToolboxErrorMessage').textContent = 'Добавьте хотя бы одну позицию'; document.getElementById('fillToolboxError').classList.remove('d-none'); return; } const submitBtn = modal.querySelector('#submitFillBtn'); const spinner = modal.querySelector('#submitFillSpinner'); const submitText = modal.querySelector('#submitFillText'); const originalText = submitText.textContent; submitText.textContent = 'Нажмите еще раз для подтверждения (10 сек)'; submitBtn.disabled = true; let confirmed = false; const timeout = setTimeout(() => { if (!confirmed) { submitText.textContent = originalText; submitBtn.disabled = false; document.getElementById('fillToolboxError').classList.add('d-none'); } }, 10000); const confirmHandler = async function (ev) { ev.preventDefault(); confirmed = true; clearTimeout(timeout); submitBtn.disabled = true; spinner.style.display = 'inline-block'; try { const response = await apiRequest('/toolbox/fill', { toolboxId: toolboxInfo.id, items: items, reason: reason, userId: userData.id }, 'POST'); if (response.status === 'ok') { showInfo('Склад успешно пополнен', 'success'); bsModal.hide(); await loadToolboxContent(toolboxInfo.id); } else { throw new Error(response.message || 'Ошибка при пополнении склада'); } } catch (error) { console.error('Ошибка при пополнении склада:', error); document.getElementById('fillToolboxErrorMessage').textContent = error.message || 'Произошла ошибка при пополнении склада'; document.getElementById('fillToolboxError').classList.remove('d-none'); submitBtn.disabled = false; spinner.style.display = 'none'; submitText.textContent = originalText; } submitBtn.removeEventListener('click', confirmHandler); }; submitBtn.addEventListener('click', confirmHandler, { once: true }); submitBtn.disabled = false; }); // cleanup on modal hide modal.addEventListener('hidden.bs.modal', () => { autocompleteInstances.clear(); setTimeout(() => { if (modal.parentNode) modal.remove(); }, 300); }); // Простой таймер для восстановления фокуса let focusRestoreTimer = null; function restoreFocusIfLost() { if (modal && bsModal._isShown) { const activeElement = document.activeElement; const isFocusInModal = modal.contains(activeElement); if (!isFocusInModal && activeElement !== document.body) { // Фокус вне модалки - не восстанавливаем return; } if (!isFocusInModal || activeElement === document.body) { // Фокус потерян или на body - восстанавливаем const firstInput = rowsContainer.querySelector('.category-input'); if (firstInput && firstInput.offsetParent !== null) { firstInput.focus(); } } } } // Показываем модалку bsModal.show(); // Устанавливаем фокус после полного отображения модалки modal.addEventListener('shown.bs.modal', () => { setTimeout(() => { const firstCategoryInput = rowsContainer.querySelector('.category-input'); if (firstCategoryInput) { firstCategoryInput.focus(); // Показываем все категории при первом фокусе const firstRow = rowsContainer.querySelector('tr'); if (firstRow) { const catAC = autocompleteInstances.get(firstRow.id)?.catAC; if (catAC) { const matches = categories .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); catAC.show(matches); } } } // Запускаем периодическую проверку фокуса focusRestoreTimer = setInterval(restoreFocusIfLost, 1000); }, 100); }); // Останавливаем таймер при закрытии модалки modal.addEventListener('hidden.bs.modal', () => { if (focusRestoreTimer) { clearInterval(focusRestoreTimer); focusRestoreTimer = null; } }); } 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 = ` `; 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; // Создаем мапы для быстрого доступа const toolkitMap = {}; const categoryMap = {}; toolkits.forEach(toolkit => { toolkitMap[toolkit.id] = toolkit; }); categories.forEach(category => { categoryMap[category.id] = category; }); // Группируем стоки по инструментам const groupedStocks = {}; stocks.forEach(stock => { if (stock.toolbox_id !== toolboxId) return; const toolkitId = stock.toolkit_id; if (!groupedStocks[toolkitId]) { groupedStocks[toolkitId] = { stocks: [], placements: new Set() }; } groupedStocks[toolkitId].stocks.push(stock); if (stock.placement) { groupedStocks[toolkitId].placements.add(stock.placement); } }); // Формируем итоговый массив const result = []; Object.keys(groupedStocks).forEach(toolkitId => { const toolkit = toolkitMap[toolkitId]; if (!toolkit) return; const group = groupedStocks[toolkitId]; const category = categoryMap[toolkit.category_id]; // Рассчитываем общие показатели const totalQuantity = group.stocks.reduce((sum, stock) => sum + stock.quantity, 0); const totalCost = group.stocks.reduce((sum, stock) => sum + (stock.quantity * stock.price), 0); // Определяем статус достаточности let indicator = null; if (quantityMonitoring) { if (totalQuantity >= toolkit.quantity_min) { indicator = { text: 'Достаточно', class: 'success' }; } else if (totalQuantity >= toolkit.quantity_min_extra) { indicator = { text: 'Мало', class: 'warning' }; } else { indicator = { text: 'Критически мало', class: 'danger' }; } } // Формируем расположение let placement = group.placements.size > 0 ? Array.from(group.placements).join(', ') : 'Своб. расположение'; // Находим дату последнего изменения const lastUpdated = group.stocks.reduce((latest, stock) => { const stockDate = new Date(stock.updated_at); return stockDate > latest ? stockDate : latest; }, new Date(0)); result.push({ id: parseInt(toolkitId), toolboxId: toolboxId, image: toolkit.image?.main || 'static/images/tools/default.png', images: toolkit.image?.additional || [], title: toolkit.title, category: category?.title || 'Без категории', totalQuantity: totalQuantity, indicator: indicator, totalCost: totalCost, // Сохраняем число, форматируем при выводе placement: placement, lastUpdated: lastUpdated.toLocaleString('ru-RU'), available: totalQuantity, // для проверки при операциях toolkitData: toolkit, // для модального окна categoryData: category // для модального окна }); }); return result; } // Функция форматирования стоимости с разделителями тысяч function formatPrice(price) { return parseFloat(price).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); } // Функция инициализации таблицы async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { let currentPage = 1; const itemsPerPage = 20; let currentSort = { field: 'title', direction: 'asc' }; let filteredData = [...data]; // Инициализация пагинации async function initializePagination() { const totalPages = Math.ceil(filteredData.length / itemsPerPage); const paginationContainer = document.getElementById('toolboxPagination'); // Очищаем текущее содержимое paginationContainer.innerHTML = ''; // Добавляем кнопки пагинации const prevBtn = document.createElement('li'); prevBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; prevBtn.innerHTML = `Назад`; prevBtn.addEventListener('click', (e) => { e.preventDefault(); if (currentPage > 1) { currentPage--; renderTable(); } }); paginationContainer.appendChild(prevBtn); // Определяем диапазон отображаемых страниц let startPage = Math.max(1, currentPage - 2); let endPage = Math.min(totalPages, currentPage + 2); for (let i = startPage; i <= endPage; i++) { const pageItem = document.createElement('li'); pageItem.className = `page-item ${i === currentPage ? 'active' : ''}`; pageItem.innerHTML = `${i}`; pageItem.addEventListener('click', (e) => { e.preventDefault(); currentPage = i; renderTable(); }); paginationContainer.appendChild(pageItem); } const nextBtn = document.createElement('li'); nextBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; nextBtn.innerHTML = `Вперед`; nextBtn.addEventListener('click', (e) => { e.preventDefault(); if (currentPage < totalPages) { currentPage++; renderTable(); } }); paginationContainer.appendChild(nextBtn); // Рендерим данные текущей страницы await renderTable(); } // Функция рендеринга таблицы async function renderTable() { const tbody = document.getElementById('toolboxItemsBody'); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const pageData = filteredData.slice(startIndex, endIndex); tbody.innerHTML = ''; pageData.forEach(item => { const tr = document.createElement('tr'); tr.dataset.id = item.id; tr.dataset.quantity = item.totalQuantity; // Определяем, какие кнопки показывать let actionButtons = ''; if (toolboxOwn === 'Мой склад' || toolboxOwn === 'Склад сотрудника') { actionButtons = `
`; } else if (toolboxOwn === 'Общий склад' && accessData.available_own_toolbox) { actionButtons = `
`; } tr.innerHTML = ` ${item.title} ${item.title}
${actionButtons} ${item.category} ${item.totalQuantity} ${quantityMonitoring ? `${item.indicator?.text || '-'}` : ''} ${formatPrice(item.totalCost)} ₽ ${toolboxOwn === 'Общий склад' ? `${item.placement}` : ''} ${item.lastUpdated} `; tbody.appendChild(tr); // Добавляем обработчики для кнопок в строке const actionBtn = tr.querySelector('.action-buttons'); if (actionBtn) { actionBtn.querySelectorAll('button[data-action]').forEach(button => { button.addEventListener('click', async (e) => { e.stopPropagation(); const action = e.currentTarget.dataset.action; const itemId = e.currentTarget.dataset.id; const selectedItem = data.find(d => d.id == itemId); if (selectedItem) { selectedItem.skipRefresh = true; await showOperationModal(action, selectedItem); } }); }); } }); // Добавляем обработчики для изображений let activeModal = null; document.querySelectorAll('.toolkit-image-link').forEach(link => { link.addEventListener('click', async (e) => { if (activeModal) return; e.preventDefault(); const itemId = e.currentTarget.dataset.id; activeModal = true; await showToolkitDetailsModal(itemId); activeModal = null; }); }); } function parseDate(d) { // d = "07.12.2025, 13:19:20" const [datePart, timePart] = d.split(', '); const [day, month, year] = datePart.split('.').map(Number); const [hour, minute, second] = timePart.split(':').map(Number); return new Date(year, month - 1, day, hour, minute, second); } // Функция сортировки function sortData(field, direction) { filteredData.sort((a, b) => { let aValue = a[field]; let bValue = b[field]; // Для числовых полей if (field === 'totalQuantity') { aValue = parseFloat(aValue); bValue = parseFloat(bValue); } // Для стоимости if (field === 'totalCost') { aValue = parseFloat(a.totalCost); bValue = parseFloat(b.totalCost); } // Для дат if (field === 'lastUpdated') { aValue = parseDate(a.lastUpdated); bValue = parseDate(b.lastUpdated); } // Для статуса if (field === 'indicator') { const order = { 'danger': 0, 'warning': 1, 'success': 2 }; aValue = order[a.indicator?.class] || 3; bValue = order[b.indicator?.class] || 3; } if (direction === 'asc') { return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; } else { return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; } }); } // Функция фильтрации async function filterData(searchText) { if (!searchText.trim()) { filteredData = [...data]; } else { const searchLower = searchText.toLowerCase(); filteredData = data.filter(item => item.title.toLowerCase().includes(searchLower) || item.toolkitData.description.toLowerCase().includes(searchLower) ); } currentPage = 1; sortData(currentSort.field, currentSort.direction); await initializePagination(); } async function filterIndicator() { const searchLower = 'мало'; filteredData = data.filter(item => (item.indicator?.text && item.indicator.text.toLowerCase().includes(searchLower)) ); currentPage = 1; sortData(currentSort.field, currentSort.direction); await initializePagination(); } // Инициализация сортировки по заголовкам document.querySelectorAll('#toolboxItemsTable th[data-sort]').forEach(th => { th.addEventListener('click', async () => { const field = th.dataset.sort; if (currentSort.field === field) { currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; } else { currentSort.field = field; currentSort.direction = 'asc'; } // Обновляем иконки сортировки document.querySelectorAll('#toolboxItemsTable th i').forEach(icon => { icon.className = 'bi bi-arrow-down-up'; }); const currentIcon = th.querySelector('i'); currentIcon.className = currentSort.direction === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'; sortData(currentSort.field, currentSort.direction); await initializePagination(); }); }); // Инициализация поиска const searchInput = document.getElementById('toolboxSearch'); searchInput.addEventListener('input', async (e) => { await filterData(e.target.value); }); // Инициализация кнопки сброса фильтра document.getElementById('resetFilter').addEventListener('click', async () => { searchInput.value = ''; searchInput.placeholder = 'Поиск по всем полям...'; await filterData(''); }); // Инициализация кнопки "мало" try { document.getElementById('notEnoughBtn').addEventListener('click', async () => { const showAll = 'Сбросить фильтр -->'; if (searchInput.placeholder === showAll) { searchInput.placeholder = 'Поиск по всем полям...'; searchInput.value = ''; await filterData(''); return; } searchInput.value = ''; searchInput.placeholder = showAll; await filterIndicator(); }); } catch (_) { } // Начальная инициализация sortData(currentSort.field, currentSort.direction); await initializePagination(); } async function getToolkitStocks(toolkitId) { const userId = userData.id; const allToolboxes = accessData.view_all_toolboxes; const resp = await apiRequest('/toolkit/', { toolkitId, userId, allToolboxes }); return resp.data; } // Функция показа модального окна с деталями инструмента async function showToolkitDetailsModal(toolkitId) { const modalId = 'toolkitDetailsModal'; let modal = document.getElementById(modalId); if (modal) { modal.remove(); } modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = modalId; modal.tabIndex = -1; let toolkitData = {}; const toolkiResponse = await apiRequest('/toolkit/', { toolkitId }, 'GET'); if (toolkiResponse.status === 'ok') { toolkitData = toolkiResponse.data; } else { showInfo('Произошла ошибка', 'error'); return; } let categories = {}; try { const categoriesResponse = await apiRequest('/toolkit/categories', {}, 'GET'); if (categoriesResponse.status === 'ok') { categories = categoriesResponse.data; } } catch (error) { console.error('Ошибка загрузки категорий:', error); } const categoryData = categories[toolkitData.category_id]; const images = toolkitData.image ? [toolkitData.image.main, ...(toolkitData.image.additional || [])] : [toolkitData.image?.main || ''];; let imagesDiv = ''; if (images.length > 1) { const carouselId = `carousel-${toolkitData.id}`; imagesDiv = `
`; } else { imagesDiv = images[0] ? `
${toolkitData.title}
` : '
'; } // Переменная для хранения данных об остатках (будет загружена при раскрытии аккордеона) let toolkitStocksData = null; let isStocksLoading = false; modal.innerHTML = ` `; 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]) => ` `).join('')}
Склад Количество Расположение
${key} ${value.count} шт. ${value.placement || ''} ${!toolkitData.hidden && value.id && accessData.available_own_toolbox ? ` ` : ''}
`; } 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 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); }); } // Функция показа модального окна для операций async function showOperationModal(operation, selectedItem) { const modalId = 'operationModal'; let modal = document.getElementById(modalId); if (modal) { modal.remove(); } modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = modalId; modal.tabIndex = -1; const operationTitles = { 'return': 'Возврат инструмента', 'writeoff': 'Списание инструмента', 'get': 'Получение инструмента' }; // Определяем максимальное доступное количество modal.innerHTML = ` `; document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); bsModal.show(); // Валидация ввода количества const quantityInput = document.getElementById('operationQuantity'); quantityInput.addEventListener('change', function () { let value = parseInt(this.value); if (value > selectedItem.totalQuantity) { this.value = selectedItem.totalQuantity; } else if (value < 1) { this.value = 1; } }); document.getElementById('confirmOperation').addEventListener('click', async (e) => { const btn = e.currentTarget; const btnText = btn.innerHTML; // Блокируем кнопку + ставим спиннер btn.disabled = true; btn.innerHTML = ` Обработка... `; const quantity = parseInt(document.getElementById('operationQuantity').value); const comment = document.getElementById('operationComment').value; // Проверка максимального количества для операций списания и получения if ((operation === 'writeoff' || operation === 'get') && quantity > selectedItem.totalQuantity) { showError(`Максимально доступное количество: ${selectedItem.totalQuantity}`); resetButton(); return; } if (comment === '') { showError('Введите обоснование'); resetButton(); return; } const success = await actionRequest(operation, quantity, comment, selectedItem); if (success) { bsModal.hide(); showInfo(`Запрос на ${operationTitles[operation]} успешно создан`, 'success'); if (!selectedItem.skipRefresh) { await loadToolboxContent(selectedItem.toolboxId); } } else { showError('Ошибка выполнения операции'); resetButton(); } function resetButton() { btn.disabled = false; btn.innerHTML = btnText; } function showError(message) { document.getElementById('operationError').classList.remove('d-none'); document.getElementById('operationErrorMessage').textContent = message; } }); modal.addEventListener('hidden.bs.modal', () => { modal.remove(); }); } async function actionRequest(operation, quantity, comment, selectedItem) { const action = { operation, quantity, comment, selectedItem }; const sendData = { userData, accessData, action }; const resp = await apiRequest('/stocks/action', sendData); if (resp.status == 'ok') { return true } else { return false } } function renderRequestsTab(tabId, tabData) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const { requests, users, toolboxes, toolkits } = tabData; // Собираем списки для фильтров const initUsers = [...new Set(requests.map(r => r.init_user_id))]; const userMap = {}; users.forEach(user => { userMap[user.id] = user.username; }); const actionTypes = [...new Set(requests.map(r => r.action))]; const ownRequestsCount = requests.filter(r => r.init_user_id === userData.id).length; // Создаем мапу для toolboxes const toolboxMap = {}; toolboxes.forEach(box => { toolboxMap[box.id] = box.title; }); // Создаем мапу для toolkits const toolkitMap = {}; toolkits.forEach(kit => { toolkitMap[kit.id] = kit.title; }); // Фильтры let currentFilters = { user: 'all', action: 'all', }; // Рендерим дополнительный контейнер с фильтрами tabOptionalContent.innerHTML = `
${(accessData.refund_request_confirm || accessData.debit_request_confirm) ? `
` : ''} ${ownRequestsCount > 0 ? ` ` : ''}
`; // Рендерим основной контейнер с таблицей запросов tabContent.innerHTML = `
Тип Оформил Со склада На склад Инструмент Кол-во Обоснование Действия
`; // Функция для фильтрации запросов function filterRequests() { let filtered = requests; // Фильтр по пользователю if (currentFilters.user !== 'all') { filtered = filtered.filter(r => r.init_user_id == currentFilters.user); } // Фильтр по типу действия if (currentFilters.action !== 'all') { filtered = filtered.filter(r => r.action === currentFilters.action); } return filtered; } // Функция для рендеринга строк таблицы function renderRequestsTable() { const tbody = document.getElementById(`${tabId}-requests-body`); const noRequestsDiv = document.getElementById(`${tabId}-no-requests`); const filteredRequests = filterRequests(); if (filteredRequests.length === 0) { tbody.innerHTML = ''; noRequestsDiv.style.display = 'block'; return; } noRequestsDiv.style.display = 'none'; tbody.innerHTML = filteredRequests.map(request => { // Определяем доступные действия const actions = []; // Кнопка отзыва (только для инициатора и неподтвержденных запросов) if (request.init_user_id === userData.id && request.accepted === null) { actions.push(` `); } // Кнопки принятия/отклонения (в зависимости от прав) let canDecide = false; // Проверяем права в зависимости от типа запроса if (request.action === 'Возврат' && accessData.refund_request_confirm) { canDecide = true; } else if (request.action === 'Списание' && accessData.debit_request_confirm) { canDecide = true; } else if (request.action !== 'Возврат' && request.action !== 'Списание' && (accessData.refund_request_confirm || accessData.debit_request_confirm)) { // Для других типов запросов, если есть хотя бы одно из прав console.warning('Unknown request action', request.action); canDecide = true; } if (canDecide) { actions.push(`
`); } // Если нет доступных действий if (actions.length === 0) { actions.push('Нет действий'); } return ` ${request.action} ${request.created_at} ${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`} ${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'} ${request.target_toolbox_id ? (toolboxMap[request.target_toolbox_id] || `Склад ${request.target_toolbox_id}`) : '-'} ${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'} ${request.quantity} ${request.reason || 'Нет обоснования'}
${actions.join('')}
`; }).join(''); } // Функция для показа модального окна подтверждения function showConfirmationModal(title, message, onConfirm) { // Проверяем, есть ли уже модальное окно let modal = document.getElementById('confirmation-modal'); if (!modal) { // Создаем модальное окно modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'confirmation-modal'; modal.tabIndex = -1; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` `; document.body.appendChild(modal); } // Устанавливаем содержимое document.getElementById('confirmation-message').innerHTML = message; // Очищаем предыдущие обработчики const confirmBtn = document.getElementById('confirm-action-btn'); const oldConfirmBtn = confirmBtn.cloneNode(true); confirmBtn.parentNode.replaceChild(oldConfirmBtn, confirmBtn); const newConfirmBtn = document.getElementById('confirm-action-btn'); // Устанавливаем новый обработчик newConfirmBtn.addEventListener('click', function () { const modalInstance = bootstrap.Modal.getInstance(modal); modalInstance.hide(); onConfirm(); }); // Показываем модальное окно const modalInstance = new bootstrap.Modal(modal); modalInstance.show(); } async function sendRequestDecision(requestId, accepted, requestResult = null) { const data = await apiRequest('/records/', { request_id: requestId, user_id: userData.id, accepted: accepted }); if (data.status == 'ok') { const requestIndex = requests.findIndex(r => r.id == requestId); if (requestIndex !== -1) { requests.splice(requestIndex, 1); } // Перерисовываем таблицу renderRequestsTable(); // Показываем уведомление об успехе requestResult = requestResult === null ? (accepted ? 'Принят' : 'Отклонен') : requestResult; showInfo(`Запрос успешно ${requestResult}`, 'success'); } else { const errorMessage = data.message || 'Ошибка сервера'; showInfo(errorMessage, 'error'); throw new Error(errorMessage); } } // Функция для обработки решения по запросу function handleRequestDecision(requestId, accepted) { const action = accepted ? 'принять' : 'отклонить'; showConfirmationModal( `Подтверждение действия`, `Вы уверены, что хотите ${action} этот запрос?`, async () => { await sendRequestDecision(requestId, accepted); } ); } // Функция для отзыва запроса function handleRequestWithdrawal(requestId) { showConfirmationModal( 'Отзыв запроса', 'Вы уверены, что хотите отозвать этот запрос?', async () => { await sendRequestDecision(requestId, false, 'Отозван'); } ); } // Функция для массовых действий function handleBulkAction(actionType) { const filteredRequests = filterRequests(); // Фильтруем только те запросы, с которыми можно совершить действие let applicableRequests = filteredRequests; if (actionType === 'accept' || actionType === 'reject') { // Для принятия/отклонения: только ожидающие решения applicableRequests = filteredRequests.filter(r => r.accepted === null); // Проверяем права для каждого запроса applicableRequests = applicableRequests.filter(r => { if (r.action === 'Возврат') { return accessData.refund_request_confirm; } else if (r.action === 'Списание') { return accessData.debit_request_confirm; } else { return accessData.refund_request_confirm || accessData.debit_request_confirm; } }); } else if (actionType === 'withdraw') { // Для отзыва: только мои и ожидающие решения applicableRequests = filteredRequests.filter(r => r.init_user_id === userData.id && r.accepted === null ); } if (applicableRequests.length === 0) { showInfo('Нет подходящих запросов для этого действия', 'warning'); return; } const actionName = actionType === 'accept' ? 'принять' : actionType === 'reject' ? 'отклонить' : 'отозвать'; showConfirmationModal( 'Массовое действие', `Вы уверены, что хотите ${actionName} все отправленные запросы (${applicableRequests.length})?`, async () => { // Отправляем запросы на сервер const promises = applicableRequests.map(request => sendRequestDecision(request.id, actionType === 'accept') ); await Promise.all(promises); } ); } // Назначаем обработчики событий для фильтров document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () { currentFilters.user = this.value; renderRequestsTable(); }); document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () { currentFilters.action = this.value; renderRequestsTable(); }); // Назначаем обработчики для массовых действий const acceptAllBtn = document.getElementById(`${tabId}-accept-all-btn`); const rejectAllBtn = document.getElementById(`${tabId}-reject-all-btn`); const withdrawAllBtn = document.getElementById(`${tabId}-withdraw-all-btn`); if (acceptAllBtn) { acceptAllBtn.addEventListener('click', () => handleBulkAction('accept')); } if (rejectAllBtn) { rejectAllBtn.addEventListener('click', () => handleBulkAction('reject')); } if (withdrawAllBtn) { withdrawAllBtn.addEventListener('click', () => handleBulkAction('withdraw')); } // Назначаем делегированные обработчики для действий в таблице document.getElementById(`${tabId}-requests-body`).addEventListener('click', function (e) { const target = e.target; // Находим ближайшую кнопку или родительскую кнопку const button = target.closest('.accept-btn, .reject-btn, .withdraw-btn'); if (!button) return; const requestId = button.dataset.requestId; if (button.classList.contains('accept-btn')) { handleRequestDecision(requestId, true); } else if (button.classList.contains('reject-btn')) { handleRequestDecision(requestId, false); } else if (button.classList.contains('withdraw-btn')) { handleRequestWithdrawal(requestId); } }); // Первоначальный рендеринг таблицы renderRequestsTable(); } function renderJurnalToolkitsTab(tabId, tabData) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const { requests, users, toolboxes, toolkits, startDate, endDate } = tabData; if (requests.length === 0) { tabContent.innerHTML = ` `; return; } // Собираем списки для фильтров const initUsers = [...new Set(requests.map(r => r.init_user_id))]; const userMap = {}; users.forEach(user => { userMap[user.id] = user.username; }); const actionTypes = [...new Set(requests.map(r => r.action))]; // Создаем мапу для toolboxes const toolboxMap = {}; toolboxes.forEach(box => { toolboxMap[box.id] = box.title; }); // Создаем мапу для toolkits const toolkitMap = {}; toolkits.forEach(kit => { toolkitMap[kit.id] = kit.title; }); const savedFilters = loadFromStorage(tabId); // Фильтры let currentFilters = { user: savedFilters?.user || 'all', action: savedFilters?.action || 'all', status: savedFilters?.status || 'all' }; // Рендерим дополнительный контейнер с фильтрами tabOptionalContent.innerHTML = `
Дата начала:
Дата окончания:
`; const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`); filterResetBtn.addEventListener('click', () => { currentFilters = { user: 'all', action: 'all', status: 'all' }; document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; saveToStorage(tabId, currentFilters); renderRequestsTable(); }); const startDateInput = document.getElementById(`${tabId}-date-from`); const endDateInput = document.getElementById(`${tabId}-date-to`); startDateInput.value = startDate; endDateInput.value = endDate; const refreshDateBtn = document.getElementById(`${tabId}-date-update-btn`); refreshDateBtn.addEventListener('click', async () => { const newStartDate = startDateInput.value; const newEndDate = endDateInput.value; const newDateRequestData = { tabId: tabId, startDate: newStartDate, endDate: newEndDate }; if (newStartDate && newEndDate) { tabContent.innerHTML = ` `; const newPeriodData = await apiRequest('/', newDateRequestData); if (newPeriodData.status == 'ok') { renderJurnalToolkitsTab(tabId, newPeriodData.data); } } }); // Рендерим основной контейнер с таблицей запросов tabContent.innerHTML = `
Тип Оформил Решил Со склада На склад Инструмент Кол-во Обоснование
`; // Функция для фильтрации запросов function filterRequests() { let filtered = requests; // Фильтр по пользователю if (currentFilters.user !== 'all') { filtered = filtered.filter(r => r.init_user_id == currentFilters.user); document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; } // Фильтр по типу действия if (currentFilters.action !== 'all') { filtered = filtered.filter(r => r.action === currentFilters.action); document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; } // Фильтр по статусу if (currentFilters.status !== 'all') { document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; switch (currentFilters.status) { case 'accepted': filtered = filtered.filter(r => r.accepted === true); break; case 'rejected': filtered = filtered.filter(r => r.accepted === false); break; } } return filtered; } // Функция для рендеринга строк таблицы function renderRequestsTable() { const tbody = document.getElementById(`${tabId}-requests-body`); const noRequestsDiv = document.getElementById(`${tabId}-no-requests`); const filteredRequests = filterRequests(); if (filteredRequests.length === 0) { tbody.innerHTML = ''; noRequestsDiv.style.display = 'block'; return; } noRequestsDiv.style.display = 'none'; tbody.innerHTML = filteredRequests.map(request => { // Определяем статус запроса let statusBadge = ''; if (request.accepted === true) { statusBadge = 'Принято'; } else { statusBadge = 'Отклонено'; } return ` ${request.action} ${statusBadge} ${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}
${request.created_at} ${userMap[request.decision_user_id] || `Пользователь ${request.decision_user_id}`}
${request.decided_at} ${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'} ${request.target_toolbox_id ? (toolboxMap[request.target_toolbox_id] || `Склад ${request.target_toolbox_id}`) : '-'} ${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'} ${request.quantity} ${request.reason || 'Нет обоснования'} `; }).join(''); } // Назначаем обработчики событий для фильтров document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () { currentFilters.user = this.value; saveToStorage(tabId, currentFilters); renderRequestsTable(); }); document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () { currentFilters.action = this.value; saveToStorage(tabId, currentFilters); renderRequestsTable(); }); document.getElementById(`${tabId}-status-filter`).addEventListener('change', function () { currentFilters.status = this.value; saveToStorage(tabId, currentFilters); renderRequestsTable(); }); // Первоначальный рендеринг таблицы renderRequestsTable(); } function renderJurnalServicesTab(tabId, tabData) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const { requests, users, categories, startDate, endDate } = tabData; if (requests.length === 0) { tabContent.innerHTML = ` `; return; } // Собираем списки для фильтров const initUsers = [...new Set(requests.map(r => r.user_id))].filter(id => id !== null); const userMap = {}; users.forEach(user => { userMap[user.id] = user.username; }); const categoriesMap = {}; categories.forEach(cat => { categoriesMap[cat.id] = { title: cat.title, description: cat.description }; }); // Собираем типы действий (ключи из details) const actionTypes = [...new Set(requests.map(r => { const details = r.details; return Object.keys(details)[0]; // Берем первый ключ как тип действия }))]; const savedFilters = loadFromStorage(tabId); // Фильтры let currentFilters = { user: savedFilters?.user || 'all', action: savedFilters?.action || 'all' }; // Рендерим дополнительный контейнер с фильтрами tabOptionalContent.innerHTML = `
Дата начала:
Дата окончания:
`; const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`); filterResetBtn.addEventListener('click', () => { currentFilters = { user: 'all', action: 'all' }; document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; saveToStorage(tabId, currentFilters); renderServicesTable(); }); const startDateInput = document.getElementById(`${tabId}-date-from`); const endDateInput = document.getElementById(`${tabId}-date-to`); startDateInput.value = startDate; endDateInput.value = endDate; const refreshDateBtn = document.getElementById(`${tabId}-date-update-btn`); refreshDateBtn.addEventListener('click', async () => { const newStartDate = startDateInput.value; const newEndDate = endDateInput.value; const newDateRequestData = { tabId: tabId, startDate: newStartDate, endDate: newEndDate }; if (newStartDate && newEndDate) { tabContent.innerHTML = ` `; const newPeriodData = await apiRequest('/', newDateRequestData); if (newPeriodData.status == 'ok') { renderJurnalServicesTab(tabId, newPeriodData.data); } } }); // Рендерим основной контейнер с таблицей сервисных событий tabContent.innerHTML = `
Дата Пользователь Действие Детали
`; // Функция для фильтрации событий function filterServices() { let filtered = requests; // Фильтр по пользователю if (currentFilters.user !== 'all') { if (currentFilters.user === 'system') { filtered = filtered.filter(r => r.user_id === null); } else { filtered = filtered.filter(r => r.user_id == currentFilters.user); } } // Фильтр по типу действия if (currentFilters.action !== 'all') { filtered = filtered.filter(r => { const details = r.details; return Object.keys(details)[0] === currentFilters.action; }); } return filtered; } // Функция для рендеринга строк таблицы function renderServicesTable() { const tbody = document.getElementById(`${tabId}-services-body`); const noServicesDiv = document.getElementById(`${tabId}-no-services`); const filteredServices = filterServices(); if (filteredServices.length === 0) { tbody.innerHTML = ''; noServicesDiv.style.display = 'block'; return; } noServicesDiv.style.display = 'none'; tbody.innerHTML = filteredServices.map(service => { const actionType = Object.keys(service.details)[0]; const actionData = service.details[actionType]; // Определяем пользователя let userName = 'Система'; if (service.user_id) { userName = userMap[service.user_id] || `Пользователь ${service.user_id}`; } // Форматируем детали в зависимости от типа действия let detailsHtml = ''; if (actionType === 'Авторизован пользователь') { // Для авторизации detailsHtml = `
${actionData}
`; } else if (actionType.includes('Добавлен') || actionType.includes('Обновлен') || actionType.includes('Добавлена')) { // Для добавления/обновления сущностей const entityName = actionData.title || actionData.username || actionData.login || ''; detailsHtml = `
${entityName}
${actionData.description ? `
${actionData.description}
` : ''} ${actionData.id ? `
ID: ${actionData.id}
` : ''} `; // Для инструментов добавляем дополнительные поля if (actionData.specifications && Object.keys(actionData.specifications).length > 0) { detailsHtml += `
Характеристики:
`; detailsHtml += `
`; for (const [key, value] of Object.entries(actionData.specifications)) { detailsHtml += `
${key}: ${value}
`; } detailsHtml += `
`; } if (actionData.external_link) { detailsHtml += `
Ссылка: ${actionData.external_link}
`; } if (actionData.quantity_min || actionData.quantity_min_extra) { detailsHtml += `
Мониторинг остатков:
`; if (actionData.quantity_min && actionData.quantity_min_extra) { detailsHtml += `
Минимальное количество: ${actionData.quantity_min}
`; detailsHtml += `
Минимальное критическое количество: ${actionData.quantity_min_extra}
`; } else if (actionData.quantity_min && !actionData.quantity_min_extra) { detailsHtml += `
Минимальное количество: ${actionData.quantity_min}
`; } else if (actionData.quantity_min_extra && !actionData.quantity_min) { detailsHtml += `
Минимальное критическое количество: ${actionData.quantity_min_extra}
`; } } if (actionData.category_id) { detailsHtml += `
Категория: ${categoriesMap[actionData.category_id].title} [${categoriesMap[actionData.category_id].description}]
`; } if (actionData.image) { detailsHtml += `
Изображения:
`; detailsHtml += `
Основное:
`; detailsHtml += `
Основное изображение инструмента`; if (actionData.image.additional) { detailsHtml += `
Дополнительные:
`; detailsHtml += `
`; actionData.image.additional.forEach(img => { detailsHtml += `
Дополнительное изображение инструмента
`; }); detailsHtml += `
`; } detailsHtml += `
`; } } return ` ${service.created_at} ${userName} ${actionType} ${detailsHtml} `; }).join(''); } // Назначаем обработчики событий для фильтров document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () { currentFilters.user = this.value; saveToStorage(tabId, currentFilters); renderServicesTable(); }); document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () { currentFilters.action = this.value; saveToStorage(tabId, currentFilters); renderServicesTable(); }); // Устанавливаем сохраненные значения фильтров if (savedFilters) { document.getElementById(`${tabId}-user-filter`).value = currentFilters.user; document.getElementById(`${tabId}-action-filter`).value = currentFilters.action; } // Первоначальный рендеринг таблицы renderServicesTable(); } document.addEventListener('DOMContentLoaded', async () => { await getCookieData(); if (!accessData || !userData) { console.warn('Access data or user data not found'); return; } prepareTabs(); }); window.openTab = openTab;