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 = { "Инструмент для ЧПУ": [ { "Тип крепления": ["BT", "ER", "HSK", "ISO", "Weldon"] }, "Допуск биения", "Длина режущей части", "Максимальная подача", "Максимальные обороты", "Рабочая часть", { "Тип спирали": ["Левосторонняя", "Правосторонняя"] } ], "Сверла": [ { "Тип хвостовика": ["Морзе", "Цилиндрический"] } ], "Токарка": [ "Пластины", { "Форма пластины": ["C", "D", "T", "V", "W", "Резьбовая"] }, "Радиус", { "Тип резьбы": ["Внутренняя", "Наружная"] }, { "Исполнение резьбы": ["Левая", "Правая"] }, "Шаг", { "Угол резьбы": [30, 55, 60] }, "Шаг", { "Профиль резьбы": ["Неполный", "Полный"] } ], "Универсальные": [ "Диаметр", "Длина", { "Вид обработки": ["На удар", "Черновая", "Чистовая"] }, { "Материал инструмента": ["HSS", "Твёрдый сплав"] }, { "Покрытие": ["AlTiN", "TiAlN", "TiN"] }, "Тип хвостовика", "Назначение", { "Обрабатываемый материал": [ "H (Твёрдые материалы)", "M (Нержавеющая сталь)", "N (Цветные металлы)", "P (Сталь)", "S (Жаропрочные)" ] } ], "Фрезеровка": [ "Кол-во перьев", { "Тип фрезы": [ "Коническая", "Концевая", "Радиусная", "Сферическая", "Торцевая", "Фасочная" ] }, "Угол спирали", "Геометрия зубьев" ] }; async function getCookieData() { accessData = await getCookie('toolbox_access'); userData = await getCookie('toolbox_user'); await checkActiveUser(); } async function checkActiveUser() { const activeCookie = loadFromStorage('active'); if (!activeCookie || !activeCookie.active || !activeCookie.datetime || Date.now() - activeCookie.datetime > 12 * 60 * 60 * 1000) { const checkActive = await apiRequest('/user/check', { userId: userData.id }); if (checkActive.status === 'ok') { saveToStorage('active', { active: true, datetime: Date.now() }); } else { window.clientManager?.initLogout(); } } } async function openTab(event, tabId, autoLoad = false) { const activeTab = loadFromStorage('tab'); if (activeTab && activeTab.tabId === tabId && !autoLoad) { return; } // Убираем активный класс со всех вкладок и кнопок 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: 'Управление запросами на инструменты' }; } tabsData['orders'] = { title: 'Заказы', icon: 'bi-basket', 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, true); } } } 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 'orders': renderOrdersTab(tabId, tabData); break; case 'jurnal_toolkits': renderJurnalToolkitsTab(tabId, tabData); break; case 'jurnal_service': renderJurnalServicesTab(tabId, tabData); break; case 'users': renderUsersTab(tabId, tabData); break; } } catch (error) { console.error('Error filling tab:', error); const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = ` `; } } 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 normalize = s => s.toLowerCase().replace(/ё/g, 'е'); const searchWords = normalize(filterText) .split(/\s+/) .filter(Boolean); const title = normalize(tool.title); const description = normalize(tool.description); const matches = searchWords.every(word => title.includes(word)) || searchWords.every(word => description.includes(word)); if (!matches) { 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, ' '); } function toolkitFillReplace(toolboxId, toolkitId, tabId) { if (!accessData.tools_registration) return ''; const manageBtns = `
`; // Навешиваем обработчики один раз if (!document.body.dataset.toolkitHandlers) { document.body.dataset.toolkitHandlers = 'true'; document.addEventListener('click', async (e) => { const actionBtn = e.target.closest('.toolkit-action-btn'); if (!actionBtn) return; const { action, toolbox, toolkit, tab } = actionBtn.dataset; // Удаляем старую модалку если была document.getElementById('toolkitDynamicModal')?.remove(); let modalHtml = ''; if (action === 'fill') { modalHtml = ` `; } if (action === 'move') { modalHtml = ` `; } document.body.insertAdjacentHTML('beforeend', modalHtml); const modalEl = document.getElementById('toolkitDynamicModal'); const modal = new bootstrap.Modal(modalEl); modal.show(); modalEl.querySelector('#toolkitModalSubmit').addEventListener('click', async () => { const form = modalEl.querySelector('#toolkitModalForm'); if (!form.checkValidity()) { form.reportValidity(); return; } const data = Object.fromEntries(new FormData(form).entries()); const userId = userData.id; const response = await apiRequest('/toolkit/quick_action', { action, toolboxId: toolbox, toolkitId: toolkit, data, userId }); if (response.status === 'ok') { showInfo('Действие успешно выполнено', 'success'); if (tab === 'toolbox') { await selectToolbox(toolbox); } if (tab === 'toolkits') { await uploadTab(tab); showInfo('В открытом окне карточки данные не обновятся автоматически', 'info'); } } else { showInfo('Произошла ошибка при выполнении действия', 'danger'); throw new Error(response.message || 'Произошла ошибка при выполнении действия'); } modal.hide(); modalEl.remove(); }, { once: true }); }); } return manageBtns; } // Функция инициализации таблицы 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} ${toolkitFillReplace(item.toolboxId, item.id, 'toolbox')} ` : ''} ${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) { const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } 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; let compatibilityData = null; let isCompatibilityLoading = false; let allToolkitsData = null; // Форматирование даты комментария let commentDateInfo = ''; if (toolkitData.comment_at) { const commentDate = new Date(toolkitData.comment_at); commentDateInfo = `
Последнее изменение: ${commentDate.toLocaleDateString('ru-RU')} ${commentDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
`; } else { commentDateInfo = `
Комментарии еще не оставляли
`; } // Информация о пользователе, оставившем комментарий let commentUserInfo = ''; if (toolkitData.comment_user_data && toolkitData.comment_user_data.username) { commentUserInfo = `
Автор: ${toolkitData.comment_user_data.username}
`; } 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 commentTextarea = modal.querySelector('#toolkitComment'); const saveCommentBtn = modal.querySelector('#saveCommentBtn'); const originalComment = toolkitData.comment_text || ''; // Проверка изменения комментария const checkCommentChanged = () => { const currentComment = commentTextarea.value.trim(); const isChanged = currentComment !== originalComment; saveCommentBtn.disabled = !isChanged || currentComment === ''; }; // Слушатель изменений в текстовом поле commentTextarea.addEventListener('input', checkCommentChanged); // Обработчик сохранения комментария saveCommentBtn.addEventListener('click', async () => { const commentText = commentTextarea.value.trim(); if (!commentText) { showInfo('Комментарий не может быть пустым', 'warning'); return; } try { // Блокируем кнопку на время отправки saveCommentBtn.disabled = true; saveCommentBtn.innerHTML = 'Сохранение...'; // Отправляем комментарий на сервер const response = await apiRequest('/toolkit/comment', { toolkitId: toolkitData.id, userId: userData.id, commentText: commentText }, 'POST'); if (response.status === 'ok') { // Обновляем информацию о комментарии без перезагрузки страницы const now = new Date(); // Обновляем информацию о дате const dateInfoDiv = modal.querySelector('.border-top .text-muted.small:first-child'); if (dateInfoDiv) { dateInfoDiv.innerHTML = ` Последнее изменение: ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} `; } // Обновляем информацию о пользователе const userInfoDiv = modal.querySelector('.border-top .text-muted.small:nth-child(2)'); if (userInfoDiv) { userInfoDiv.innerHTML = ` Автор: ${userData.username || 'Текущий пользователь'} `; } else if (userData.username) { // Если элемента с информацией о пользователе не было, создаем его const commentInfoDiv = modal.querySelector('.border-top .mb-2'); if (commentInfoDiv) { const newUserInfo = document.createElement('div'); newUserInfo.className = 'text-muted small'; newUserInfo.innerHTML = ` Автор: ${userData.username} `; commentInfoDiv.appendChild(newUserInfo); } } // Обновляем оригинальный комментарий для дальнейших проверок toolkitData.comment_text = commentText; toolkitData.comment_at = now.toISOString(); toolkitData.comment_user_data = { username: userData.username }; // Показываем успешное сообщение showInfo('Комментарий успешно сохранен', 'success'); // Кнопка становится неактивной, так как изменения сохранены saveCommentBtn.innerHTML = 'Сохранено'; setTimeout(() => { saveCommentBtn.innerHTML = 'Сохранить'; checkCommentChanged(); }, 1500); } else { throw new Error(response.message || 'Ошибка сохранения комментария'); } } catch (error) { console.error('Ошибка при сохранении комментария:', error); showInfo(error.message || 'Произошла ошибка при сохранении комментария', 'danger'); saveCommentBtn.disabled = false; saveCommentBtn.innerHTML = 'Сохранить'; } }); // Функция для загрузки данных об остатках 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 ? ` ` : ''} ${value.id ? toolkitFillReplace(value.id, toolkitData.id, 'toolkits') : ''}
`; } 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 loadCompatibilityData = async () => { if (isCompatibilityLoading) return; const compatibilityLoading = modal.querySelector('#compatibilityLoading'); const compatibilityContent = modal.querySelector('#compatibilityContent'); const compatibilityError = modal.querySelector('#compatibilityError'); const addCompatibilityBtn = modal.querySelector('#addCompatibilityBtn'); try { isCompatibilityLoading = true; // Показываем спиннер, скрываем контент и ошибку compatibilityLoading.classList.remove('d-none'); compatibilityContent.classList.add('d-none'); compatibilityError.classList.add('d-none'); // Загружаем данные о совместимости const response = await apiRequest(`/toolkit/compatibility?toolkitId=${toolkitData.id}`, {}, 'GET'); if (response.status === 'ok') { compatibilityData = response.data; // Формируем HTML для совместимости let compatibilityHtml = ''; if (Object.keys(compatibilityData.records || {}).length > 0) { compatibilityHtml = `
${Object.entries(compatibilityData.records || {}).map(([recordId, compatibleToolkitId]) => { const compatibleToolkit = compatibilityData.toolkits[compatibleToolkitId]; const compatibleCategory = categories[compatibleToolkit?.category_id]; return ` `; }).join('')}
Инструмент Категория Комментарий
${compatibleToolkit?.title || 'Неизвестный инструмент'} ${compatibleToolkit ? ` ` : ''} ${accessData.tools_edit ? ` ` : ''} ${compatibleCategory?.title || 'Неизвестно'} ${compatibleToolkit?.comment_text || 'Нет комментария'}
`; } else { compatibilityHtml = `
Нет данных о совместимых инструментах
`; } // Вставляем HTML и показываем контент compatibilityContent.innerHTML = compatibilityHtml; compatibilityContent.classList.remove('d-none'); // Показываем кнопку добавления, если есть права if (accessData.tools_edit) { addCompatibilityBtn.style.display = 'inline-block'; } // Добавляем обработчики для кнопок просмотра инструмента compatibilityContent.querySelectorAll('.view-toolkit-btn').forEach(button => { button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const compatibleToolkitId = e.currentTarget.dataset.toolkitId; const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } modal.addEventListener('hidden.bs.modal', async () => { await showToolkitDetailsModal(compatibleToolkitId); }, { once: true }); }); }); // Добавляем обработчики для кнопок удаления compatibilityContent.querySelectorAll('.delete-compatibility-btn').forEach(button => { button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const recordId = e.currentTarget.dataset.recordId; const compatibleToolkitId = e.currentTarget.dataset.toolkitId; if (confirm('Вы уверены, что хотите удалить эту связь совместимости?')) { await deleteCompatibility(recordId, compatibleToolkitId); } }); }); } else { throw new Error(response.message || 'Ошибка загрузки данных о совместимости'); } } catch (error) { console.error('Ошибка при загрузке данных о совместимости:', error); compatibilityError.classList.remove('d-none'); } finally { compatibilityLoading.classList.add('d-none'); isCompatibilityLoading = false; } }; // Функция для удаления совместимости const deleteCompatibility = async (recordId, compatibleToolkitId) => { try { const response = await apiRequest('/toolkit/compatibility', { action: 'delete', userId: userData.id, data: { toolkitId: toolkitData.id, compatibleToolkitId: compatibleToolkitId } }, 'POST'); if (response.status === 'ok') { showInfo('Связь совместимости успешно удалена', 'success'); // Обновляем данные compatibilityData = null; await loadCompatibilityData(); } else { throw new Error(response.message || 'Ошибка удаления связи'); } } catch (error) { console.error('Ошибка при удалении совместимости:', error); showInfo(error.message || 'Произошла ошибка при удалении связи', 'danger'); } }; // Функция для добавления совместимости const addCompatibility = async (compatibleToolkitId) => { try { const response = await apiRequest('/toolkit/compatibility', { action: 'add', userId: userData.id, data: { toolkitId: toolkitData.id, compatibleToolkitId: compatibleToolkitId } }, 'POST'); if (response.status === 'ok') { showInfo('Связь совместимости успешно добавлена', 'success'); // Обновляем данные compatibilityData = null; await loadCompatibilityData(); // Закрываем модальное окно добавления const addModal = bootstrap.Modal.getInstance(document.getElementById(`${toolkitData.id}-add-compatibility-modal`)); if (addModal) addModal.hide(); } else { throw new Error(response.message || 'Ошибка добавления связи'); } } catch (error) { console.error('Ошибка при добавлении совместимости:', error); showInfo(error.message || 'Произошла ошибка при добавлении связи', 'danger'); } }; // Функция для показа модального окна добавления совместимости const showAddCompatibilityModal = async () => { // Загружаем все инструменты, если еще не загружены if (!allToolkitsData) { try { const response = await apiRequest('/toolkit/all', {}, 'GET'); if (response.status === 'ok') { allToolkitsData = response.data; } } catch (error) { console.error('Ошибка загрузки списка инструментов:', error); showInfo('Не удалось загрузить список инструментов', 'danger'); return; } } // Создаем модальное окно const addModalId = `${toolkitData.id}-add-compatibility-modal`; let addModal = document.getElementById(addModalId); if (addModal) { addModal.remove(); } addModal = document.createElement('div'); addModal.className = 'modal fade'; addModal.id = addModalId; addModal.tabIndex = -1; // Фильтруем инструменты: исключаем текущий и уже совместимые const compatibleIds = Object.values(compatibilityData?.records || {}); const filteredToolkits = Object.values(allToolkitsData || {}).filter(toolkit => toolkit.id !== toolkitData.id && !compatibleIds.includes(toolkit.id) ); addModal.innerHTML = ` `; document.body.appendChild(addModal); const bsAddModal = new bootstrap.Modal(addModal); bsAddModal.show(); // Функция поиска инструментов const searchInput = addModal.querySelector('#compatibilitySearch'); const toolkitsList = addModal.querySelector('#compatibilityToolkitsList'); const rows = toolkitsList.querySelectorAll('tr'); searchInput.addEventListener('input', function () { const searchTerm = this.value.toLowerCase(); rows.forEach(row => { const title = row.dataset.toolkitTitle; const isVisible = title.includes(searchTerm); row.style.display = isVisible ? '' : 'none'; }); }); // Обработчики для кнопок выбора addModal.querySelectorAll('.select-compatibility-btn').forEach(button => { button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const compatibleToolkitId = e.currentTarget.dataset.toolkitId; await addCompatibility(compatibleToolkitId); }); }); // Очистка при закрытии addModal.addEventListener('hidden.bs.modal', () => { addModal.remove(); }); }; // Обработчик события раскрытия аккордеона остатков const stocksCollapse = modal.querySelector('#stocksCollapse'); stocksCollapse.addEventListener('show.bs.collapse', async () => { if (!toolkitStocksData && !isStocksLoading) { await loadToolkitStocks(); } }); // Обработчик события раскрытия аккордеона совместимости const compatibilityCollapse = modal.querySelector('#compatibilityCollapse'); compatibilityCollapse.addEventListener('show.bs.collapse', async () => { if (!compatibilityData && !isCompatibilityLoading) { await loadCompatibilityData(); } }); // Обработчик для кнопки добавления совместимости const addCompatibilityBtn = modal.querySelector('#addCompatibilityBtn'); if (addCompatibilityBtn) { addCompatibilityBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showAddCompatibilityModal(); }); // Остановка всплытия события при клике на кнопку внутри аккордеона const compatibilityHeading = modal.querySelector('#compatibilityHeading'); compatibilityHeading.addEventListener('click', (e) => { if (addCompatibilityBtn.contains(e.target)) { e.stopPropagation(); } }); } // Очистка при закрытии модалки 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 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 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); /* ---------- helpers ---------- */ function buildSpecNameDropdown() { return Object.entries(predefinedSpecs).map(([category, items]) => `
  • ${items.map(item => { const name = typeof item === 'string' ? item : Object.keys(item)[0]; return `
  • `; }).join('')} `).join(''); } function updateValueDropdown(specName) { const dropdown = document.getElementById('specValueDropdown'); dropdown.innerHTML = ''; const values = findSpecValues(specName); if (!values) { dropdown.innerHTML = `
  • Только ручной ввод
  • `; return; } values.forEach(val => { dropdown.innerHTML += `
  • `; }); } function findSpecValues(name) { for (const items of Object.values(predefinedSpecs)) { for (const item of items) { if (typeof item === 'object' && item[name]) { return item[name]; } } } return null; } /* ---------- global callbacks ---------- */ window.selectPredefinedSpec = function (name) { document.getElementById('specName').value = name; updateValueDropdown(name); }; window.selectSpecValue = function (value) { document.getElementById('specValue').value = value; }; /* ---------- save ---------- */ specModal.querySelector('#saveSpecBtn').addEventListener('click', () => { const name = document.getElementById('specName').value.trim(); const value = document.getElementById('specValue').value.trim(); if (!name) return showError('Введите название характеристики'); if (!value) return showError('Введите значение характеристики'); 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(() => 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 renderOrdersTab(tabId, tabData) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const { orders, users, startDate, endDate, fullAccess } = tabData; // Создаем мапу пользователей const userMap = {}; users.forEach(user => { userMap[user.id] = user.username; }); // const ordersStatuses = { // } // Сохраненные фильтры const savedFilters = loadFromStorage(tabId); let currentFilters = { customer: savedFilters?.customer || 'all', status: savedFilters?.status || 'all', search: savedFilters?.search || '' }; // Хранилище измененных данных const changedOrders = {}; const statusesList = [] const statusesMap = { 'new': 'Новый', 'working': 'В работе', 'complete': 'Выполнен', 'cancelled': 'Отменен', } // Оригинальные данные заказов (для сравнения) const originalOrders = {}; orders.forEach(o => { originalOrders[o.id] = { status: o.status, executor_comment: o.executor_comment || '' }; if (!statusesList.includes(o.status)) { statusesList.push(o.status) } }); // Рендерим дополнительный контейнер с фильтрами и кнопками tabOptionalContent.innerHTML = `
    ${!fullAccess ? `
    ` : `
    ` }
    Дата начала:
    Дата окончания:
    `; // Модальное окно для нового заказа if (!fullAccess) { document.body.insertAdjacentHTML('beforeend', ` `); } // Инициализация фильтров if (fullAccess) { document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer; } document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; document.getElementById(`${tabId}-search-filter`).value = currentFilters.search; // Обработчики событий const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`); filterResetBtn.addEventListener('click', () => { currentFilters = { customer: 'all', status: 'all', search: '' }; if (fullAccess) { document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer; } document.getElementById(`${tabId}-status-filter`).value = currentFilters.status; document.getElementById(`${tabId}-search-filter`).value = currentFilters.search; saveToStorage(tabId, currentFilters); renderOrdersTable(); }); // Дата фильтры 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; if (newStartDate && newEndDate) { tabContent.innerHTML = ` `; const cookiesData = { userData, accessData }; const newPeriodData = await apiRequest('/', { tabId: tabId, startDate: newStartDate, endDate: newEndDate, cookiesData }); if (newPeriodData.status === 'ok') { renderOrdersTab(tabId, { ...tabData, ...newPeriodData.data, startDate: newStartDate, endDate: newEndDate, fullAccess }); await checkNewOrders(); } } }); // Кнопка нового заказа if (!fullAccess) { const newOrderBtn = document.getElementById(`${tabId}-new-order-btn`); newOrderBtn.addEventListener('click', () => { const modal = new bootstrap.Modal(document.getElementById(`${tabId}-new-order-modal`)); modal.show(); }); // Отправка нового заказа const orderSubmitBtn = document.getElementById(`${tabId}-order-submit-btn`); orderSubmitBtn.onclick = async () => { if (orderSubmitBtn.disabled) return; orderSubmitBtn.disabled = true; const description = document.getElementById(`${tabId}-order-description`).value; if (!description.trim()) { showInfo('Пожалуйста, введите описание заказа', 'warning'); orderSubmitBtn.disabled = false; return; } try { const result = await apiRequest('/orders/new', { userId: userData.id, customer_comment: description }); if (result.status === 'ok') { const modal = bootstrap.Modal.getInstance( document.getElementById(`${tabId}-new-order-modal`) ); modal.hide(); document.getElementById(`${tabId}-order-description`).value = ''; refreshDateBtn.click(); } else { showInfo(result.message || 'Ошибка при создании заказа', 'danger'); } } finally { orderSubmitBtn.disabled = false; } }; } if (orders.length === 0) { tabContent.innerHTML = ` `; return; } // Рендерим таблицу tabContent.innerHTML = `
    ${fullAccess ? '' : ''}
    Заказчик Описание заказа Статус Исполнитель Комментарий исполнителя⚙️
    `; // Функция фильтрации function filterOrders() { let filtered = orders; // Фильтр по заказчику if (fullAccess && currentFilters.customer !== 'all') { filtered = filtered.filter(o => o.customer_id == currentFilters.customer); } // Фильтр по статусу if (currentFilters.status !== 'all') { filtered = filtered.filter(o => o.status === currentFilters.status); } // Поиск if (currentFilters.search.trim()) { const searchTerm = currentFilters.search.toLowerCase(); filtered = filtered.filter(o => { return ( (userMap[o.customer_id] || '').toLowerCase().includes(searchTerm) || o.customer_comment.toLowerCase().includes(searchTerm) || (userMap[o.executor_id] || '').toLowerCase().includes(searchTerm) || (o.executor_comment || '').toLowerCase().includes(searchTerm) || o.status.toLowerCase().includes(searchTerm) ); }); } return filtered; } // Функция рендеринга таблицы function renderOrdersTable() { const tbody = document.getElementById(`${tabId}-orders-body`); const noOrdersDiv = document.getElementById(`${tabId}-no-orders`); const filteredOrders = filterOrders(); if (filteredOrders.length === 0) { tbody.innerHTML = ''; noOrdersDiv.style.display = 'block'; return; } noOrdersDiv.style.display = 'none'; tbody.innerHTML = filteredOrders.map(order => { // Определяем класс для статуса const statusClass = { new: 'warning', working: 'primary', complete: 'success', cancelled: 'danger' }[order.status] || 'secondary'; // Определяем русское название статуса const statusText = { new: 'Новый', working: 'В работе', complete: 'Выполнен', cancelled: 'Отменен' }[order.status] || order.status; // Рендерим статус let statusCell; if (fullAccess && (order.status === 'new' || order.status === 'working')) { statusCell = ` `; } else { statusCell = `${statusText}`; } // Рендерим строку return ` ${userMap[order.customer_id] || `Пользователь ${order.customer_id}`}
    ${order.created_at} ${order.customer_comment} ${statusCell}
    ${order.updated_at} ${order.executor_id ? (userMap[order.executor_id] || `Пользователь ${order.executor_id}`) : '-'} ${fullAccess ? ` `: ''} `; }).join(''); function updateRowState(orderId) { const original = originalOrders[orderId]; const current = changedOrders[orderId]; const hasChanges = current && ( (current.status !== undefined && current.status !== original.status) || (current.executor_comment !== undefined && current.executor_comment !== original.executor_comment) ); const saveBtn = document.querySelector( `.save-row-btn[data-order-id="${orderId}"]` ); if (saveBtn) { saveBtn.disabled = !hasChanges; } } // Добавляем обработчики изменений if (fullAccess) { document.querySelectorAll(`.order-status`).forEach(select => { select.addEventListener('change', function () { const orderId = this.dataset.orderId; changedOrders[orderId] = { ...changedOrders[orderId], status: this.value }; updateRowState(orderId); }); }); document.querySelectorAll(`.executor-comment`).forEach(textarea => { textarea.addEventListener('input', function () { const orderId = this.dataset.orderId; changedOrders[orderId] = { ...changedOrders[orderId], executor_comment: this.value }; updateRowState(orderId); }); }); document.querySelectorAll('.save-row-btn').forEach(btn => { btn.addEventListener('click', async function () { const orderId = this.dataset.orderId; const original = originalOrders[orderId]; const current = changedOrders[orderId]; if (!current) return; // Формируем diff const payload = { orderId, userId: userData.id }; if (current.status !== undefined && current.status !== original.status) { payload.status = current.status; } if ( current.executor_comment !== undefined && current.executor_comment !== original.executor_comment ) { payload.comment = current.executor_comment; } if (Object.keys(payload).length === 2) return; this.disabled = true; const result = await apiRequest('/orders/', payload); if (result.status === 'ok') { // Обновляем оригинал originalOrders[orderId] = { status: payload.status ?? original.status, executor_comment: payload.comment ?? original.executor_comment }; delete changedOrders[orderId]; if (payload.status && original.status === 'new') { await checkNewOrders(); } this.innerHTML = ''; setTimeout(() => { this.innerHTML = ''; }, 1500); if (originalOrders[orderId].status === 'complete' || originalOrders[orderId].status === 'cancelled') { const foundOrder = orders.find(order => order.id === Number(orderId)); if (foundOrder) { foundOrder.executor_id = userData.id; foundOrder.executor_comment = originalOrders[orderId].executor_comment; foundOrder.status = current.status; } renderOrdersTable(); } } else { showInfo('Ошибка сохранения', 'danger'); this.disabled = false; } }); }); } } // Обработчики фильтров if (fullAccess) { document.getElementById(`${tabId}-customer-filter`).addEventListener('change', function () { currentFilters.customer = this.value; saveToStorage(tabId, currentFilters); renderOrdersTable(); }); } document.getElementById(`${tabId}-status-filter`).addEventListener('change', function () { currentFilters.status = this.value; saveToStorage(tabId, currentFilters); renderOrdersTable(); }); document.getElementById(`${tabId}-search-filter`).addEventListener('input', function () { currentFilters.search = this.value; saveToStorage(tabId, currentFilters); renderOrdersTable(); }); // Первоначальный рендеринг renderOrdersTable(); } 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.includes('пользователь')) { // Для авторизации detailsHtml = `
    ${typeof actionData === 'object' ? Object.entries(actionData).map(([key, value]) => `
    ${key}: ${value}
    `).join('') : 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(); } function renderUsersTab(tabId, tabData) { const tabContent = document.getElementById(`${tabId}-tab-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const { users, accessLevels } = tabData; users.sort((a, b) => a.username.localeCompare(b.username)); // Создаем мапу уровней доступа для быстрого доступа const accessLevelMap = {}; const accessLevelTitles = []; accessLevels.forEach(level => { accessLevelMap[level.id] = level.title; accessLevelTitles.push(level.title); }); const userUsernames = []; const userLogins = []; users.forEach(user => { userUsernames.push(user.username); userLogins.push(user.login); }); // Рендерим опциональный блок с кнопками tabOptionalContent.innerHTML = `
    ${accessData.users_creation ? ` ` : ''} ${accessData.access_level_view ? ` ` : ''}
    `; // Функция для показа модального окна с подтверждением function showConfirmationModal(title, message, confirmCallback) { const modalId = `${tabId}-confirmation-modal`; // Удаляем старую модалку, если есть const existingModal = document.getElementById(modalId); if (existingModal) { existingModal.remove(); } const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); const modal = new bootstrap.Modal(document.getElementById(modalId)); document.getElementById(`${modalId}-confirm-btn`).addEventListener('click', () => { confirmCallback(); modal.hide(); }); modal.show(); // Удаляем модалку после скрытия modal._element.addEventListener('hidden.bs.modal', () => { modal._element.remove(); }); } // Функция для рендеринга карточек пользователей function renderUsersCards() { if (!users || users.length === 0) { tabContent.innerHTML = ` `; return; } tabContent.innerHTML = `
    ${users.map(user => { const isActive = user.is_active; const levelTitle = accessLevelMap[user.access_level_id] || `Уровень ${user.access_level_id}`; const createdAt = new Date(user.created_at).toLocaleDateString('ru-RU'); return `
    ${user.username}
    ${user.username || 'Без имени'}
    ${user.login || 'Без логина'}
    ${levelTitle}
    Создан: ${createdAt}
    Обновлен: ${user.updated_at ? new Date(user.updated_at).toLocaleDateString('ru-RU') : 'Нет данных'}
    ${accessData.users_edit ? ` ` : ''} ${accessData.users_disabling ? ` ` : ''}
    `; }).join('')}
    `; // Назначаем обработчики для кнопок в карточках document.querySelectorAll(`.edit-user-btn`).forEach(btn => { btn.addEventListener('click', async () => { const userId = btn.dataset.userId; const user = users.find(u => u.id == userId); if (user) { await openEditUserModal(user); } }); }); document.querySelectorAll(`.toggle-user-btn`).forEach(btn => { btn.addEventListener('click', async () => { const userId = btn.dataset.userId; const isActive = btn.dataset.isActive === 'true'; const user = users.find(u => u.id == userId); if (user) { const action = isActive ? 'заблокировать' : 'разблокировать'; showConfirmationModal( `${isActive ? 'Блокировка' : 'Разблокировка'} пользователя`, `Вы уверены, что хотите ${action} пользователя ${user.username}?`, () => toggleUserStatus(userId, !isActive) ); } }); }); } // Функция для открытия модального окна редактирования пользователя async function openEditUserModal(user = null) { const isNew = !user; const modalId = `${tabId}-edit-user-modal`; // Удаляем старую модалку, если есть const existingModal = document.getElementById(modalId); if (existingModal) { existingModal.remove(); } const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); const modal = new bootstrap.Modal(document.getElementById(modalId)); let photoFile = null; let removePhoto = false; // Обработчики для фото if (accessData.users_edit) { const changePhotoBtn = document.getElementById(`${modalId}-change-photo-btn`); const photoInput = document.getElementById(`${modalId}-photo-input`); const photoPreview = document.getElementById(`${modalId}-photo-preview`); changePhotoBtn.addEventListener('click', () => { photoInput.click(); }); photoInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { if (file.type.startsWith('image/')) { photoFile = file; const reader = new FileReader(); reader.onload = (e) => { photoPreview.src = e.target.result; }; reader.readAsDataURL(file); removePhoto = false; } else { showConfirmationModal('Ошибка', 'Пожалуйста, выберите файл изображения', () => { }); photoInput.value = ''; } } }); if (document.getElementById(`${modalId}-remove-photo-btn`)) { document.getElementById(`${modalId}-remove-photo-btn`).addEventListener('click', () => { photoPreview.src = 'static/images/users/default.png'; photoFile = null; removePhoto = true; }); } } // Обработчик удаления пользователя if (!isNew && accessData.users_disabling) { document.getElementById(`${modalId}-delete-btn`).addEventListener('click', () => { modal.hide(); showConfirmationModal( 'Удаление пользователя', `Вы уверены, что хотите удалить пользователя ${user.username}? Это действие нельзя отменить.`, () => deleteUser(user.id) ); }); } // Обработчик сохранения document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => { const form = document.getElementById(`${modalId}-form`); if (!form.checkValidity()) { form.reportValidity(); return; } const formUserData = { login: document.getElementById(`${modalId}-login`).value.trim(), username: document.getElementById(`${modalId}-username`).value.trim(), access_level_id: parseInt(document.getElementById(`${modalId}-access-level`).value), }; if (isNew || ((user && formUserData.login != user.login))) { if (userLogins.includes(formUserData.login)) { showInfo('Пользователь с таким логином уже существует', 'warning'); return; } } if (isNew || ((user && formUserData.username != user.username))) { if (userUsernames.includes(formUserData.username)) { showInfo('Пользователь с таким именем уже существует', 'warning'); return; } } // Добавляем пароль, если он указан const password = document.getElementById(`${modalId}-password`).value; if (password || isNew) { formUserData.password = password || ''; } // Определяем действие let action = isNew ? 'create' : 'update'; // Обработка фото if (photoFile) { if (photoFile.size > 5 * 1024 * 1024) { showInfo('Фото больше 5 МБ, оно не подходит', 'warning'); return; } formUserData.photo = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error('Ошибка чтения файла')); reader.readAsDataURL(photoFile); }); } else if (removePhoto) { formUserData.photo = ''; } let changedUserData = {}; if (isNew) { changedUserData = formUserData; } else { changedUserData = Object.keys(formUserData).reduce((acc, key) => { if (formUserData[key] !== user[key]) { acc[key] = formUserData[key]; } return acc; }, {}); } if (Object.keys(changedUserData).length === 0) { showInfo('Нет изменений', 'info'); return; } // Обновляем данные if (!isNew) { changedUserData.id = user.id; } await sendUserRequest(changedUserData, modal, action); }); modal.show(); // Удаляем модалку после скрытия modal._element.addEventListener('hidden.bs.modal', () => { modal._element.remove(); }); } // Функция для обновления данных async function refreshThisTab() { // Обновляем данные const newData = await apiRequest('/', { tabId: 'users' }); if (newData && newData.status === 'ok') { renderUsersTab(tabId, newData.data); } else { showInfo('Не удалось загрузить обновленные данные', 'error'); } } // Функция для отправки запроса на изменение пользователя const currentUserId = userData.id; async function sendUserRequest(userData, modal, action) { const result = await apiRequest('/user/', { action, userData, userId: currentUserId }); const actionTextMap = { create: 'создан', update: 'обновлен', remove_photo: 'обновлен', delete: 'удален' } if (result && result.status === 'ok') { if (modal) { modal.hide(); } showInfo('Пользователь успешно ' + actionTextMap[action], 'success'); await refreshThisTab(); } else { const errorMsg = result?.message || 'Произошла ошибка при ' + actionTextMap[action] + 'ии'; showInfo(errorMsg, 'error'); } } // Функция для удаления пользователя async function deleteUser(userId) { await sendUserRequest({ id: userId }, null, 'delete'); } // Функция для блокировки/разблокировки пользователя async function toggleUserStatus(userId, newStatus) { await sendUserRequest({ id: userId, is_active: newStatus }, null, 'update'); } // Функция для открытия модального окна управления уровнями доступа async function openAccessLevelsModal() { const modalId = `${tabId}-access-levels-modal`; // Удаляем старую модалку, если есть const existingModal = document.getElementById(modalId); if (existingModal) { existingModal.remove(); } // Создаем копию уровней доступа для редактирования const editableLevels = JSON.parse(JSON.stringify(accessLevels)); let newLevelMode = false; let currentLevelId = editableLevels[0]?.id; const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); const modal = new bootstrap.Modal(document.getElementById(modalId)); // Функция для загрузки содержимого уровня function loadLevelContent(levelId, isNew = false) { let level; if (isNew) { level = { id: 'new', title: '', description: '', tools_creation: false, tools_registration: false, tools_edit: false, tools_delete: false, users_creation: false, users_edit: false, users_disabling: false, users_view: false, available_own_toolbox: false, view_all_toolboxes: false, view_requests: false, view_services: false, access_level_view: false, access_level_edit: false, manage_toolboxes: false, debit_request_confirm: false, refund_request_confirm: false }; } else { level = editableLevels.find(l => l.id == levelId); } if (!level) return; currentLevelId = level.id; newLevelMode = isNew; const contentDiv = document.getElementById(`${modalId}-level-content`); const isEditable = accessData.access_level_edit; contentDiv.innerHTML = `
    ${isNew || isEditable ? ` ` : `
    ${level.title}
    `}
    ${isNew || isEditable ? ` ` : `
    ${level.description || 'Нет описания'}
    `}
    Основные права:
    ${generateCheckbox('tools_creation', 'Создание инструментов', level.tools_creation)} ${generateCheckbox('tools_edit', 'Редактирование инструментов', level.tools_edit)} ${generateCheckbox('users_view', 'Просмотр пользователей', level.users_view)} ${generateCheckbox('users_edit', 'Редактирование пользователей', level.users_edit)} ${generateCheckbox('users_disabling', 'Блокировка пользователей', level.users_disabling)} ${generateCheckbox('users_creation', 'Создание пользователей', level.users_creation)} ${generateCheckbox('access_level_view', 'Просмотр уровней доступа', level.access_level_view)} ${generateCheckbox('access_level_edit', 'Редактирование уровней доступа', level.access_level_edit)}
    Дополнительные права:
    ${generateCheckbox('view_requests', 'Просмотр журнала движения', level.view_requests)} ${generateCheckbox('view_services', 'Просмотр сервисного журнала', level.view_services)} ${generateCheckbox('manage_toolboxes', 'Управление складами', level.manage_toolboxes)} ${generateCheckbox('available_own_toolbox', 'Есть собственный склад', level.available_own_toolbox)} ${generateCheckbox('tools_registration', 'Оприходование инструментов', level.tools_registration)} ${generateCheckbox('tools_delete', 'Удаление инструментов', level.tools_delete)} ${generateCheckbox('debit_request_confirm', 'Решение по списанию', level.debit_request_confirm)} ${generateCheckbox('refund_request_confirm', 'Решение по возврату', level.refund_request_confirm)} ${generateCheckbox('view_all_toolboxes', 'Просмотр всех складов', level.view_all_toolboxes)}
    `; // Вспомогательная функция для генерации чекбоксов function generateCheckbox(name, label, checked) { return `
    `; } // Показываем/скрываем кнопки сохранения document.getElementById(`${modalId}-save-btn`).style.display = (isEditable && !isNew) ? 'block' : 'none'; document.getElementById(`${modalId}-add-save-btn`).style.display = (isEditable && isNew) ? 'block' : 'none'; } // Обработчики для вкладок уровней document.querySelectorAll(`.level-tab-btn`).forEach((btn, index) => { btn.addEventListener('click', () => { // Обновляем активную вкладку document.querySelectorAll(`.level-tab-btn`).forEach(b => { b.classList.remove('btn-primary'); b.classList.add('btn-outline-primary'); }); btn.classList.remove('btn-outline-primary'); btn.classList.add('btn-primary'); // Загружаем содержимое уровня const levelId = btn.dataset.levelId; newLevelMode = false; loadLevelContent(levelId, false); }); }); // Обработчик для добавления нового уровня if (accessData.access_level_edit) { document.getElementById(`${modalId}-add-level-btn`).addEventListener('click', () => { // Создаем новую вкладку const tabsContainer = document.getElementById(`${modalId}-level-tabs`); const newTabId = 'new-' + Date.now(); // Обновляем все вкладки document.querySelectorAll(`.level-tab-btn`).forEach(b => { b.classList.remove('btn-primary'); b.classList.add('btn-outline-primary'); }); // Добавляем новую вкладку const newTab = document.createElement('button'); newTab.className = 'btn btn-primary level-tab-btn'; newTab.textContent = 'Новый уровень'; newTab.dataset.levelId = newTabId; newTab.addEventListener('click', () => { document.querySelectorAll(`.level-tab-btn`).forEach(b => { b.classList.remove('btn-primary'); b.classList.add('btn-outline-primary'); }); newTab.classList.remove('btn-outline-primary'); newTab.classList.add('btn-primary'); loadLevelContent(newTabId, true); }); // Убираем кнопку добавления document.getElementById(`${modalId}-add-level-btn`).style.display = 'none'; tabsContainer.appendChild(newTab); loadLevelContent(newTabId, true); }); } async function sendLevelData(isNew) { const level = editableLevels.find(l => l.id == currentLevelId); if (!level && !isNew) return; // Собираем данные из формы const levelData = { title: document.getElementById(`${modalId}-level-title`).value, description: document.getElementById(`${modalId}-level-description`).value }; if (!levelData.title.trim() || (!isNew && levelData.title !== level.title && accessLevelTitles.includes(levelData.title)) || (isNew && accessLevelTitles.includes(levelData.title))) { showInfo('Ошибка при сохранении', 'error'); showInfo('Введите уникальное название уровня доступа', 'warning'); return; } if (!levelData.description.trim()) { showInfo('Ошибка при сохранении', 'error'); showInfo('Введите описание уровня доступа', 'warning'); return; } if (!isNew) { levelData.id = level.id; } // Проверяем обязательные поля и уникальность if (isNew) { if (!levelData.title.trim() || editableLevels.includes(levelData.title)) { showInfo('Ошибка при добавлении', 'error'); showInfo('Введите уникальное название уровня доступа', 'warning'); return; } } else { if (levelData.title !== level.title && (!levelData.title.trim() || editableLevels.includes(levelData.title))) { showInfo('Ошибка при сохранении', 'error'); showInfo('Введите уникальное название уровня доступа', 'warning'); return; } } // Собираем все чекбоксы const checkboxNames = [ 'tools_creation', 'tools_registration', 'tools_edit', 'tools_delete', 'users_creation', 'users_edit', 'users_disabling', 'users_view', 'available_own_toolbox', 'view_all_toolboxes', 'view_requests', 'view_services', 'access_level_view', 'access_level_edit', 'manage_toolboxes', 'debit_request_confirm', 'refund_request_confirm' ]; checkboxNames.forEach(name => { levelData[name] = document.getElementById(`${modalId}-${name}`).checked; }); let changedLevelData = {}; if (isNew) { changedLevelData = levelData; } else { changedLevelData = Object.keys(levelData).reduce((acc, key) => { if (levelData[key] !== level[key]) { acc[key] = levelData[key]; } return acc; }, {}); } if (Object.keys(changedLevelData).length === 0) { showInfo('Нет изменений', 'info'); return; } if (!isNew) { changedLevelData.id = level.id; } const result = await apiRequest('/user/level', { action: isNew ? 'create' : 'update', changedLevelData, userId: currentUserId }); if (result && result.status === 'ok') { modal.hide(); showInfo('Данные сохранены', 'success'); // Обновляем данные await refreshThisTab(); } else { const errorMsg = result?.message || 'Произошла ошибка при сохранении'; showInfo(errorMsg, 'error'); } } // Обработчик для сохранения изменений существующего уровня if (accessData.access_level_edit) { document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => { await sendLevelData(newLevelMode); }); // Обработчик для добавления нового уровня document.getElementById(`${modalId}-add-save-btn`).addEventListener('click', async () => { await sendLevelData(newLevelMode); }); } // Загружаем первый уровень по умолчанию if (editableLevels.length > 0) { loadLevelContent(editableLevels[0].id); } modal.show(); // Удаляем модалку после скрытия modal._element.addEventListener('hidden.bs.modal', () => { modal._element.remove(); }); } // Обработчики для кнопок в опциональном блоке if (accessData.users_creation) { document.getElementById(`${tabId}-add-user-btn`).addEventListener('click', () => { openEditUserModal(); }); } if (accessData.access_level_view) { document.getElementById(`${tabId}-access-levels-btn`).addEventListener('click', () => { openAccessLevelsModal(); }); } // Первоначальный рендеринг карточек renderUsersCards(); } async function checkNewOrders() { const result = await apiRequest('/orders/', {}, 'GET'); if (result && result.orders > 0) { const ordersTabBtn = document.getElementById('orders-tab'); // Удаляем старый бейдж, если есть const oldBadge = ordersTabBtn.querySelector('.orders-badge'); if (oldBadge) oldBadge.remove(); const newOrdersBadge = document.createElement('span'); newOrdersBadge.className = 'badge rounded-pill bg-danger position-absolute orders-badge'; newOrdersBadge.textContent = result.orders; // Позиция: правый верх newOrdersBadge.style.top = '6px'; newOrdersBadge.style.right = '8px'; ordersTabBtn.appendChild(newOrdersBadge); } if (result && result.orders == 0) { const ordersTabBtn = document.getElementById('orders-tab'); const oldBadge = ordersTabBtn.querySelector('.orders-badge'); if (oldBadge) oldBadge.remove(); } } document.addEventListener('DOMContentLoaded', async () => { await getCookieData(); if (!accessData || !userData) { console.warn('Access data or user data not found'); console.log(accessData, userData); return; } prepareTabs(); await checkNewOrders(); }); window.openTab = openTab;