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

1339 lines
59 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';
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">
<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"></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 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;
}
// Создаем навигацию по складам
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"
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;
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 ? 'Склад сотрудника' : 'Общий склад';
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));
// Отображаем содержимое склада
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>
<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>
<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>
`;
// Инициализация таблицы с данными
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 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.category.toLowerCase().includes(searchLower) ||
item.placement.toLowerCase().includes(searchLower) ||
item.totalQuantity.toString().includes(searchLower) ||
item.totalCost.toString().includes(searchLower) ||
(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 = '';
await filterData('');
});
// Начальная инициализация
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('/stocks/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="mb-3 d-flex flex-column flex-md-row align-items-md-center justify-content-left gap-2">
<label for="operationQuantity" class="form-label">Количество: ${operation === 'writeoff' || operation === 'get' ? `(макс: ${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="mb-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();
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;