1339 lines
59 KiB
JavaScript
1339 lines
59 KiB
JavaScript
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; |