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; async function getCookieData() { accessData = await getCookie('toolbox_access'); userData = await getCookie('toolbox_user'); } async function openTab(event, tabId) { // Убираем активный класс со всех вкладок и кнопок document.querySelectorAll('.tab-nav-btn').forEach(btn => { btn.classList.remove('active'); btn.querySelector('.nav-icon').classList.remove('text-primary'); btn.querySelector('.nav-icon').classList.add('text-muted'); }); document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.remove('show', 'active'); }); // Добавляем активный класс выбранной вкладке и кнопке event.currentTarget.classList.add('active'); event.currentTarget.querySelector('.nav-icon').classList.remove('text-muted'); event.currentTarget.querySelector('.nav-icon').classList.add('text-primary'); document.getElementById(tabId).classList.add('show', 'active'); 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) { tabsData['requests'] = { title: 'Запросы', icon: 'bi-chat-left-text', description: 'Управление запросами на инструменты' }; } if (accessData.view_services) { tabsData['jurnal_toolkits'] = { title: 'Журнал перемещений', icon: 'bi-journal-text', description: 'Журнал перемещений инструментов' }; } if (accessData.view_requests) { 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); } 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': renderSimpleTab(tabId, tabData, 'Запросы на инструменты'); break; case 'toolkits': renderToolkitsTab(tabId, tabData.toolkits, tabData.categories); break; case 'jurnal_toolkits': renderSimpleTab(tabId, tabData, 'Журнал перемещений'); break; case 'jurnal_service': renderSimpleTab(tabId, tabData, 'Сервисный журнал'); break; case 'users': renderSimpleTab(tabId, tabData, 'Пользователи системы'); break; } } catch (error) { console.error('Error filling tab:', error); const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = ` `; } } function renderSimpleTab(tabId, tabData, title) { const tabContent = document.getElementById(`${tabId}-tab-content`); tabContent.innerHTML = `
${title}
${Object.entries(tabData).map(([key, value]) => `
${key}

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

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

${description}

${tool.quantity_min ? ` Мин: ${tool.quantity_min} ${tool.quantity_min_extra ? `( ${tool.quantity_min_extra})` : ''} ` : ''}
`; }).join(''); const cards = container.querySelectorAll('.toolkit-card'); cards.forEach(card => { card.addEventListener('click', async event => { const toolId = event.currentTarget.dataset.toolid; const tool = tools.find(t => t.id == toolId); await showToolkitDetailsModal(tool); }); }); } // Функция для настройки фильтров function setupFilters(tabId, tools, categoriesMap) { const searchInput = document.getElementById(`${tabId}-search-input`); const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`); // Текущие значения фильтров let currentFilter = { category: 'all', search: '' }; // Обработчик для кнопок категорий filterButtons.forEach(button => { button.addEventListener('click', function () { // Убираем активный класс у всех кнопок filterButtons.forEach(btn => btn.classList.remove('active')); // Добавляем активный класс текущей кнопке this.classList.add('active'); currentFilter.category = this.dataset.category; renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category); }); }); // Обработчик для поля поиска if (searchInput) { let searchTimeout; searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { currentFilter.search = this.value.trim(); renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category); }, 300); }); // Очистка поиска searchInput.insertAdjacentHTML('afterend', ` `); const clearBtn = document.getElementById(`${tabId}-clear-search`); if (clearBtn) { clearBtn.addEventListener('click', function () { searchInput.value = ''; currentFilter.search = ''; renderToolkitCards(tabId, tools, categoriesMap, '', currentFilter.category); this.classList.add('d-none'); }); searchInput.addEventListener('input', function () { clearBtn.classList.toggle('d-none', !this.value); }); } } } 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')); const toolboxNav = `
${tabData.map((toolbox, index) => ` `).join('')}
`; // Создаем контейнер для содержимого склада const toolboxContent = `

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

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

