Files
toolbox/api/static/js/index.js
T

1941 lines
85 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getCookie } from '/static/js/cookies.js';
import { apiRequest } from '/static/js/api.js';
import { showInfo } from '/static/js//toast.js';
let accessData;
let userData;
let currentToolboxData = null;
async function getCookieData() {
accessData = await getCookie('toolbox_access');
userData = await getCookie('toolbox_user');
}
async function openTab(event, tabId) {
// Убираем активный класс со всех вкладок и кнопок
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
btn.classList.remove('active');
btn.querySelector('.nav-icon').classList.remove('text-primary');
btn.querySelector('.nav-icon').classList.add('text-muted');
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
// Добавляем активный класс выбранной вкладке и кнопке
event.currentTarget.classList.add('active');
event.currentTarget.querySelector('.nav-icon').classList.remove('text-muted');
event.currentTarget.querySelector('.nav-icon').classList.add('text-primary');
document.getElementById(tabId).classList.add('show', 'active');
await uploadTab(tabId);
}
function prepareTabs() {
let tabsData = {
'toolbox': {
title: 'Склад',
icon: 'bi-box-seam',
description: 'Управление остатками инструмента на складе'
},
'toolkits': {
title: 'Инструменты',
icon: 'bi-tools',
description: 'Каталог инструментов'
},
};
if (accessData.available_own_toolbox) {
tabsData['requests'] = {
title: 'Запросы',
icon: 'bi-chat-left-text',
description: 'Управление запросами на инструменты'
};
}
if (accessData.view_services) {
tabsData['jurnal_toolkits'] = {
title: 'Журнал перемещений',
icon: 'bi-journal-text',
description: 'Журнал перемещений инструментов'
};
}
if (accessData.view_requests) {
tabsData['jurnal_service'] = {
title: 'Сервисный журнал',
icon: 'bi-journal-richtext',
description: 'Журнал сервисных запросов'
};
}
if (accessData.users_view) {
tabsData['users'] = {
title: 'Пользователи',
icon: 'bi-people',
description: 'Управление пользователями'
};
}
const tabs = `
<div class="container-fluid p-0">
<!-- Верхняя панель навигации -->
<div class="row g-0 mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm" style="width: fit-content; margin: 0 auto;">
<div class="card-body p-3">
<div id="mainTabsNavWrapper">
<nav class="nav nav-pills gap-2" id="mainTabsNav" role="tablist">
${Object.entries(tabsData).map(([tabId, tabData], index) => `
<button class="nav-link tab-nav-btn d-flex flex-column align-items-center justify-content-center py-3 px-2"
id="${tabId}-tab"
role="tab"
onclick="openTab(event, '${tabId}')"
style="min-width: 120px; transition: all 0.3s ease;">
<i class="${tabData.icon} nav-icon fs-3 mb-2 text-muted" style="transition: all 0.3s ease;"></i>
<span class="nav-title fw-medium" style="font-size: 0.9rem;">${tabData.title}</span>
<div class="nav-indicator"></div>
</button>
`).join('')}
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Контент вкладок -->
<div class="row g-0">
<div class="col-12">
<div class="tab-content" id="mainTabsContent">
${Object.entries(tabsData).map(([tabId, tabData]) => `
<div class="tab-pane fade p-0"
id="${tabId}"
role="tabpanel">
<div class="card border-0 shadow-sm mb-1">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2">
<div class="card-body py-2">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded me-3">
<i class="${tabData.icon} fs-3 text-primary"></i>
</div>
<div>
<h2 class="mb-1">${tabData.title}</h2>
<p class="text-muted mb-0">${tabData.description}</p>
</div>
</div>
</div>
<div id="${tabId}-tab-optional-content" class="px-4 mt-3" style="max-width: 65%;"></div>
</div>
<div id="${tabId}-tab-content" class="px-4">
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
`;
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 = `
<div class="alert alert-danger m-4" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
Ошибка при загрузке данных: ${error.message}
</div>
`;
}
}
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 = `
<div class="alert alert-danger m-4" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
Ошибка отображения данных
</div>
`;
}
}
function renderSimpleTab(tabId, tabData, title) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
tabContent.innerHTML = `
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">${title}</h5>
<div class="row">
${Object.entries(tabData).map(([key, value]) => `
<div class="col-12 col-md-6 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="card-title mb-2">${key}</h6>
<p class="card-text">
${typeof value === 'object' ? JSON.stringify(value, null, 4) : value}
</p>
</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
</div>
`;
}
function renderToolkitsTab(tabId, toolsArray, categoriesList) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
// Преобразуем объект в массив, если передан объект
const tools = Array.isArray(toolsArray) ? toolsArray : Object.values(toolsArray);
let uniqueCategories = {};
categoriesList.forEach(cat => {
uniqueCategories[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
});
tools.forEach(tool => {
tool['category'] = uniqueCategories[tool.category_id]?.title || '';
tool['category_desc'] = uniqueCategories[tool.category_id]?.description || '';
});
// Сортируем инструменты: сначала по названию категории, затем по названию
const sortedTools = [...tools].sort((a, b) => {
// Получаем названия категорий
const catA = uniqueCategories[a.category_id]?.title || '';
const catB = uniqueCategories[b.category_id]?.title || '';
// Сначала сравниваем категории
if (catA < catB) return -1;
if (catA > catB) return 1;
// Если категории одинаковые, сравниваем названия инструментов
const titleA = a.title || '';
const titleB = b.title || '';
return titleA.localeCompare(titleB, 'ru');
});
// Создаем HTML структуру
tabContent.innerHTML = `
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<!-- Блок фильтров -->
<div class="row mb-4">
<div class="col-12 col-md-8 mb-3 mb-md-0">
<div class="d-flex flex-wrap gap-2">
<button class="btn filter-btn active"
data-category="all">
Все категории
</button>
${Object.values(uniqueCategories).map(category => `
<button class="btn filter-btn"
data-category="${category.id}">
${category.title}
</button>
`).join('')}
</div>
</div>
<div class="col-12 col-md-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text"
class="form-control"
id="${tabId}-search-input"
placeholder="Поиск по названию и описанию...">
</div>
</div>
</div>
<!-- Контейнер для карточек -->
<div id="${tabId}-cards-container" class="row g-4">
<!-- Карточки будут вставлены здесь -->
</div>
</div>
</div>
</div>
</div>
`;
// Рендерим карточки
renderToolkitCards(tabId, sortedTools, uniqueCategories);
// Добавляем обработчики событий для фильтров
setupFilters(tabId, tools, uniqueCategories);
}
// Функция для рендеринга карточек
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all') {
const container = document.getElementById(`${tabId}-cards-container`);
// Фильтруем инструменты
const filteredTools = tools.filter(tool => {
// Фильтр по категории
const categoryMatch = categoryFilter === 'all' || tool.category_id == categoryFilter;
// Фильтр по тексту
const searchMatch = !filterText ||
(tool.title && tool.title.toLowerCase().includes(filterText.toLowerCase())) ||
(tool.description && tool.description.toLowerCase().includes(filterText.toLowerCase()));
return categoryMatch && searchMatch;
});
// Рендерим карточки
if (filteredTools.length === 0) {
container.innerHTML = `
<div class="col-12 text-center py-5">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
Ничего не найдено. Попробуйте изменить параметры фильтрации.
</div>
</div>
`;
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 `
<div class="col-12 col-sm-6 col-lg-4 col-xl-3">
<div class="card toolkit-card h-100 border-0 shadow-sm"
data-toolid="${tool.id}">
<div class="position-relative">
<img src="${imageUrl}"
class="card-img-top toolkit-card-img"
alt="${tool.title || 'Инструмент'}"
onerror="this.src='static/images/tools/default.png'">
<span class="position-absolute top-0 end-0 m-2 category-badge bg-primary text-white rounded-pill">
${categoryName}
</span>
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title mb-2">${tool.title || 'Без названия'}</h5>
<p class="card-text flex-grow-1 toolkit-description text-muted">
${description}
</p>
<div class="mt-2">
${tool.quantity_min ? `
<small class="text-muted">
<i class="bi bi-box me-1"></i>
Мин: ${tool.quantity_min}
${tool.quantity_min_extra ? `(<i class="bi bi-exclamation-triangle"></i> ${tool.quantity_min_extra})` : ''}
</small>
` : ''}
</div>
</div>
</div>
</div>
`;
}).join('');
const cards = container.querySelectorAll('.toolkit-card');
cards.forEach(card => {
card.addEventListener('click', async event => {
const toolId = event.currentTarget.dataset.toolid;
const tool = tools.find(t => t.id == toolId);
await showToolkitDetailsModal(tool);
});
});
}
// Функция для настройки фильтров
function setupFilters(tabId, tools, categoriesMap) {
const container = document.getElementById(`${tabId}-cards-container`);
const searchInput = document.getElementById(`${tabId}-search-input`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`);
// Текущие значения фильтров
let currentFilter = {
category: 'all',
search: ''
};
// Обработчик для кнопок категорий
filterButtons.forEach(button => {
button.addEventListener('click', function () {
// Убираем активный класс у всех кнопок
filterButtons.forEach(btn => btn.classList.remove('active'));
// Добавляем активный класс текущей кнопке
this.classList.add('active');
currentFilter.category = this.dataset.category;
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category);
});
});
// Обработчик для поля поиска
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilter.search = this.value.trim();
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category);
}, 300);
});
// Очистка поиска
searchInput.insertAdjacentHTML('afterend', `
<button class="btn btn-outline-secondary d-none"
type="button"
id="${tabId}-clear-search">
<i class="bi bi-x-lg"></i>
</button>
`);
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addToolboxModalLabel">Добавить новый склад</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form id="addToolboxForm" novalidate>
<div class="modal-body">
<div class="mb-3">
<label for="toolboxTitle" class="form-label required">Название склада</label>
<input type="text" class="form-control" id="toolboxTitle"
required minlength="3" maxlength="100">
<div class="invalid-feedback">
Пожалуйста, введите название склада (не менее 3 символов)
</div>
</div>
<div class="mb-3">
<label for="toolboxDescription" class="form-label required">Описание склада</label>
<textarea class="form-control" id="toolboxDescription"
rows="2" minlength="3" maxlength="500"></textarea>
<div class="invalid-feedback">
Пожалуйста, введите описание (не менее 3 символов)
</div>
</div>
<div class="mb-3" id="toolboxMonitoringContainer">
<div class="form-check form-switch d-flex flex-column flex-md-row align-items-md-center justify-content-left"">
<input class="form-check-input me-2" type="checkbox"
role="switch" id="toolboxMonitoring">
<label class="form-check-label" for="toolboxMonitoring">
Отслеживание остатков
</label>
</div>
<div class="form-text">
Включите для отслеживания минимальных остатков инструментов
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="submit" class="btn btn-primary" id="submitToolboxBtn">
<span class="spinner-border spinner-border-sm me-1"
id="submitToolboxSpinner" style="display: none;"></span>
<span id="submitToolboxText">Добавить склад</span>
</button>
</div>
</form>
</div>
</div>
`;
// Если редактирование
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 ? `
<strong>Ошибка!</strong> Не удалось добавить склад.
Проверьте соединение и попробуйте еще раз.
` : `
<strong>Ошибка!</strong> Не удалось обновить склад.
Проверьте соединение и попробуйте еще раз.
`;
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 = `
<div class="text-center py-5">
<i class="bi bi-inboxes display-1 text-muted mb-3"></i>
<h4 class="text-muted">Нет доступных складов</h4>
<p class="text-muted">У вас нет доступа ни к одному складу</p>
</div>
`;
return;
}
// Создаем навигацию по складам
// Сортируем список складов по названию
tabData.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
const toolboxNav = `
<div class="d-flex flex-wrap gap-2" id="toolboxNav">
${tabData.map((toolbox, index) => `
<button class="btn btn-outline-primary toolbox-nav-btn d-flex align-items-center mb-2"
onclick="selectToolbox(${toolbox.id}, ${index})"
data-toolbox-id="${toolbox.id}">
<i class="bi bi-box-seam me-2"></i>
<span>${toolbox.title}</span>
<div class="toolbox-nav-indicator"></div>
</button>
`).join('')}
</div>
`;
// Создаем контейнер для содержимого склада
const toolboxContent = `
<div class="row mt-2 mb-3">
<div class="col-12">
<div class="card border-0 shadow-sm toolbox-content-container">
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<i class="bi bi-box display-1 text-muted mb-3"></i>
<h4 class="text-muted mb-2">Выберите склад для просмотра</h4>
<p class="text-muted text-center">
Для отображения содержимого склада нажмите на одну из кнопок выше
</p>
</div>
</div>
</div>
</div>
`;
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 = `
<i class="bi bi-plus-square-fill fs-5 me-2"></i>
<span>Добавить</span>
`;
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 = `
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<h4 class="text-muted mb-2">Загрузка содержимого склада</h4>
<p class="text-muted">Пожалуйста, подождите...</p>
</div>
`;
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', () => {
fillToolbox(toolboxInfo);
});
} else {
contentContainer.querySelector('#fillToolbox').remove();
}
}
if (toolboxData.length === 0) {
contentContainer.innerHTML = `
<div class="card-header bg-info border-bottom">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">${toolboxInfo?.title || 'Склад'}</h5>
<p class="text-muted mb-0 small">${toolboxInfo?.description || 'Описание отсутствует'}</p>
</div>
<button class="btn btn-sm btn-outline-danger" id="editToolbox">
<i class="bi bi-pencil-square"></i> Редактировать
</button>
<button class="btn btn-sm btn-success" id="fillToolbox">
<i class="bi bi-cart-plus-fill me-1"></i>Пополнить
</button>
<span class="badge bg-secondary">${toolboxOwn}</span>
</div>
</div>
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<i class="bi bi-box display-1 text-muted mb-3"></i>
<h4 class="text-muted mb-2">Склад пуст</h4>
<p class="text-muted">В этом складе нет инструментов</p>
<div class="mt-3 d-flex justify-content-end">
<button class="btn btn-danger" id="deleteToolbox">
<i class="bi bi-trash me-2"></i>
Удалить склад
</button>
</div>
</div>
`;
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 = `
<div class="card-header bg-info border-bottom">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">${toolboxInfo?.title || 'Склад'}</h5>
<p class="text-muted mb-0 small">${toolboxInfo?.description || 'Описание отсутствует'}</p>
</div>
<button class="btn btn-sm btn-outline-danger" id="editToolbox">
<i class="bi bi-pencil-square"></i> Редактировать
</button>
<button class="btn btn-sm btn-success" id="fillToolbox">
<i class="bi bi-cart-plus-fill me-1"></i>Пополнить
</button>
<span class="badge bg-secondary">${toolboxOwn}</span>
</div>
</div>
<div class="card-body p-0">
<div class="border">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="input-group me-2" style="flex: 1; max-width: 500px;">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" id="toolboxSearch" class="form-control" placeholder="Поиск по всем полям...">
<button class="btn btn-outline-secondary" type="button" id="resetFilter">
<i class="bi bi-x-circle"></i>
</button>
</div>
</div>
${notEnough > 0 && quantityMonitoring ? `
<div class="d-flex align-items-center">
<button class="btn btn-sm btn-warning position-relative" id="notEnoughBtn">
<i class="bi bi-exclamation-triangle me-1"></i>
<span class="badge rounded-pill bg-danger position-absolute top-0 start-100 translate-middle">
${notEnough}
<span class="visually-hidden">unread messages</span>
</span>
Осталось мало
</button>
</div>
` : ''}
<div class="d-flex align-items-center justify-content-between ">
<span>
Количество позиций: <span class="fw-bold me-2">${processedData.length}</span>
Количество инструментов: <span class="fw-bold me-2">${totalQuantity}</span>
Общая стоимость: <span class="fw-bold me-2">${totalCost}</span>
</span>
</div>
</div>
<div class="p-0">
<div class="table-responsive" style="max-height: 600px;">
<table class="table table-hover table-striped mb-0" id="toolboxItemsTable">
<thead class="table-light sticky-top" style="top: 0;">
<tr>
<th style="width: 60px;"></th>
<th data-sort="title">Название <i class="bi bi-arrow-down-up"></i></th>
<th data-sort="category">Категория <i class="bi bi-arrow-down-up"></i></th>
<th data-sort="totalQuantity">Количество <i class="bi bi-arrow-down-up"></i></th>
${quantityMonitoring ? '<th data-sort="indicator">Статус <i class="bi bi-arrow-down-up"></i></th>' : ''}
<th data-sort="totalCost">Стоимость <i class="bi bi-arrow-down-up"></i></th>
${toolboxOwn === 'Общий склад' ? `<th data-sort="placement">Расположение <i class="bi bi-arrow-down-up"></i></th>` : ''}
<th data-sort="lastUpdated">Последнее изменение <i class="bi bi-arrow-down-up"></i></th>
</tr>
</thead>
<tbody id="toolboxItemsBody">
<!-- Данные будут загружены динамически -->
</tbody>
</table>
</div>
<div class="card-footer">
<nav aria-label="Навигация по страницам">
<ul class="pagination pagination-sm justify-content-center mb-0" id="toolboxPagination">
<!-- Пагинация будет сгенерирована динамически -->
</ul>
</nav>
</div>
</div>
</div>
</div>
`;
handleEditBtn();
handleFillBtn()
// Инициализация таблицы с данными
await initializeToolboxTable(processedData, toolboxOwn, quantityMonitoring);
} else {
throw new Error(resp.message || 'Ошибка загрузки данных склада');
}
} catch (error) {
console.error('Error loading toolbox content:', error);
contentContainer.innerHTML = `
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<i class="bi bi-exclamation-triangle display-1 text-danger mb-3"></i>
<h4 class="text-danger mb-2">Ошибка загрузки</h4>
<p class="text-muted text-center">
Не удалось загрузить содержимое склада<br>
<small class="text-danger">${error.message}</small>
</p>
<button class="btn btn-primary mt-3" onclick="selectToolbox(${toolboxId})">
<i class="bi bi-arrow-clockwise me-2"></i>
Попробовать снова
</button>
</div>
`;
}
}
function fillToolbox(toolboxInfo) {
console.log(toolboxInfo);
showInfo('Функционал еще в разработке', 'warning');
}
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 = `
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
<div class="modal-content">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title text-danger mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Удаление склада
</h5>
<button type="button" class="btn-close mb-2" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body pt-0">
<div class="alert alert-warning mt-3" role="alert">
<div class="d-flex">
<i class="bi bi-exclamation-triangle-fill fs-4 me-2"></i>
<div>
<h6 class="alert-heading mb-2">Внимание! Это действие необратимо</h6>
<p class="mb-0">Вы собираетесь удалить склад. Это действие нельзя отменить.</p>
</div>
</div>
</div>
<div class="mb-4">
<p class="mb-2">Вы уверены, что хотите удалить следующий склад?</p>
<div class="card border">
<div class="card-body">
<h6 class="card-title mb-2">${toolboxInfo.title}</h6>
${toolboxInfo.description ? `<p class="text-muted small mb-1">${toolboxInfo.description}</p>` : ''}
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-calendar me-1"></i>
${toolboxInfo.created_at ? `Создан: ${toolboxInfo.created_at}` : ''}
</small>
<small class="text-muted">
<i class="bi bi-clock me-1"></i>
${toolboxInfo.updated_at ? `Обновлен: ${toolboxInfo.updated_at}` : ''}
</small>
</div>
</div>
</div>
</div>
<div class="alert alert-info" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle me-2 mt-1"></i>
<div>
<p class="mb-1">
<strong>Важно:</strong> Если по складу были операции движения инструментов, удаление будет невозможно.
</p>
<p class="mb-0 small">
В случае проблем с удалением обратитесь к администратору системы.
</p>
</div>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="confirmDeleteCheckbox">
<label class="form-check-label" for="confirmDeleteCheckbox">
Я понимаю последствия и хочу удалить этот склад
</label>
</div>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn" disabled>
<span class="spinner-border spinner-border-sm me-1"
id="deleteSpinner" style="display: none;"></span>
Удалить склад
</button>
</div>
</div>
</div>
`;
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 = `<a class="page-link" href="#">Назад</a>`;
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 = `<a class="page-link" href="#">${i}</a>`;
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 = `<a class="page-link" href="#">Вперед</a>`;
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 = `
<div class="btn-group btn-group-sm action-buttons">
<button class="btn btn-outline-primary" data-action="return" data-id="${item.id}" title="Вернуть">
<i class="bi bi-arrow-left-circle"></i>
</button>
<button class="btn btn-outline-danger" data-action="writeoff" data-id="${item.id}" title="Списать">
<i class="bi bi-trash"></i>
</button>
</div>
`;
} else if (toolboxOwn === 'Общий склад' && accessData.available_own_toolbox) {
actionButtons = `
<div class="btn-group btn-group-sm action-buttons">
<button class="btn btn-outline-success" data-action="get" data-id="${item.id}" title="Получить">
<i class="bi bi-box-arrow-in-down"></i> Получить
</button>
</div>
`;
}
tr.innerHTML = `
<td>
<a href="#" class="toolkit-image-link" data-id="${item.id}">
<img src="${item.image}" alt="${item.title}"
class="img-thumbnail" style="width: 50px; height: 50px; object-fit: cover;">
</a>
</td>
<td>${item.title}<br>${actionButtons}</td>
<td>${item.category}</td>
<td>${item.totalQuantity}</td>
${quantityMonitoring ?
`<td><span class="badge bg-${item.indicator?.class || 'secondary'}">${item.indicator?.text || '-'}</span></td>` : ''}
<td>${formatPrice(item.totalCost)} ₽</td>
${toolboxOwn === 'Общий склад' ? `<td>${item.placement}</td>` : ''}
<td>${item.lastUpdated}</td>
`;
tbody.appendChild(tr);
// Добавляем обработчики для кнопок в строке
const actionBtn = tr.querySelector('.action-buttons');
if (actionBtn) {
actionBtn.querySelectorAll('button[data-action]').forEach(button => {
button.addEventListener('click', async (e) => {
e.stopPropagation();
const action = e.currentTarget.dataset.action;
const itemId = e.currentTarget.dataset.id;
const selectedItem = data.find(d => d.id == itemId);
if (selectedItem) {
await showOperationModal(action, selectedItem);
}
});
});
}
});
// Добавляем обработчики для изображений
document.querySelectorAll('.toolkit-image-link').forEach(link => {
link.addEventListener('click', async (e) => {
e.preventDefault();
const itemId = e.currentTarget.dataset.id;
const item = data.find(d => d.id == itemId);
if (item) {
await showToolkitDetailsModal(item);
}
});
});
}
function parseDate(d) {
// d = "07.12.2025, 13:19:20"
const [datePart, timePart] = d.split(', ');
const [day, month, year] = datePart.split('.').map(Number);
const [hour, minute, second] = timePart.split(':').map(Number);
return new Date(year, month - 1, day, hour, minute, second);
}
// Функция сортировки
function sortData(field, direction) {
filteredData.sort((a, b) => {
let aValue = a[field];
let bValue = b[field];
// Для числовых полей
if (field === 'totalQuantity') {
aValue = parseFloat(aValue);
bValue = parseFloat(bValue);
}
// Для стоимости
if (field === 'totalCost') {
aValue = parseFloat(a.totalCost);
bValue = parseFloat(b.totalCost);
}
// Для дат
if (field === 'lastUpdated') {
aValue = parseDate(a.lastUpdated);
bValue = parseDate(b.lastUpdated);
}
// Для статуса
if (field === 'indicator') {
const order = { 'danger': 0, 'warning': 1, 'success': 2 };
aValue = order[a.indicator?.class] || 3;
bValue = order[b.indicator?.class] || 3;
}
if (direction === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
}
// Функция фильтрации
async function filterData(searchText) {
if (!searchText.trim()) {
filteredData = [...data];
} else {
const searchLower = searchText.toLowerCase();
filteredData = data.filter(item =>
item.title.toLowerCase().includes(searchLower) ||
item.toolkitData.description.toLowerCase().includes(searchLower)
);
}
currentPage = 1;
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
async function filterIndicator() {
const searchLower = 'мало';
filteredData = data.filter(item =>
(item.indicator?.text && item.indicator.text.toLowerCase().includes(searchLower))
);
currentPage = 1;
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
// Инициализация сортировки по заголовкам
document.querySelectorAll('#toolboxItemsTable th[data-sort]').forEach(th => {
th.addEventListener('click', async () => {
const field = th.dataset.sort;
if (currentSort.field === field) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.field = field;
currentSort.direction = 'asc';
}
// Обновляем иконки сортировки
document.querySelectorAll('#toolboxItemsTable th i').forEach(icon => {
icon.className = 'bi bi-arrow-down-up';
});
const currentIcon = th.querySelector('i');
currentIcon.className = currentSort.direction === 'asc' ?
'bi bi-arrow-up' : 'bi bi-arrow-down';
sortData(currentSort.field, currentSort.direction);
await initializePagination();
});
});
// Инициализация поиска
const searchInput = document.getElementById('toolboxSearch');
searchInput.addEventListener('input', async (e) => {
await filterData(e.target.value);
});
// Инициализация кнопки сброса фильтра
document.getElementById('resetFilter').addEventListener('click', async () => {
searchInput.value = '';
searchInput.placeholder = 'Поиск по всем полям...';
await filterData('');
});
// Инициализация кнопки "мало"
try {
document.getElementById('notEnoughBtn').addEventListener('click', async () => {
const showAll = 'Сбросить фильтр -->';
if (searchInput.placeholder === showAll) {
searchInput.placeholder = 'Поиск по всем полям...';
searchInput.value = '';
await filterData('');
return;
}
searchInput.value = '';
searchInput.placeholder = showAll;
await filterIndicator();
});
} catch (_) { }
// Начальная инициализация
sortData(currentSort.field, currentSort.direction);
await initializePagination();
}
async function getToolkitStocks(toolkitId) {
const userId = userData.id;
const allToolboxes = accessData.view_all_toolboxes;
const resp = await apiRequest('/toolkit/', { toolkitId, userId, allToolboxes });
return resp.data;
}
// Функция показа модального окна с деталями инструмента
async function showToolkitDetailsModal(item) {
const modalId = 'toolkitDetailsModal';
let modal = document.getElementById(modalId);
if (modal) {
modal.remove();
}
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = modalId;
modal.tabIndex = -1;
let images = [];
if (typeof item.image === 'string') {
images = item.image ?
[item.image, ...(item.images || [])] :
[item.image];
} else {
images = item.image ?
[item.image.main, ...(item.image.additional || [])] :
[item.image.main];;
}
let imagesDiv = '';
if (images.length > 1) {
const carouselId = `carousel-${item.id}`;
imagesDiv = `
<div class="col-md-4">
<div id="${carouselId}" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner">
${images.map((img, index) => `
<div class="carousel-item ${index === 0 ? 'active' : ''}">
<a href="${img}" data-lightbox="toolkit-${item.id}" data-title="${item.title}">
<img src="${img}" alt="${item.title}"
class="d-block w-100 rounded mb-3"
style="max-height: 300px; object-fit: contain; cursor: zoom-in;">
</a>
</div>
`).join('')}
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#${carouselId}" data-bs-slide="prev"
style="width: 40px; height: 40px; top: 50%; transform: translateY(-50%); left: 10px; opacity: 0.8; background-color: rgba(0, 0, 0, 0.5); border-radius: 50%;">
<span class="carousel-control-prev-icon" style="filter: invert(1); width: 20px; height: 20px;"></span>
<span class="visually-hidden">Предыдущее</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#${carouselId}" data-bs-slide="next"
style="width: 40px; height: 40px; top: 50%; transform: translateY(-50%); right: 10px; opacity: 0.8; background-color: rgba(0, 0, 0, 0.5); border-radius: 50%;">
<span class="carousel-control-next-icon" style="filter: invert(1); width: 20px; height: 20px;"></span>
<span class="visually-hidden">Следующее</span>
</button>
<div class="carousel-indicators" style="bottom: -30px;">
${images.map((_, index) => `
<button type="button" data-bs-target="#${carouselId}"
data-bs-slide-to="${index}"
class="${index === 0 ? 'active' : ''}"
aria-label="Slide ${index + 1}"
style="width: 12px; height: 12px; border-radius: 50%; border: 2px solid #333; background-color: rgba(255, 255, 255, 0.5); margin: 0 3px;"></button>
`).join('')}
</div>
</div>
</div>
`;
} else {
imagesDiv = `
<div class="col-md-4">
<a href="${images[0]}" data-lightbox="toolkit-${item.id}" data-title="${item.title}">
<img src="${images[0]}" alt="${item.title}"
class="img-fluid rounded mb-3"
style="max-height: 300px; object-fit: contain; cursor: zoom-in;">
</a>
</div>
`;
}
const description = item.description || item.toolkitData?.description || 'Нет описания';
const specifications = item.toolkitData?.specifications || item.specifications || {};
const external_link = item.external_link || item.toolkitData?.external_link || null;
const category_desc = item.categoryData?.description || item.category_desc || '';
const toolkitStocks = await getToolkitStocks(item.id);
modal.innerHTML = `
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-tools me-1"></i>${item.title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
${imagesDiv}
<div class="col-md-8">
<h6><i class="bi bi-info-circle me-1"></i>Описание:</h6>
<p>${description}</p>
<h6 class="mt-3"><i class="bi bi-list me-1"></i>Категория:</h6>
<p><strong>${item.category}</strong> - ${category_desc}</p>
${specifications ? `
<h6 class="mt-3"><i class="bi bi-gear me-1"></i>Характеристики:</h6>
<table class="table table-sm">
${Object.entries(specifications).map(([key, value]) => `
<tr>
<td style="width: 40%"><strong>${key}:</strong></td>
<td>${value}</td>
</tr>
`).join('')}
</table>
` : ''}
${toolkitStocks ? `
<h6 class="mt-3"><i class="bi bi-box me-1"></i>Остатки на складах: <strong>${toolkitStocks.count}</strong> шт.</h6>
<table class="table table-sm">
${Object.entries(toolkitStocks.toolboxes).map(([key, value]) => `
<tr>
<td style="width: 60%"><strong>${key}:</strong></td>
<td>${value.count} шт.</td>
<td class="fw-bold">${value.placement || ''}</td>
</tr>
`).join('')}
</table>
` : ''}
${external_link ? `
<a href="${external_link}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right me-1"></i>Внешняя ссылка
</a>
` : ''}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
// После создания модального окна добавьте инициализацию lightbox
lightbox.option({
'resizeDuration': 200,
'wrapAround': true,
'albumLabel': "Изображение %1 из %2",
'fadeDuration': 300,
'imageFadeDuration': 300
});
}
// Функция показа модального окна для операций
async function showOperationModal(operation, selectedItem) {
const modalId = 'operationModal';
let modal = document.getElementById(modalId);
if (modal) {
modal.remove();
}
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = modalId;
modal.tabIndex = -1;
const operationTitles = {
'return': 'Возврат инструмента',
'writeoff': 'Списание инструмента',
'get': 'Получение инструмента'
};
// Определяем максимальное доступное количество
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${operationTitles[operation]}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><strong>${selectedItem.title}</strong> (доступно: ${selectedItem.totalQuantity} шт.)</p>
<div class="d-flex flex-column flex-md-row align-items-center gap-2">
<label for="operationQuantity" class="form-label mb-0">Количество: (макс: ${selectedItem.totalQuantity})</label>
<input type="number" class="form-control" id="operationQuantity" style="max-width: 100px"
min="1" ${(operation === 'writeoff' || operation === 'get') ? `max="${selectedItem.totalQuantity}"` : ''} value="1">
</div>
<div class="my-3">
<label for="operationComment" class="form-label">Обоснование:</label>
<textarea class="form-control" id="operationComment" rows="2"></textarea>
</div>
</div>
<div id="operationError" class="alert alert-danger d-none mx-3" role="alert">
<span><i class="bi bi-exclamation-triangle me-2"></i> <span id="operationErrorMessage">Ошибка выполнения операции</span></span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmOperation">Подтвердить</button>
</div>
</div>
</div>
`;
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 = `
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
Обработка...
`;
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');
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;