import { getCookie } from '/static/js/cookies.js';
import { apiRequest } from '/static/js/api.js';
import { showInfo } from '/static/js//toast.js';
let accessData;
let userData;
let currentToolboxData = null;
// Список предопределенных характеристик с разделами
const predefinedSpecs = {
"Универсальные": [
"Диаметр",
"Длина",
"Черновая",
"Чистовая",
"Материал инструмента",
"Покрытие (TiN, TiAlN, AlTiN)",
"Тип хвостовика",
"Назначение",
"По стали",
"По нержавейке",
"По алюминию",
"Твёрдый сплав",
"HSS"
],
"Фрезеровка": [
"Кол-во перьев",
"Тип фрезы (концевая, торцевая, черновая)",
"Угол спирали",
"Геометрия зубьев"
],
"Токарка": [
"Пластины",
"Форма пластины (C, D, V, W, T)",
"Радиус",
"Наружная",
"Внутренняя",
"Резьбовая",
"Шаг",
"Тип державки",
"Направление (правое/левое)",
"Система крепления"
],
"Сверла": [
"Угол заточки (118°, 135°)",
"Тип хвостовика (цилиндрический, Морзе)",
"Глубокое сверление"
],
"Инструмент для ЧПУ": [
"Тип инструмента (фреза, сверло, развертка, гравёр, фасочник)",
"Тип обработки (2D, 3D, контурная, карманная)",
"Ступенчатая геометрия",
"Тип крепления (ER, Weldon, HSK, BT, ISO)",
"Максимальные обороты",
"Максимальная подача",
"Допуск биения",
"Тип спирали (правосторонняя, левосторонняя)",
"Длина режущей части",
"Рабочая часть"
]
};
async function getCookieData() {
accessData = await getCookie('toolbox_access');
userData = await getCookie('toolbox_user');
}
async function openTab(event, tabId) {
// Убираем активный класс со всех вкладок и кнопок
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
btn.classList.remove('active');
btn.querySelector('.nav-icon').classList.remove('text-primary');
btn.querySelector('.nav-icon').classList.add('text-muted');
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
// Добавляем активный класс выбранной вкладке и кнопке
event.currentTarget.classList.add('active');
event.currentTarget.querySelector('.nav-icon').classList.remove('text-muted');
event.currentTarget.querySelector('.nav-icon').classList.add('text-primary');
document.getElementById(tabId).classList.add('show', 'active');
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], index) => `
${tabData.title}
`).join('')}
${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 = `
Ошибка при загрузке данных: ${error.message}
`;
}
}
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 = `
Внимание! Категория может быть удалена только если в ней нет инструментов.
При попытке удаления категории, используемой в инструментах, вы получите увдомление об успехе,
при этом категория не будет удалена!
Название: ${escapeHtml(categories[index].title)}
Описание: ${escapeHtml(categories[index].description)}
`;
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;
if (accessData.tools_creation) {
tabOptionalContent.innerHTML = `
`;
const manageCategoryBtn = document.getElementById('manageCategoryBtn');
manageCategoryBtn.addEventListener('click', () => manageCategory(categoriesArray));
const addToolBtn = document.getElementById('addToolBtn');
addToolBtn.addEventListener('click', () => manageToolkit());
} else {
tabOptionalContent.innerHTML = `
`;
}
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 => `
${category.title}
`).join('')}
`;
// Рендерим карточки
renderToolkitCards(tabId, toolsList, categoriesData);
// Добавляем обработчики событий для фильтров
setupFilters(tabId, toolsList, categoriesData);
}
// Функция для рендеринга карточек
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all', showHiddenTools = false) {
const container = document.getElementById(`${tabId}-cards-container`);
// Фильтруем инструменты
const filteredTools = tools.filter(tool => {
// Показываем скрытые инструменты, если флаг установлен
const showHidden = showHiddenTools || !tool.hidden;
// Фильтр по категории
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 && showHidden;
});
// Рендерим карточки
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 `
`;
}).join('');
const cards = container.querySelectorAll('.toolkit-card');
cards.forEach(card => {
card.addEventListener('click', async event => {
const toolId = event.currentTarget.dataset.toolid;
await showToolkitDetailsModal(toolId);
});
});
}
// Функция для настройки фильтров
function setupFilters(tabId, tools, categoriesMap) {
const searchInput = document.getElementById(`${tabId}-search-input`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`);
const showHiddenToolsCheckbox = document.getElementById(`showHiddenTools`);
showHiddenToolsCheckbox.addEventListener('change', () => {
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category, showHiddenToolsCheckbox.checked);
});
// Текущие значения фильтров
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 = `
`;
// Создаем контейнер для содержимого склада
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 = `
Склад пуст
В этом складе нет инструментов
Удалить склад
`;
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 = `
`;
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} ${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;
await showToolkitDetailsModal(itemId);
});
});
}
function parseDate(d) {
// d = "07.12.2025, 13:19:20"
const [datePart, timePart] = d.split(', ');
const [day, month, year] = datePart.split('.').map(Number);
const [hour, minute, second] = timePart.split(':').map(Number);
return new Date(year, month - 1, day, hour, minute, second);
}
// Функция сортировки
function sortData(field, direction) {
filteredData.sort((a, b) => {
let aValue = a[field];
let bValue = b[field];
// Для числовых полей
if (field === 'totalQuantity') {
aValue = parseFloat(aValue);
bValue = parseFloat(bValue);
}
// Для стоимости
if (field === 'totalCost') {
aValue = parseFloat(a.totalCost);
bValue = parseFloat(b.totalCost);
}
// Для дат
if (field === 'lastUpdated') {
aValue = parseDate(a.lastUpdated);
bValue = parseDate(b.lastUpdated);
}
// Для статуса
if (field === 'indicator') {
const order = { 'danger': 0, 'warning': 1, 'success': 2 };
aValue = order[a.indicator?.class] || 3;
bValue = order[b.indicator?.class] || 3;
}
if (direction === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
}
// Функция фильтрации
async function filterData(searchText) {
if (!searchText.trim()) {
filteredData = [...data];
} else {
const searchLower = searchText.toLowerCase();
filteredData = data.filter(item =>
item.title.toLowerCase().includes(searchLower) ||
item.toolkitData.description.toLowerCase().includes(searchLower)
);
}
currentPage = 1;
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
async function filterIndicator() {
const searchLower = 'мало';
filteredData = data.filter(item =>
(item.indicator?.text && item.indicator.text.toLowerCase().includes(searchLower))
);
currentPage = 1;
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
// Инициализация сортировки по заголовкам
document.querySelectorAll('#toolboxItemsTable th[data-sort]').forEach(th => {
th.addEventListener('click', async () => {
const field = th.dataset.sort;
if (currentSort.field === field) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.field = field;
currentSort.direction = 'asc';
}
// Обновляем иконки сортировки
document.querySelectorAll('#toolboxItemsTable th i').forEach(icon => {
icon.className = 'bi bi-arrow-down-up';
});
const currentIcon = th.querySelector('i');
currentIcon.className = currentSort.direction === 'asc' ?
'bi bi-arrow-up' : 'bi bi-arrow-down';
sortData(currentSort.field, currentSort.direction);
await initializePagination();
});
});
// Инициализация поиска
const searchInput = document.getElementById('toolboxSearch');
searchInput.addEventListener('input', async (e) => {
await filterData(e.target.value);
});
// Инициализация кнопки сброса фильтра
document.getElementById('resetFilter').addEventListener('click', async () => {
searchInput.value = '';
searchInput.placeholder = 'Поиск по всем полям...';
await filterData('');
});
// Инициализация кнопки "мало"
try {
document.getElementById('notEnoughBtn').addEventListener('click', async () => {
const showAll = 'Сбросить фильтр -->';
if (searchInput.placeholder === showAll) {
searchInput.placeholder = 'Поиск по всем полям...';
searchInput.value = '';
await filterData('');
return;
}
searchInput.value = '';
searchInput.placeholder = showAll;
await filterIndicator();
});
} catch (_) { }
// Начальная инициализация
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
async function getToolkitStocks(toolkitId) {
const userId = userData.id;
const allToolboxes = accessData.view_all_toolboxes;
const resp = await apiRequest('/toolkit/', { toolkitId, userId, allToolboxes });
return resp.data;
}
// Функция показа модального окна с деталями инструмента
async function showToolkitDetailsModal(toolkitId) {
const modalId = 'toolkitDetailsModal';
let modal = document.getElementById(modalId);
if (modal) {
modal.remove();
}
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = modalId;
modal.tabIndex = -1;
let toolkitData = {};
const toolkiResponse = await apiRequest('/toolkit/', { toolkitId }, 'GET');
if (toolkiResponse.status === 'ok') {
toolkitData = toolkiResponse.data;
} else {
showInfo('Произошла ошибка', 'error');
return;
}
let categories = {};
try {
const categoriesResponse = await apiRequest('/toolkit/categories', {}, 'GET');
if (categoriesResponse.status === 'ok') {
categories = categoriesResponse.data;
}
} catch (error) {
console.error('Ошибка загрузки категорий:', error);
}
const categoryData = categories[toolkitData.category_id];
const images = toolkitData.image ?
[toolkitData.image.main, ...(toolkitData.image.additional || [])] :
[toolkitData.image?.main || ''];;
let imagesDiv = '';
if (images.length > 1) {
const carouselId = `carousel-${toolkitData.id}`;
imagesDiv = `
${images.map((img, index) => `
`).join('')}
Предыдущее
Следующее
${images.map((_, index) => `
`).join('')}
`;
} else {
imagesDiv = images[0] ? `
` : '
';
}
// Переменная для хранения данных об остатках (будет загружена при раскрытии аккордеона)
let toolkitStocksData = null;
let isStocksLoading = false;
modal.innerHTML = `
${imagesDiv}
Описание:
${toolkitData.description}
Категория:
${categoryData.title} - ${categoryData.description}
${Object.keys(toolkitData.specifications).length > 0 ? `
Характеристики:
${Object.entries(toolkitData.specifications).map(([key, value]) => `
${key}:
${value}
`).join('')}
` : ''}
Загрузка...
Загрузка данных об остатках...
Не удалось загрузить данные об остатках
${toolkitData.external_link ? `
` : ''}
`;
if (!toolkitData.hidden) {
if (accessData.tools_edit) {
const footer = modal.querySelector('.modal-footer');
const editButton = document.createElement('button');
editButton.className = 'btn btn-outline-primary';
editButton.textContent = 'Редактировать';
footer.prepend(editButton);
editButton.addEventListener('click', async () => {
try {
modal.querySelector('.btn-close').click();
await manageToolkit(toolkitData, categories, 'update');
} catch (error) {
console.error(error);
}
});
}
if (accessData.tools_creation) {
const footer = modal.querySelector('.modal-footer');
const dubleButton = document.createElement('button');
dubleButton.className = 'btn btn-outline-success';
dubleButton.textContent = 'Скопировать';
footer.prepend(dubleButton);
dubleButton.addEventListener('click', async () => {
try {
modal.querySelector('.btn-close').click();
await manageToolkit(toolkitData, categories, 'copy');
} catch (error) {
console.error(error);
}
});
}
if (accessData.tools_delete) {
const footer = modal.querySelector('.modal-footer');
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-outline-danger';
deleteButton.textContent = 'Удалить';
footer.prepend(deleteButton);
deleteButton.addEventListener('click', async () => {
try {
modal.querySelector('.btn-close').click();
await deleteToolkit(toolkitData);
} catch (error) {
console.error(error);
}
});
}
} else {
if (accessData.tools_delete) {
const footer = modal.querySelector('.modal-footer');
const showButton = document.createElement('button');
showButton.className = 'btn btn-outline-danger';
showButton.textContent = 'Показывать';
footer.prepend(showButton);
showButton.addEventListener('click', async () => {
try {
// Подготавливаем данные для отправки
const formData = { toolkitId: toolkitData.id, userId: userData.id, hidden: false };
setTimeout(() => {
modal.querySelector('.btn-close').click();
}, 300);
// Отправляем запрос на отображение
const response = await apiRequest('/toolkit/hide', formData, 'POST');
// Показываем результат
if (response.status === 'ok') {
// Успешное отображение
// Обновляем список инструментов
if (typeof uploadTab === 'function') {
await uploadTab('toolkits');
}
showInfo(response.message || 'Инструмент успешно отображен', 'success');
} else {
// Ошибка при отображении
showInfo(response.message || 'Произошла ошибка при отображении инструмента', 'danger');
}
} catch (error) {
console.error('Ошибка при скрытии инструмента:', error);
}
});
}
}
// Добавляем модальное окно в DOM
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// Функция для загрузки данных об остатках
const loadToolkitStocks = async () => {
if (isStocksLoading) return;
const stocksLoading = modal.querySelector('#stocksLoading');
const stocksContent = modal.querySelector('#stocksContent');
const stocksError = modal.querySelector('#stocksError');
try {
isStocksLoading = true;
// Показываем спиннер, скрываем контент и ошибку
stocksLoading.classList.remove('d-none');
stocksContent.classList.add('d-none');
stocksError.classList.add('d-none');
// Загружаем данные
toolkitStocksData = await getToolkitStocks(toolkitData.id);
if (toolkitStocksData) {
// Формируем HTML для остатков
let stocksHtml = '';
if (toolkitStocksData.count > 0) {
stocksHtml = `
Общее количество: ${toolkitStocksData.count} шт.
Склад
Количество
Расположение
${Object.entries(toolkitStocksData.toolboxes || {}).map(([key, value]) => `
${key}
${value.count} шт.
${value.placement || ''}
${!toolkitData.hidden && value.id && accessData.available_own_toolbox ? `
` : ''}
`).join('')}
`;
} else {
stocksHtml = `
На складах отсутствуют остатки этого инструмента
`;
}
// Вставляем HTML и показываем контент
stocksContent.innerHTML = stocksHtml;
stocksContent.classList.remove('d-none');
// Добавляем обработчики для кнопок получения
stocksContent.querySelectorAll('.get-stock-btn').forEach(button => {
button.addEventListener('click', async (e) => {
e.stopPropagation();
const action = 'get';
const id = e.currentTarget.dataset.id;
const toolboxId = e.currentTarget.dataset.toolboxId;
const available = e.currentTarget.dataset.available;
const totalQuantity = available;
const title = toolkitData.title;
const totalCost = e.currentTarget.dataset.totalcost;
const skipRefresh = true;
const selectedItem = {
id,
toolboxId,
available,
totalQuantity,
title,
totalCost,
skipRefresh
};
await showOperationModal(action, selectedItem);
modal.querySelector('button[data-bs-dismiss="modal"]').click();
});
});
} else {
throw new Error('Нет данных об остатках');
}
} catch (error) {
console.error('Ошибка при загрузке остатков:', error);
stocksError.classList.remove('d-none');
} finally {
stocksLoading.classList.add('d-none');
isStocksLoading = false;
}
};
// Обработчик события раскрытия аккордеона
const stocksCollapse = modal.querySelector('#stocksCollapse');
stocksCollapse.addEventListener('show.bs.collapse', async () => {
// Загружаем данные только если они еще не загружены
if (!toolkitStocksData && !isStocksLoading) {
await loadToolkitStocks();
}
});
// Обработчик для принудительной перезагрузки данных (например, при повторном открытии аккордеона)
const stocksHeading = modal.querySelector('#stocksHeading');
stocksHeading.addEventListener('click', async (e) => {
// Если данные уже загружены, можно обновить их при повторном клике
const isExpanded = stocksCollapse.classList.contains('show');
if (isExpanded && toolkitStocksData) {
// Можно добавить кнопку обновления или обновлять автоматически
// Для простоты пока оставляем как есть
}
});
// Очистка при закрытии модалки
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
// Инициализация lightbox
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 = `
Изображения
Основное изображение
${mainImagePreview ? `
${mainImageFile ? 'Готово к загрузке' : 'Основное изображение загружено'}
` : `
Перетащите изображение сюда
или кликните для выбора файла
JPG, PNG до 5MB
`}
Дополнительные изображения
Добавить
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
// Получаем элементы DOM
const mainImageDropZone = modal.querySelector('#mainImageDropZone');
const mainImageInput = modal.querySelector('#mainImageInput');
const mainImageContent = modal.querySelector('#mainImageContent');
const removeMainImageBtn = modal.querySelector('#removeMainImageBtn');
const additionalImagesContainer = modal.querySelector('#additionalImagesContainer');
const addAdditionalImageBtn = modal.querySelector('#addAdditionalImageBtn');
const additionalImagesDropZone = modal.querySelector('#additionalImagesDropZone');
const additionalImagesInput = modal.querySelector('#additionalImagesInput');
const specificationsList = modal.querySelector('#specificationsList');
const addSpecBtn = modal.querySelector('#addSpecBtn');
const submitBtn = modal.querySelector('#submitToolkitBtn');
const spinner = modal.querySelector('#submitToolkitSpinner');
const submitText = modal.querySelector('#submitToolkitText');
const errorDiv = modal.querySelector('#manageToolkitError');
const errorMessage = modal.querySelector('#manageToolkitErrorMessage');
// Функция для отображения ошибок
function showError(message) {
errorMessage.textContent = message;
errorDiv.classList.remove('d-none');
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Функция для скрытия ошибок
function hideError() {
errorDiv.classList.add('d-none');
}
// Функция для преобразования файла в base64
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
// Функция для обновления превью основного изображения
function updateMainImagePreview() {
if (mainImagePreview) {
mainImageContent.innerHTML = `
${mainImageFile ? 'Готово к загрузке' : 'Основное изображение загружено'}
`;
// Добавляем обработчик для кнопки удаления
const newRemoveBtn = mainImageContent.querySelector('#removeMainImageBtn');
newRemoveBtn.addEventListener('click', removeMainImage);
} else {
mainImageContent.innerHTML = `
Перетащите изображение сюда
или кликните для выбора файла
JPG, PNG до 5MB
`;
}
}
// Функция для удаления основного изображения
function removeMainImage() {
mainImageFile = null;
mainImagePreview = '';
mainImagePreview = data.image.main; // Возвращаем оригинальное изображение если было
updateMainImagePreview();
}
// Обработчики для основного изображения
mainImageDropZone.addEventListener('click', () => mainImageInput.click());
mainImageDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
mainImageDropZone.style.backgroundColor = '#f8f9fa';
});
mainImageDropZone.addEventListener('dragleave', () => {
mainImageDropZone.style.backgroundColor = '';
});
mainImageDropZone.addEventListener('drop', async (e) => {
e.preventDefault();
mainImageDropZone.style.backgroundColor = '';
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
mainImageFile = file;
mainImagePreview = URL.createObjectURL(file);
updateMainImagePreview();
} else {
showError('Пожалуйста, выберите файл изображения');
}
}
});
mainImageInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
showError('Размер файла не должен превышать 5MB');
return;
}
mainImageFile = file;
mainImagePreview = URL.createObjectURL(file);
updateMainImagePreview();
}
e.target.value = '';
});
// Функция для обновления превью дополнительных изображений
function updateAdditionalImages() {
additionalImagesContainer.innerHTML = '';
if (additionalImages.length > 0) {
additionalImagesDropZone.classList.add('d-none');
additionalImages.forEach((image, index) => {
const isNewFile = image.isFile && image.isNew;
const imgElement = document.createElement('div');
imgElement.className = 'mb-3';
imgElement.innerHTML = `
${isNewFile ? 'Готово к загрузке' : 'Изображение загружено'}
${isNewFile && image.file ? `
${image.file.name}
` : ''}
`;
additionalImagesContainer.appendChild(imgElement);
});
} else {
additionalImagesDropZone.classList.remove('d-none');
}
}
// Функция для удаления дополнительного изображения
window.removeAdditionalImage = function (index) {
// Освобождаем blob URL если это новый файл
const image = additionalImages[index];
if (image.isFile && image.isNew && image.preview.startsWith('blob:')) {
URL.revokeObjectURL(image.preview);
}
additionalImages.splice(index, 1);
updateAdditionalImages();
};
// Обработчики для дополнительных изображений
addAdditionalImageBtn.addEventListener('click', () => additionalImagesInput.click());
additionalImagesDropZone.addEventListener('click', () => additionalImagesInput.click());
additionalImagesDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
additionalImagesDropZone.style.backgroundColor = '#f8f9fa';
});
additionalImagesDropZone.addEventListener('dragleave', () => {
additionalImagesDropZone.style.backgroundColor = '';
});
additionalImagesDropZone.addEventListener('drop', async (e) => {
e.preventDefault();
additionalImagesDropZone.style.backgroundColor = '';
const files = Array.from(e.dataTransfer.files);
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (imageFiles.length > 0) {
for (const file of imageFiles) {
if (file.size > 5 * 1024 * 1024) {
showError(`Файл ${file.name} превышает 5MB`);
continue;
}
additionalImages.push({
preview: URL.createObjectURL(file),
originalUrl: null,
isNew: true,
isFile: true,
file: file
});
}
updateAdditionalImages();
} else {
showError('Пожалуйста, выберите файлы изображений');
}
});
additionalImagesInput.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (imageFiles.length > 0) {
for (const file of imageFiles) {
if (file.size > 5 * 1024 * 1024) {
showError(`Файл ${file.name} превышает 5MB`);
continue;
}
additionalImages.push({
preview: URL.createObjectURL(file),
originalUrl: null,
isNew: true,
isFile: true,
file: file
});
}
updateAdditionalImages();
}
e.target.value = '';
});
// Функция для обновления списка характеристик
function updateSpecificationsList() {
specificationsList.innerHTML = '';
if (Object.keys(specifications).length === 0) {
specificationsList.innerHTML = `
Характеристики не добавлены
`;
return;
}
const table = document.createElement('table');
table.className = 'table table-sm';
table.innerHTML = `
Характеристика
Значение
Действия
${Object.entries(specifications).map(([key, value], index) => `
${key}
${value}
`).join('')}
`;
specificationsList.appendChild(table);
}
// Функция для удаления характеристики
window.removeSpecification = function (key) {
delete specifications[key];
updateSpecificationsList();
showInfo('Характеристика удалена', 'success');
};
// Функция для редактирования характеристики
window.editSpecification = function (oldKey, oldValue) {
addSpecificationModal(oldKey, oldValue);
};
// Функция для добавления/редактирования характеристики
function addSpecificationModal(oldKey = null, oldValue = null) {
const isEditMode = oldKey !== null;
const modalTitle = isEditMode ? 'Редактирование характеристики' : 'Добавление характеристики';
const saveButtonText = isEditMode ? 'Сохранить изменения' : 'Добавить характеристику';
const specModal = document.createElement('div');
specModal.className = 'modal fade';
specModal.id = 'addSpecModal';
specModal.innerHTML = `
`;
document.body.appendChild(specModal);
const bsSpecModal = new bootstrap.Modal(specModal);
// Функция выбора предопределенной характеристики
window.selectPredefinedSpec = function (name) {
document.getElementById('specName').value = name;
};
// Обработчик сохранения
specModal.querySelector('#saveSpecBtn').addEventListener('click', () => {
const name = document.getElementById('specName').value.trim();
const value = document.getElementById('specValue').value.trim();
if (!name) {
showError('Введите название характеристики');
return;
}
if (!value) {
showError('Введите значение характеристики');
return;
}
// Если это режим редактирования и название изменилось, удаляем старую характеристику
if (isEditMode && oldKey !== name) {
delete specifications[oldKey];
}
// Добавляем/обновляем характеристику
specifications[name] = value;
updateSpecificationsList();
bsSpecModal.hide();
setTimeout(() => {
specModal.remove();
showInfo(isEditMode ? 'Характеристика обновлена' : 'Характеристика добавлена', 'success');
}, 300);
});
// Очистка при закрытии
specModal.addEventListener('hidden.bs.modal', () => {
setTimeout(() => {
if (specModal.parentNode) specModal.remove();
}, 300);
});
bsSpecModal.show();
setTimeout(() => document.getElementById('specName').focus(), 100);
}
// Обработчик для добавления характеристики
addSpecBtn.addEventListener('click', () => addSpecificationModal());
// Инициализируем списки
updateMainImagePreview();
updateAdditionalImages();
updateSpecificationsList();
// Обработчик отправки формы
modal.querySelector('#manageToolkitForm').addEventListener('submit', async function (e) {
e.preventDefault();
// Собираем данные
const formData = {
title: document.getElementById('toolkitTitle').value.trim(),
category_id: document.getElementById('toolkitCategory').value,
description: document.getElementById('toolkitDescription').value.trim(),
external_link: document.getElementById('toolkitExternalLink').value.trim(),
specifications: specifications,
};
// Добавляем поля количества
const quantityMin = document.getElementById('toolkitQuantityMin').value.trim();
const quantityMinExtra = document.getElementById('toolkitQuantityMinExtra').value.trim();
if (quantityMin) {
formData.quantity_min = parseInt(quantityMin);
}
if (quantityMinExtra) {
formData.quantity_min_extra = parseInt(quantityMinExtra);
}
if (toolkitData?.id) {
formData.id = toolkitData.id;
}
// Валидация
if (!formData.title) {
showError('Введите название инструмента');
return;
}
if (!formData.category_id) {
showError('Выберите категорию');
return;
}
// Проверка значений количества
if (quantityMin && isNaN(parseInt(quantityMin))) {
showError('Низкое количество должно быть числом');
return;
}
if (quantityMinExtra && isNaN(parseInt(quantityMinExtra))) {
showError('Критическое количество должно быть числом');
return;
}
// Подготавливаем изображения
const imageData = {
main: mainImageFile ? await fileToBase64(mainImageFile) : data.image.main,
additional: []
};
// Обрабатываем дополнительные изображения
for (const image of additionalImages) {
if (image.isFile && image.isNew && image.file) {
// Новый файл - конвертируем в base64
imageData.additional.push(await fileToBase64(image.file));
} else if (!image.isFile && image.originalUrl) {
// Существующее изображение - отправляем оригинальный URL
imageData.additional.push(image.originalUrl);
}
}
formData.image = imageData;
// Показываем спиннер
submitBtn.disabled = true;
spinner.style.display = 'inline-block';
hideError();
try {
// Отправляем запрос
const userId = userData.id;
const response = await apiRequest('/toolkit/manage', { action, formData, userId }, 'POST');
if (response.status === 'ok') {
showInfo(isEditMode ? 'Инструмент обновлен' : 'Инструмент создан', 'success');
bsModal.hide();
// Обновляем список инструментов
if (typeof uploadTab === 'function') {
await uploadTab('toolkits');
}
} else {
throw new Error(response.message || 'Ошибка сохранения');
}
} catch (error) {
console.error('Ошибка при сохранении инструмента:', error);
showError(error.message || 'Произошла ошибка при сохранении инструмента');
// Возвращаем кнопку в исходное состояние
submitBtn.disabled = false;
spinner.style.display = 'none';
}
});
// Очистка при закрытии модалки
modal.addEventListener('hidden.bs.modal', () => {
// Освобождаем URL объекты
if (mainImagePreview && mainImagePreview.startsWith('blob:')) {
URL.revokeObjectURL(mainImagePreview);
}
// Освобождаем blob URL для дополнительных изображений
additionalImages.forEach(image => {
if (image.isFile && image.isNew && image.preview.startsWith('blob:')) {
URL.revokeObjectURL(image.preview);
}
});
setTimeout(() => {
if (modal.parentNode) modal.remove();
}, 300);
});
// Показываем модалку
bsModal.show();
return new Promise((resolve) => {
modal.addEventListener('hidden.bs.modal', () => {
resolve(null);
});
});
}
async function deleteToolkit(toolkitData) {
// Создаем модальное окно подтверждения удаления
const modalHTML = `
`;
// Добавляем модальное окно в DOM
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHTML;
document.body.appendChild(modalContainer);
const modal = new bootstrap.Modal(document.getElementById('deleteToolkitModal'));
const confirmBtn = document.getElementById('confirmDeleteBtn');
const hideBtn = document.getElementById('confirmHideBtn');
const resultMessage = document.getElementById('deleteResultMessage');
modal.show();
hideBtn.addEventListener('click', async () => {
hideBtn.disabled = true;
hideBtn.innerHTML = ' Скрытие...';
try {
// Подготавливаем данные для отправки
const formData = { toolkitId: toolkitData.id, userId: userData.id, hidden: true };
// Отправляем запрос на скрытие
const response = await apiRequest('/toolkit/hide', formData, 'POST');
// Показываем результат
if (response.status === 'ok') {
// Успешное скрытие
resultMessage.className = 'alert alert-success mt-3';
resultMessage.innerHTML = ` ${response.message || 'Инструмент успешно скрыт'}`;
resultMessage.classList.remove('d-none');
// Показываем общее уведомление
showInfo(response.message || 'Инструмент успешно скрыт', 'success');
setTimeout(() => {
modal.hide();
}, 1000);
// Обновляем список инструментов
if (typeof uploadTab === 'function') {
await uploadTab('toolkits');
}
} else {
// Ошибка при скрытии
resultMessage.className = 'alert alert-danger mt-3';
resultMessage.innerHTML = ` ${response.message || 'Произошла ошибка при скрытии инструмента'}`;
resultMessage.classList.remove('d-none');
}
} catch (error) {
console.error('Ошибка при скрытии инструмента:', error);
resultMessage.className = 'alert alert-danger mt-3';
resultMessage.innerHTML = ` Произошла ошибка при скрытии инструмента`;
resultMessage.classList.remove('d-none');
hideBtn.disabled = false;
hideBtn.innerHTML = ' Скрыть';
}
});
// Обработчик подтверждения удаления
confirmBtn.addEventListener('click', async () => {
// Блокируем кнопку и показываем индикатор загрузки
confirmBtn.disabled = true;
confirmBtn.innerHTML = ' Удаление...';
try {
// Подготавливаем данные для отправки
const formData = { id: toolkitData.id };
const userId = userData.id;
// Отправляем запрос на удаление
const response = await apiRequest('/toolkit/manage', {
action: 'delete',
formData,
userId
}, 'POST');
// Показываем результат
if (response.status === 'ok') {
// Успешное удаление
resultMessage.className = 'alert alert-success mt-3';
resultMessage.innerHTML = ` ${response.message || 'Инструмент успешно удален'}`;
resultMessage.classList.remove('d-none');
if (typeof uploadTab === 'function') {
await uploadTab('toolkits');
}
// Показываем общее уведомление
showInfo(response.message || 'Инструмент успешно удален', 'success');
// Закрываем модальное окно через 2 секунды
setTimeout(() => {
modal.hide();
}, 2000);
} else {
// Ошибка удаления (возможно, есть движения)
resultMessage.className = 'alert alert-danger mt-3';
resultMessage.innerHTML = ` ${response.message || 'Не удалось удалить инструмент'}`;
resultMessage.classList.remove('d-none');
// Разблокируем кнопку и возвращаем исходный текст
confirmBtn.disabled = false;
confirmBtn.textContent = 'Удалить';
// Если есть конкретное сообщение о движениях
if (response.message && response.message.includes('движени')) {
resultMessage.innerHTML += 'Удаление этого инструмента невозможно, так как были операции движения. Его можно только скрыть. ';
}
// Показываем общее уведомление
showInfo(response.message || 'Не удалось удалить инструмент', 'danger');
}
} catch (error) {
console.error('Ошибка при удалении инструмента:', error);
resultMessage.className = 'alert alert-danger mt-3';
resultMessage.innerHTML = ` Произошла ошибка при удалении: ${error.message}`;
resultMessage.classList.remove('d-none');
// Разблокируем кнопку и возвращаем исходный текст
confirmBtn.disabled = false;
confirmBtn.textContent = 'Удалить';
}
});
// Очистка при закрытии модального окна
document.getElementById('deleteToolkitModal').addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modalContainer);
});
}
// Функция показа модального окна для операций
async function showOperationModal(operation, selectedItem) {
const modalId = 'operationModal';
let modal = document.getElementById(modalId);
if (modal) {
modal.remove();
}
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = modalId;
modal.tabIndex = -1;
const operationTitles = {
'return': 'Возврат инструмента',
'writeoff': 'Списание инструмента',
'get': 'Получение инструмента'
};
// Определяем максимальное доступное количество
modal.innerHTML = `
${selectedItem.title} (доступно: ${selectedItem.totalQuantity} шт.)
Количество: (макс: ${selectedItem.totalQuantity})
Обоснование:
Ошибка выполнения операции
`;
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;