`; tabOptionalContent.innerHTML = toolboxNav; if (accessData.manage_toolboxes) { const addToolboxBtn = document.createElement('button'); addToolboxBtn.className = 'btn btn-outline-success toolbox-nav-btn d-flex align-items-center mb-2'; addToolboxBtn.innerHTML = ` Добавить `; addToolboxBtn.addEventListener('click', function (e) { e.preventDefault(); addToolbox(); }); document.getElementById('toolboxNav').appendChild(addToolboxBtn); } tabContent.innerHTML = toolboxContent; } // Функция для выбора склада window.selectToolbox = async function (toolboxId, index) { // Убираем активный класс со всех кнопок складов document.querySelectorAll('.toolbox-nav-btn').forEach(btn => { btn.classList.remove('active'); }); // Добавляем активный класс выбранной кнопке const selectedBtn = document.querySelector(`.toolbox-nav-btn[data-toolbox-id="${toolboxId}"]`); if (selectedBtn) { selectedBtn.classList.add('active'); } // Загружаем содержимое склада 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}

`; } } async function fillToolbox(toolboxInfo) { const allToolkitsData = await apiRequest('/toolkit/fill_prepare'); if (allToolkitsData.status !== 'ok') { showInfo('Ошибка загрузки данных инструментов', 'error'); return; } // Удаляем старое модальное окно let modal = document.getElementById('fillToolboxModal'); if (modal) modal.remove(); // ============================== // Подготовка данных // ============================== const { toolkits, categories, placements } = allToolkitsData.data; const placementMap = {}; placements .filter(p => p.toolbox_id === toolboxInfo.id) .forEach(p => placementMap[p.toolkit_id] = p.placement); // lower -> category const categoriesByLower = Object.fromEntries(categories.map(c => [c.title.toLowerCase().trim(), c])); const toolkitsMap = Object.fromEntries(toolkits.map(t => [t.id, t])); const toolkitsByCategory = {}; toolkits.forEach(t => { if (!toolkitsByCategory[t.category_id]) toolkitsByCategory[t.category_id] = []; toolkitsByCategory[t.category_id].push(t); }); // lower title -> toolkit within category const toolkitsLowerByCategory = {}; Object.entries(toolkitsByCategory).forEach(([catId, list]) => { const map = {}; list.forEach(t => map[t.title.toLowerCase().trim()] = t); toolkitsLowerByCategory[catId] = map; }); // ============================== // Вспомогательные функции // ============================== const normalize = s => (s || '').toLowerCase().trim(); // exact match by lower title const findCategoryByExact = title => { if (!title) return null; return categoriesByLower[title.toLowerCase().trim()] || null; }; const findToolkitByExact = (categoryId, title) => { if (!categoryId || !title) return null; const map = toolkitsLowerByCategory[categoryId] || {}; return map[title.toLowerCase().trim()] || null; }; // format cost const formatCost = value => { if (typeof value !== 'number') value = parseFloat(value) || 0; return formatPrice(value); }; // debounce function debounce(fn, delay = 200) { let t; return function (...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), delay); }; } // Inject minimal styles for autocomplete dropdown (only once) (function injectStyles() { if (document.getElementById('fillToolboxAutocompleteStyles')) return; const style = document.createElement('style'); style.id = 'fillToolboxAutocompleteStyles'; style.textContent = ` .ft-autocomplete { position: relative; } .ft-autocomplete-list { position: absolute; z-index: 2000; left: 0; right: 0; max-height: 220px; overflow: auto; background: white; border: 1px solid #dee2e6; border-radius: 0.25rem; box-shadow: 0 .25rem .5rem rgba(0,0,0,.08); } .ft-autocomplete-item { padding: .375rem .5rem; cursor: pointer; } .ft-autocomplete-item:hover, .ft-autocomplete-item.active { background: #7abb92ff; } .ft-autocomplete-empty { padding: .375rem .5rem; color: #6c757d; } `; document.head.appendChild(style); })(); // create suggestions list element for an input (returns container and helper functions) function createAutocomplete(forInput) { // wrapper ft-autocomplete should exist around input let wrapper = forInput.closest('.ft-autocomplete'); if (!wrapper) { wrapper = document.createElement('div'); wrapper.className = 'ft-autocomplete'; forInput.parentNode.insertBefore(wrapper, forInput); wrapper.appendChild(forInput); } let list = wrapper.querySelector('.ft-autocomplete-list'); if (!list) { list = document.createElement('div'); list.className = 'ft-autocomplete-list'; list.style.display = 'none'; wrapper.appendChild(list); } function show(items) { list.innerHTML = ''; if (!items || items.length === 0) { const empty = document.createElement('div'); empty.className = 'ft-autocomplete-empty'; empty.textContent = 'Ничего не найдено'; list.appendChild(empty); list.style.display = 'block'; return; } items.forEach(it => { const div = document.createElement('div'); div.className = 'ft-autocomplete-item'; div.textContent = it.title; div.dataset.valueId = it.id; div.addEventListener('mousedown', (e) => { // mousedown чтобы сработало до blur e.preventDefault(); if (typeof wrapper._onSelect === 'function') wrapper._onSelect(it); hide(); }); list.appendChild(div); }); list.style.display = 'block'; } function hide() { list.innerHTML = ''; list.style.display = 'none'; } function onSelect(fn) { wrapper._onSelect = fn; } return { wrapper, list, show, hide, onSelect }; } // ============================== // Создание строки таблицы // ============================== function createRow(rowIndex = 0) { const rowId = `row-${Date.now()}-${rowIndex}`; return `
`; } // ============================== // Создаём модалку // ============================== modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'fillToolboxModal'; modal.tabIndex = -1; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` `; document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); const rowsContainer = modal.querySelector('#fillToolboxRows'); rowsContainer.innerHTML = createRow(0); // ============================== // Автокомплит + логика строки // ============================== // Закрытие всех автокомплитов при клике вне document.addEventListener('click', (e) => { const openLists = document.querySelectorAll('.ft-autocomplete-list'); openLists.forEach(l => { if (!l.parentElement.contains(e.target)) l.style.display = 'none'; }); }); // Список для хранения ссылок на автокомплиты const autocompleteInstances = new Map(); function setupRow(row) { const rowId = row.id; const categoryInput = row.querySelector('.category-input'); const categoryIdInput = row.querySelector('.category-id'); const toolkitInput = row.querySelector('.toolkit-input'); const toolkitIdInput = row.querySelector('.toolkit-id'); const qty = row.querySelector('.quantity'); const price = row.querySelector('.price'); const placement = row.querySelector('.placement'); const costInput = row.querySelector('.cost'); // create autocompletes const catAC = createAutocomplete(categoryInput); const toolAC = createAutocomplete(toolkitInput); // Сохраняем ссылки для возможной очистки autocompleteInstances.set(rowId, { catAC, toolAC }); // CATEGORY behavior const showCategorySuggestions = debounce(() => { const q = normalize(categoryInput.value); let matches; if (!q) { // Если поле пустое - показываем все категории matches = categories .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); } else { // Если есть текст - фильтруем по вхождению matches = categories .filter(c => normalize(c.title).includes(q)) .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); } catAC.show(matches); }, 200); catAC.onSelect((cat) => { categoryIdInput.value = cat.id; categoryInput.value = cat.title; categoryInput.classList.remove('is-invalid'); categoryInput.classList.add('is-valid'); toolkitInput.disabled = false; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; // Фокус на поле инструмента после выбора категории setTimeout(() => toolkitInput.focus(), 10); }); categoryInput.addEventListener('focus', function () { // При фокусе на поле показываем все категории, если поле пустое if (!categoryInput.value.trim()) { showCategorySuggestions(); } }); categoryInput.addEventListener('input', function () { categoryIdInput.value = ''; categoryInput.classList.remove('is-valid', 'is-invalid'); toolkitInput.disabled = true; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; showCategorySuggestions(); }); categoryInput.addEventListener('blur', function () { // Небольшая задержка для обработки клика по автокомплиту setTimeout(() => { const v = categoryInput.value.trim(); if (!v) return; const matched = findCategoryByExact(v); if (matched) { categoryIdInput.value = matched.id; categoryInput.value = matched.title; categoryInput.classList.remove('is-invalid'); categoryInput.classList.add('is-valid'); toolkitInput.disabled = false; toolkitInput.focus(); } else { categoryInput.classList.add('is-invalid'); categoryIdInput.value = ''; toolkitInput.disabled = true; toolkitInput.value = ''; toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; } }, 150); }); // TOOLKIT behavior const showToolkitSuggestions = debounce(() => { const catId = categoryIdInput.value; if (!catId) { toolAC.hide(); return; } const q = normalize(toolkitInput.value); const pool = toolkitsByCategory[catId] || []; let matches; if (!q) { // Если поле пустое - показываем все инструменты в категории matches = pool .sort((a, b) => a.title.localeCompare(b.title)) .map(t => ({ id: t.id, title: t.title })); } else { // Если есть текст - фильтруем по вхождению matches = pool .filter(t => normalize(t.title).includes(q)) .sort((a, b) => a.title.localeCompare(b.title)) .map(t => ({ id: t.id, title: t.title })); } toolAC.show(matches); }, 200); toolAC.onSelect((tool) => { toolkitIdInput.value = tool.id; toolkitInput.value = tool.title; toolkitInput.classList.remove('is-invalid'); toolkitInput.classList.add('is-valid'); qty.disabled = false; price.disabled = false; placement.disabled = false; placement.value = placementMap[tool.id] || ''; // Фокус на поле количества после выбора инструмента setTimeout(() => qty.focus(), 10); }); toolkitInput.addEventListener('focus', function () { // При фокусе на поле показываем все инструменты в категории, если поле пустое const catId = categoryIdInput.value; if (catId && !toolkitInput.value.trim()) { showToolkitSuggestions(); } }); toolkitInput.addEventListener('input', function () { toolkitIdInput.value = ''; toolkitInput.classList.remove('is-valid', 'is-invalid'); qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; showToolkitSuggestions(); }); toolkitInput.addEventListener('blur', function () { setTimeout(() => { const v = toolkitInput.value.trim(); const catId = categoryIdInput.value; if (!v || !catId) return; const matched = findToolkitByExact(catId, v); if (matched) { toolkitIdInput.value = matched.id; toolkitInput.value = matched.title; toolkitInput.classList.remove('is-invalid'); toolkitInput.classList.add('is-valid'); qty.disabled = false; price.disabled = false; placement.disabled = false; placement.value = placementMap[matched.id] || ''; qty.focus(); } else { toolkitInput.classList.add('is-invalid'); toolkitIdInput.value = ''; qty.disabled = true; price.disabled = true; placement.disabled = true; placement.value = ''; costInput.value = '0.00 ₽'; } }, 150); }); // recalc cost function recalc() { const q = parseFloat(qty.value) || 0; const p = parseFloat(price.value.toString().replace(',', '.')) || 0; const c = q * p; costInput.value = formatCost(c) + ' ₽'; updateTotals(); } qty.addEventListener('input', recalc); price.addEventListener('input', recalc); // Фокус на следующее поле при нажатии Enter const handleEnterKey = (currentField, nextField) => { if (nextField && !nextField.disabled) { nextField.focus(); if (nextField === toolkitInput && !nextField.value.trim()) { // Если переходим к пустому полю инструмента, показываем все варианты showToolkitSuggestions(); } } }; categoryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(categoryInput, toolkitInput); } }); toolkitInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(toolkitInput, qty); } }); qty.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(qty, price); } }); price.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleEnterKey(price, placement); } }); placement.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); // При Enter в последнем поле - добавить новую строку modal.querySelector('#addRowBtn').click(); } }); // remove row row.querySelector('.remove-row').addEventListener('click', () => { if (rowsContainer.children.length <= 1) return; // Очищаем автокомплиты const instances = autocompleteInstances.get(row.id); if (instances) { instances.catAC.hide(); instances.toolAC.hide(); autocompleteInstances.delete(row.id); } row.remove(); updateTotals(); // Фокус на первую строку после удаления const firstRow = rowsContainer.querySelector('tr'); if (firstRow) { const firstInput = firstRow.querySelector('.category-input'); if (firstInput) firstInput.focus(); } // Отключаем кнопку удаления, если осталась одна строка if (rowsContainer.children.length === 1) { const btn = rowsContainer.querySelector('.remove-row'); if (btn) btn.disabled = true; } }); } // Инициализируем только первую строку const firstRow = rowsContainer.querySelector('tr'); if (firstRow) setupRow(firstRow); // add row button modal.querySelector('#addRowBtn').addEventListener('click', () => { const newRowHTML = createRow(rowsContainer.children.length); rowsContainer.insertAdjacentHTML('beforeend', newRowHTML); // Настройка новой строки const newRow = rowsContainer.lastElementChild; setupRow(newRow); // Включаем кнопки удаления const removeButtons = rowsContainer.querySelectorAll('.remove-row'); if (removeButtons.length > 1) removeButtons.forEach(b => b.disabled = false); // Фокус на поле категории в новой строке setTimeout(() => { const categoryInput = newRow.querySelector('.category-input'); if (categoryInput) categoryInput.focus(); }, 10); }); // totals function updateTotals() { let totalQty = 0; let totalCost = 0; let filled = 0; rowsContainer.querySelectorAll('tr').forEach(r => { const qty = parseInt(r.querySelector('.quantity')?.value) || 0; const cost = parseFloat((r.querySelector('.cost')?.value || '').replace(/[^0-9.,]/g, '').replace(',', '.')) || 0; if (qty && cost) { totalQty += qty; totalCost += cost; filled++; } }); modal.querySelector('#totalQuantity').textContent = totalQty; modal.querySelector('#totalCost').textContent = formatCost(totalCost) + ' ₽'; modal.querySelector('#totalRowsCount').textContent = filled + ' позиций'; } // form submit with validation + double confirmation like у тебя modal.querySelector('#fillToolboxForm').addEventListener('submit', async function (e) { e.preventDefault(); const reasonInput = modal.querySelector('#fillReason'); const reason = reasonInput.value.trim(); if (reason.length < 10) { showInfo('Укажите обоснование (минимум 10 символов)', 'error'); return; } const rows = rowsContainer.querySelectorAll('tr'); const items = []; let isValid = true; let errorMessage = ''; rows.forEach((row, index) => { if (!isValid) return; const catId = row.querySelector('.category-id').value; const toolId = row.querySelector('.toolkit-id').value; const qty = row.querySelector('.quantity').value; const price = row.querySelector('.price').value; // Strict: category and toolkit must match exact list entries if (!catId) { isValid = false; errorMessage = `Выберите категорию из списка в строке ${index + 1}`; row.querySelector('.category-input').classList.add('is-invalid'); row.querySelector('.category-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (!toolId) { isValid = false; errorMessage = `Выберите инструмент из списка в строке ${index + 1}`; row.querySelector('.toolkit-input').classList.add('is-invalid'); row.querySelector('.toolkit-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (!qty || !price) { isValid = false; errorMessage = `Заполните все обязательные поля в строке ${index + 1}`; if (!qty) row.querySelector('.quantity').classList.add('is-invalid'); if (!price) row.querySelector('.price').classList.add('is-invalid'); row.querySelector('.quantity').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } items.push({ toolkit_id: parseInt(toolId), quantity: parseInt(qty), price: parseFloat(price.toString().replace(',', '.')), placement: row.querySelector('.placement').value || null }); }); if (!isValid) { document.getElementById('fillToolboxErrorMessage').textContent = errorMessage; document.getElementById('fillToolboxError').classList.remove('d-none'); document.getElementById('fillToolboxError').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } if (items.length === 0) { document.getElementById('fillToolboxErrorMessage').textContent = 'Добавьте хотя бы одну позицию'; document.getElementById('fillToolboxError').classList.remove('d-none'); return; } const submitBtn = modal.querySelector('#submitFillBtn'); const spinner = modal.querySelector('#submitFillSpinner'); const submitText = modal.querySelector('#submitFillText'); const originalText = submitText.textContent; submitText.textContent = 'Нажмите еще раз для подтверждения (10 сек)'; submitBtn.disabled = true; let confirmed = false; const timeout = setTimeout(() => { if (!confirmed) { submitText.textContent = originalText; submitBtn.disabled = false; document.getElementById('fillToolboxError').classList.add('d-none'); } }, 10000); const confirmHandler = async function (ev) { ev.preventDefault(); confirmed = true; clearTimeout(timeout); submitBtn.disabled = true; spinner.style.display = 'inline-block'; try { const response = await apiRequest('/toolbox/fill', { toolboxId: toolboxInfo.id, items: items, reason: reason, userId: userData.id }, 'POST'); if (response.status === 'ok') { showInfo('Склад успешно пополнен', 'success'); bsModal.hide(); await loadToolboxContent(toolboxInfo.id); } else { throw new Error(response.message || 'Ошибка при пополнении склада'); } } catch (error) { console.error('Ошибка при пополнении склада:', error); document.getElementById('fillToolboxErrorMessage').textContent = error.message || 'Произошла ошибка при пополнении склада'; document.getElementById('fillToolboxError').classList.remove('d-none'); submitBtn.disabled = false; spinner.style.display = 'none'; submitText.textContent = originalText; } submitBtn.removeEventListener('click', confirmHandler); }; submitBtn.addEventListener('click', confirmHandler, { once: true }); submitBtn.disabled = false; }); // cleanup on modal hide modal.addEventListener('hidden.bs.modal', () => { autocompleteInstances.clear(); setTimeout(() => { if (modal.parentNode) modal.remove(); }, 300); }); // Простой таймер для восстановления фокуса let focusRestoreTimer = null; function restoreFocusIfLost() { if (modal && bsModal._isShown) { const activeElement = document.activeElement; const isFocusInModal = modal.contains(activeElement); if (!isFocusInModal && activeElement !== document.body) { // Фокус вне модалки - не восстанавливаем return; } if (!isFocusInModal || activeElement === document.body) { // Фокус потерян или на body - восстанавливаем const firstInput = rowsContainer.querySelector('.category-input'); if (firstInput && firstInput.offsetParent !== null) { firstInput.focus(); } } } } // Показываем модалку bsModal.show(); // Устанавливаем фокус после полного отображения модалки modal.addEventListener('shown.bs.modal', () => { setTimeout(() => { const firstCategoryInput = rowsContainer.querySelector('.category-input'); if (firstCategoryInput) { firstCategoryInput.focus(); // Показываем все категории при первом фокусе const firstRow = rowsContainer.querySelector('tr'); if (firstRow) { const catAC = autocompleteInstances.get(firstRow.id)?.catAC; if (catAC) { const matches = categories .sort((a, b) => a.title.localeCompare(b.title)) .map(c => ({ id: c.id, title: c.title })); catAC.show(matches); } } } // Запускаем периодическую проверку фокуса focusRestoreTimer = setInterval(restoreFocusIfLost, 1000); }, 100); }); // Останавливаем таймер при закрытии модалки modal.addEventListener('hidden.bs.modal', () => { if (focusRestoreTimer) { clearInterval(focusRestoreTimer); focusRestoreTimer = null; } }); } async function deleteToolbox(toolboxId) { // Находим информацию о складе const toolboxInfo = currentToolboxData.find(t => t.id === toolboxId); if (!toolboxInfo) { showInfo('Склад не найден', 'error'); return; } // Проверяем, существует ли уже модальное окно let modal = document.getElementById('deleteToolboxModal'); if (modal) { modal.remove(); } // Создаем модальное окно подтверждения modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'deleteToolboxModal'; modal.tabIndex = -1; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` `; document.body.appendChild(modal); // Инициализация модального окна const bsModal = new bootstrap.Modal(modal); // Получаем элементы const confirmCheckbox = modal.querySelector('#confirmDeleteCheckbox'); const confirmBtn = modal.querySelector('#confirmDeleteBtn'); const deleteSpinner = modal.querySelector('#deleteSpinner'); // Активация кнопки при подтверждении confirmCheckbox.addEventListener('change', function () { confirmBtn.disabled = !this.checked; }); // Обработчик кнопки удаления confirmBtn.addEventListener('click', async function () { if (!confirmCheckbox.checked) return; // Показываем индикатор загрузки и отключаем кнопку confirmBtn.disabled = true; deleteSpinner.style.display = 'inline-block'; try { // Отправляем запрос на удаление const userId = userData.id; const resp = await apiRequest('/toolbox/', { toolboxId, userId }, 'DELETE'); // Проверяем успешность запроса if (resp.status == 'ok') { // Успешное удаление bsModal.hide(); showInfo('Склад успешно удален', 'success'); await uploadTab('toolbox'); } else { // Обработка ошибок от сервера let errorMessage = 'Не удалось удалить склад'; if (resp.message) { errorMessage += ': ' + resp.message; } // Показываем конкретное сообщение об ошибке showInfo(errorMessage, 'error'); // Возвращаем кнопку в исходное состояние confirmBtn.disabled = false; deleteSpinner.style.display = 'none'; } } catch (error) { console.error('Ошибка при удалении склада:', error); // Возвращаем кнопку в исходное состояние confirmBtn.disabled = false; deleteSpinner.style.display = 'none'; // Показываем общее сообщение об ошибке showInfo('Произошла ошибка при удалении склада. Попробуйте еще раз.', 'error'); } }); // Очистка при закрытии модального окна modal.addEventListener('hidden.bs.modal', () => { // Удаляем модальное окно из DOM setTimeout(() => { if (modal.parentNode) { modal.remove(); } }, 300); }); // Показываем модальное окно bsModal.show(); } // Функция обработки данных склада function processToolboxData(toolboxData, toolboxId, quantityMonitoring) { const { stocks, toolkits, categories } = toolboxData; // Создаем мапы для быстрого доступа const toolkitMap = {}; const categoryMap = {}; toolkits.forEach(toolkit => { toolkitMap[toolkit.id] = toolkit; }); categories.forEach(category => { categoryMap[category.id] = category; }); // Группируем стоки по инструментам const groupedStocks = {}; stocks.forEach(stock => { if (stock.toolbox_id !== toolboxId) return; const toolkitId = stock.toolkit_id; if (!groupedStocks[toolkitId]) { groupedStocks[toolkitId] = { stocks: [], placements: new Set() }; } groupedStocks[toolkitId].stocks.push(stock); if (stock.placement) { groupedStocks[toolkitId].placements.add(stock.placement); } }); // Формируем итоговый массив const result = []; Object.keys(groupedStocks).forEach(toolkitId => { const toolkit = toolkitMap[toolkitId]; if (!toolkit) return; const group = groupedStocks[toolkitId]; const category = categoryMap[toolkit.category_id]; // Рассчитываем общие показатели const totalQuantity = group.stocks.reduce((sum, stock) => sum + stock.quantity, 0); const totalCost = group.stocks.reduce((sum, stock) => sum + (stock.quantity * stock.price), 0); // Определяем статус достаточности let indicator = null; if (quantityMonitoring) { if (totalQuantity >= toolkit.quantity_min) { indicator = { text: 'Достаточно', class: 'success' }; } else if (totalQuantity >= toolkit.quantity_min_extra) { indicator = { text: 'Мало', class: 'warning' }; } else { indicator = { text: 'Критически мало', class: 'danger' }; } } // Формируем расположение let placement = group.placements.size > 0 ? Array.from(group.placements).join(', ') : 'Своб. расположение'; // Находим дату последнего изменения const lastUpdated = group.stocks.reduce((latest, stock) => { const stockDate = new Date(stock.updated_at); return stockDate > latest ? stockDate : latest; }, new Date(0)); result.push({ id: parseInt(toolkitId), toolboxId: toolboxId, image: toolkit.image?.main || 'static/images/tools/default.png', images: toolkit.image?.additional || [], title: toolkit.title, category: category?.title || 'Без категории', totalQuantity: totalQuantity, indicator: indicator, totalCost: totalCost, // Сохраняем число, форматируем при выводе placement: placement, lastUpdated: lastUpdated.toLocaleString('ru-RU'), available: totalQuantity, // для проверки при операциях toolkitData: toolkit, // для модального окна categoryData: category // для модального окна }); }); return result; } // Функция форматирования стоимости с разделителями тысяч function formatPrice(price) { return parseFloat(price).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); } // Функция инициализации таблицы async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) { let currentPage = 1; const itemsPerPage = 20; let currentSort = { field: 'title', direction: 'asc' }; let filteredData = [...data]; // Инициализация пагинации async function initializePagination() { const totalPages = Math.ceil(filteredData.length / itemsPerPage); const paginationContainer = document.getElementById('toolboxPagination'); const tbody = document.getElementById('toolboxItemsBody'); // Очищаем текущее содержимое paginationContainer.innerHTML = ''; // Добавляем кнопки пагинации const prevBtn = document.createElement('li'); prevBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; prevBtn.innerHTML = `Назад`; prevBtn.addEventListener('click', (e) => { e.preventDefault(); if (currentPage > 1) { currentPage--; renderTable(); } }); paginationContainer.appendChild(prevBtn); // Определяем диапазон отображаемых страниц let startPage = Math.max(1, currentPage - 2); let endPage = Math.min(totalPages, currentPage + 2); for (let i = startPage; i <= endPage; i++) { const pageItem = document.createElement('li'); pageItem.className = `page-item ${i === currentPage ? 'active' : ''}`; pageItem.innerHTML = `${i}`; pageItem.addEventListener('click', (e) => { e.preventDefault(); currentPage = i; renderTable(); }); paginationContainer.appendChild(pageItem); } const nextBtn = document.createElement('li'); nextBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; nextBtn.innerHTML = `Вперед`; nextBtn.addEventListener('click', (e) => { e.preventDefault(); if (currentPage < totalPages) { currentPage++; renderTable(); } }); paginationContainer.appendChild(nextBtn); // Рендерим данные текущей страницы await renderTable(); } // Функция рендеринга таблицы async function renderTable() { const tbody = document.getElementById('toolboxItemsBody'); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const pageData = filteredData.slice(startIndex, endIndex); tbody.innerHTML = ''; pageData.forEach(item => { const tr = document.createElement('tr'); tr.dataset.id = item.id; tr.dataset.quantity = item.totalQuantity; // Определяем, какие кнопки показывать let actionButtons = ''; if (toolboxOwn === 'Мой склад' || toolboxOwn === 'Склад сотрудника') { actionButtons = `
`; } else if (toolboxOwn === 'Общий склад' && accessData.available_own_toolbox) { actionButtons = `
`; } tr.innerHTML = ` ${item.title} ${item.title}
${actionButtons} ${item.category} ${item.totalQuantity} ${quantityMonitoring ? `${item.indicator?.text || '-'}` : ''} ${formatPrice(item.totalCost)} ₽ ${toolboxOwn === 'Общий склад' ? `${item.placement}` : ''} ${item.lastUpdated} `; tbody.appendChild(tr); // Добавляем обработчики для кнопок в строке const actionBtn = tr.querySelector('.action-buttons'); if (actionBtn) { actionBtn.querySelectorAll('button[data-action]').forEach(button => { button.addEventListener('click', async (e) => { e.stopPropagation(); const action = e.currentTarget.dataset.action; const itemId = e.currentTarget.dataset.id; const selectedItem = data.find(d => d.id == itemId); if (selectedItem) { await showOperationModal(action, selectedItem); } }); }); } }); // Добавляем обработчики для изображений document.querySelectorAll('.toolkit-image-link').forEach(link => { link.addEventListener('click', async (e) => { e.preventDefault(); const itemId = e.currentTarget.dataset.id; const item = data.find(d => d.id == itemId); if (item) { await showToolkitDetailsModal(item); } }); }); } 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(item) { const modalId = 'toolkitDetailsModal'; let modal = document.getElementById(modalId); if (modal) { modal.remove(); } modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = modalId; modal.tabIndex = -1; let images = []; if (typeof item.image === 'string') { images = item.image ? [item.image, ...(item.images || [])] : [item.image]; } else { images = item.image ? [item.image.main, ...(item.image.additional || [])] : [item.image.main];; } let imagesDiv = ''; if (images.length > 1) { const carouselId = `carousel-${item.id}`; imagesDiv = `
`; } else { imagesDiv = `
${item.title}
`; } const description = item.description || item.toolkitData?.description || 'Нет описания'; const specifications = item.toolkitData?.specifications || item.specifications || {}; const external_link = item.external_link || item.toolkitData?.external_link || null; const category_desc = item.categoryData?.description || item.category_desc || ''; const toolkitStocks = await getToolkitStocks(item.id); modal.innerHTML = ` `; // Добавляем обработчики для кнопок в строке modal.querySelectorAll('button[data-action]').forEach(button => { button.addEventListener('click', async (e) => { e.stopPropagation(); const action = e.currentTarget.dataset.action; const id = e.currentTarget.dataset.id; const toolboxId = e.currentTarget.dataset.toolbox_id; const available = e.currentTarget.dataset.available; const totalQuantity = available; const title = item.title; const totalCost = e.currentTarget.dataset.totalcost; const skipRefresh = true; const selectedItem = { id, toolboxId, available, totalQuantity, title, totalCost, skipRefresh }; await showOperationModal(action, selectedItem); modal.querySelector('button[data-bs-dismiss="modal"]').click(); }); }); document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); bsModal.show(); modal.addEventListener('hidden.bs.modal', () => { modal.remove(); }); // После создания модального окна добавьте инициализацию lightbox lightbox.option({ 'resizeDuration': 200, 'wrapAround': true, 'albumLabel': "Изображение %1 из %2", 'fadeDuration': 300, 'imageFadeDuration': 300 }); } // Функция показа модального окна для операций 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 formatKey(key) { const keyMap = { 'id': 'ID', 'title': 'Название', 'description': 'Описание', 'owner_id': 'ID владельца', 'monitoring': 'Мониторинг', 'created_at': 'Дата создания', 'updated_at': 'Дата обновления' }; return keyMap[key] || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' '); } function formatValue(value) { if (value === null || value === undefined) return '—'; if (typeof value === 'boolean') return value ? 'Да' : 'Нет'; if (typeof value === 'object') return JSON.stringify(value); return value.toString(); } document.addEventListener('DOMContentLoaded', async () => { await getCookieData(); if (!accessData || !userData) { console.warn('Access data or user data not found'); return; } prepareTabs(); }); window.openTab = openTab;