Files
sklad_legacy/js/search.js
cyrteL e572c34e99 Add files via upload
v1.3.0
2025-07-28 18:24:57 +03:00

823 lines
33 KiB
JavaScript
Raw Permalink 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.

// JavaScript для страницы поиска
class SearchManager {
constructor() {
this.warehouse = new WarehouseManager();
this.searchResults = {
items: [],
operations: []
};
this.currentPage = 1;
this.resultsPerPage = 12;
this.currentQuery = '';
this.filters = {};
this.init();
}
init() {
$(document).ready(async () => {
try {
// Проверяем авторизацию
if (!auth.isUserAuthenticated()) {
console.log('Пользователь не авторизован, перенаправление на login.html');
window.location.href = 'login.html';
return;
}
console.log('Инициализация поиска для пользователя:', auth.getCurrentUser()?.name);
// Загружаем категории
await this.loadCategories();
// Загружаем категории в навигацию
await this.loadCategoriesNav();
// Загружаем категории в выпадающее меню
await this.loadCategoriesDropdown();
// Обновляем информацию о пользователе
this.updateUserInfo();
// Привязываем события
this.bindEvents();
// Инициализируем фильтры
this.initializeFilters();
} catch (error) {
console.error('Ошибка инициализации:', error);
auth.showNotification('Ошибка загрузки данных', 'danger');
}
});
}
bindEvents() {
// Поиск
$('#searchBtn').on('click', () => this.performSearch());
$('#searchQuery').on('keypress', (e) => {
if (e.which === 13) {
this.performSearch();
}
});
// Расширенные фильтры
$('#toggleAdvancedFilters').on('click', () => this.toggleAdvancedFilters());
$('#clearFiltersBtn').on('click', () => this.clearFilters());
// Сортировка и экспорт
$('#sortResults').on('change', () => this.applySorting());
$('#exportResultsBtn').on('click', () => this.exportResults());
// Переключение вкладок
$('#resultsTabs a').on('click', (e) => {
e.preventDefault();
$(e.target).tab('show');
});
// Обработчик выхода
$('#logoutBtn').on('click', (e) => {
e.preventDefault();
auth.logout();
window.location.href = 'login.html';
});
}
// Загрузка категорий для фильтра
async loadCategories() {
try {
const categories = await this.warehouse.getCategories();
const categoryFilter = $('#categoryFilter');
categories.forEach(category => {
const option = `<option value="${category.id}">${category.name}</option>`;
categoryFilter.append(option);
});
} catch (error) {
console.error('Ошибка при загрузке категорий:', error);
}
}
// Инициализация фильтров
initializeFilters() {
this.filters = {
searchType: 'all',
category: '',
minPrice: null,
maxPrice: null,
startDate: '',
endDate: ''
};
}
// Выполнение поиска
async performSearch() {
const query = $('#searchQuery').val().trim();
const searchType = $('#searchType').val();
if (!query) {
auth.showNotification('Введите поисковый запрос', 'warning');
return;
}
this.currentQuery = query;
this.filters.searchType = searchType;
this.collectFilters();
this.showLoading();
try {
await this.searchItems(query);
await this.searchOperations(query);
this.displayResults();
this.hideLoading();
} catch (error) {
console.error('Ошибка при поиске:', error);
auth.showNotification('Ошибка при выполнении поиска', 'danger');
this.hideLoading();
}
}
// Сбор фильтров
collectFilters() {
this.filters.category = $('#categoryFilter').val();
this.filters.minPrice = $('#minPrice').val() ? parseFloat($('#minPrice').val()) : null;
this.filters.maxPrice = $('#maxPrice').val() ? parseFloat($('#maxPrice').val()) : null;
this.filters.startDate = $('#startDate').val();
this.filters.endDate = $('#endDate').val();
}
// Поиск позиций
async searchItems(query) {
if (this.filters.searchType === 'operations') {
this.searchResults.items = [];
return;
}
const searchFilters = {
category: this.filters.category,
minPrice: this.filters.minPrice,
maxPrice: this.filters.maxPrice
};
this.searchResults.items = await this.warehouse.searchItems(query, searchFilters);
}
// Поиск операций
async searchOperations(query) {
if (this.filters.searchType === 'items') {
this.searchResults.operations = [];
return;
}
const operations = await this.warehouse.getOperations();
// Фильтруем операции по запросу
this.searchResults.operations = operations.filter(operation => {
const searchText = query.toLowerCase();
return (
operation.itemName.toLowerCase().includes(searchText) ||
operation.employeeName.toLowerCase().includes(searchText) ||
operation.notes.toLowerCase().includes(searchText) ||
operation.supplier.toLowerCase().includes(searchText) ||
operation.recipient.toLowerCase().includes(searchText)
);
});
// Применяем фильтры по дате
if (this.filters.startDate) {
this.searchResults.operations = this.searchResults.operations.filter(op =>
new Date(op.date) >= new Date(this.filters.startDate)
);
}
if (this.filters.endDate) {
this.searchResults.operations = this.searchResults.operations.filter(op =>
new Date(op.date) <= new Date(this.filters.endDate)
);
}
}
// Отображение результатов
displayResults() {
const totalResults = this.searchResults.items.length + this.searchResults.operations.length;
if (totalResults === 0) {
this.showNoResults();
return;
}
this.showResults();
this.updateResultsCount();
this.renderItemsResults();
this.renderOperationsResults();
this.renderPagination();
}
// Показать результаты
showResults() {
$('#emptyState').hide();
$('#loadingState').hide();
$('#searchResults').show();
}
// Показать отсутствие результатов
showNoResults() {
$('#emptyState').hide();
$('#loadingState').hide();
$('#searchResults').hide();
$('#emptyState').html(`
<i class="fas fa-search fa-3x text-muted mb-3"></i>
<h4>Результаты не найдены</h4>
<p class="text-muted">По запросу "${this.currentQuery}" ничего не найдено. Попробуйте изменить поисковый запрос или фильтры.</p>
`).show();
}
// Обновление счетчиков результатов
updateResultsCount() {
const totalResults = this.searchResults.items.length + this.searchResults.operations.length;
$('#resultsCount').text(`Найдено результатов: ${totalResults}`);
$('#itemsCount').text(this.searchResults.items.length);
$('#operationsCount').text(this.searchResults.operations.length);
}
// Рендеринг результатов товаров
renderItemsResults() {
const container = $('#itemsResultsContainer');
container.empty();
if (this.searchResults.items.length === 0) {
container.html(`
<div class="col-12">
<div class="empty-state">
<i class="fas fa-box-open"></i>
<h5>Позиции не найдены</h5>
<p>Попробуйте изменить поисковый запрос</p>
</div>
</div>
`);
return;
}
// Пагинация для позиций
const totalPages = Math.ceil(this.searchResults.items.length / this.resultsPerPage);
const startIndex = (this.currentPage - 1) * this.resultsPerPage;
const endIndex = startIndex + this.resultsPerPage;
const pageItems = this.searchResults.items.slice(startIndex, endIndex);
pageItems.forEach(item => {
const itemHtml = this.createItemCard(item);
container.append(itemHtml);
});
}
// Создание карточки позиции
createItemCard(item) {
const category = this.getCategoryInfo(item.category);
const stockStatus = this.getStockStatus(item);
return `
<div class="col-lg-4 col-md-6 mb-4">
<div class="card product-card h-100">
<div class="product-image">
<i class="${category.icon} fa-3x"></i>
</div>
<div class="card-body">
<h6 class="card-title">${this.highlightQuery(item.name)}</h6>
<p class="card-text text-muted small">${this.highlightQuery(item.description.substring(0, 80))}${item.description.length > 80 ? '...' : ''}</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-secondary">${category.name}</span>
<span class="badge ${stockStatus.class}">${stockStatus.text}</span>
</div>
<div class="row text-center mb-3">
<div class="col-6">
<small class="text-muted">Цена</small>
<div class="fw-bold">${item.price.toLocaleString()} ₽</div>
</div>
<div class="col-6">
<small class="text-muted">Количество</small>
<div class="fw-bold">${item.quantity} шт.</div>
</div>
</div>
<div class="d-grid">
<button class="btn btn-outline-primary btn-sm" onclick="searchManager.viewItemDetails(${item.id})">
<i class="fas fa-eye me-1"></i>Просмотр
</button>
</div>
</div>
</div>
</div>
`;
}
// Рендеринг результатов операций
renderOperationsResults() {
const tbody = $('#operationsResultsTable tbody');
tbody.empty();
if (this.searchResults.operations.length === 0) {
tbody.html(`
<tr>
<td colspan="6" class="text-center text-muted py-4">
<i class="fas fa-history fa-2x mb-3"></i>
<br>Операции не найдены
</td>
</tr>
`);
return;
}
// Пагинация для операций
const totalPages = Math.ceil(this.searchResults.operations.length / this.resultsPerPage);
const startIndex = (this.currentPage - 1) * this.resultsPerPage;
const endIndex = startIndex + this.resultsPerPage;
const pageOperations = this.searchResults.operations.slice(startIndex, endIndex);
pageOperations.forEach(operation => {
const row = this.createOperationRow(operation);
tbody.append(row);
});
}
// Создание строки операции
createOperationRow(operation) {
const typeIcon = operation.type === 'incoming' ? 'fa-box-open text-success' : 'fa-truck text-warning';
const typeText = operation.type === 'incoming' ? 'Приход' : 'Расход';
const statusClass = this.getStatusClass(operation.status);
const statusText = this.getStatusText(operation.status);
return `
<tr>
<td>${this.formatDate(operation.date)}</td>
<td>
<i class="fas ${typeIcon} me-1"></i>
${typeText}
</td>
<td>${this.highlightQuery(operation.itemName)}</td>
<td>${operation.quantity}</td>
<td>${this.highlightQuery(operation.employeeName)}</td>
<td>
<span class="status-badge ${statusClass}">${statusText}</span>
</td>
</tr>
`;
}
// Подсветка поискового запроса
highlightQuery(text) {
if (!this.currentQuery) return text;
const regex = new RegExp(`(${this.escapeRegex(this.currentQuery)})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
// Экранирование специальных символов для regex
escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Рендеринг пагинации
renderPagination() {
const pagination = $('#resultsPagination');
pagination.empty();
const totalResults = this.searchResults.items.length + this.searchResults.operations.length;
const totalPages = Math.ceil(totalResults / this.resultsPerPage);
if (totalPages <= 1) return;
// Кнопка "Предыдущая"
const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
pagination.append(`
<li class="page-item ${prevDisabled}">
<a class="page-link" href="#" onclick="searchManager.goToPage(${this.currentPage - 1})">
<i class="fas fa-chevron-left"></i>
</a>
</li>
`);
// Номера страниц
const startPage = Math.max(1, this.currentPage - 2);
const endPage = Math.min(totalPages, this.currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
const active = i === this.currentPage ? 'active' : '';
pagination.append(`
<li class="page-item ${active}">
<a class="page-link" href="#" onclick="searchManager.goToPage(${i})">${i}</a>
</li>
`);
}
// Кнопка "Следующая"
const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
pagination.append(`
<li class="page-item ${nextDisabled}">
<a class="page-link" href="#" onclick="searchManager.goToPage(${this.currentPage + 1})">
<i class="fas fa-chevron-right"></i>
</a>
</li>
`);
}
// Переход на страницу
goToPage(page) {
this.currentPage = page;
this.renderItemsResults();
this.renderOperationsResults();
this.renderPagination();
}
// Применение сортировки
applySorting() {
const sortBy = $('#sortResults').val();
if (sortBy === 'relevance') {
// Сортировка по релевантности (по умолчанию)
return;
}
// Сортировка позиций
this.searchResults.items.sort((a, b) => {
let aValue = a[sortBy];
let bValue = b[sortBy];
if (sortBy === 'date') {
aValue = new Date(aValue);
bValue = new Date(bValue);
}
return aValue > bValue ? 1 : -1;
});
// Сортировка операций
this.searchResults.operations.sort((a, b) => {
let aValue = a[sortBy];
let bValue = b[sortBy];
if (sortBy === 'date') {
aValue = new Date(aValue);
bValue = new Date(bValue);
}
return aValue > bValue ? 1 : -1;
});
this.currentPage = 1;
this.renderItemsResults();
this.renderOperationsResults();
this.renderPagination();
}
// Переключение расширенных фильтров
toggleAdvancedFilters() {
const advancedFilters = $('#advancedFilters');
const toggleBtn = $('#toggleAdvancedFilters');
if (advancedFilters.is(':visible')) {
advancedFilters.slideUp();
toggleBtn.html('<i class="fas fa-filter me-1"></i>Расширенные фильтры');
} else {
advancedFilters.slideDown();
toggleBtn.html('<i class="fas fa-times me-1"></i>Скрыть фильтры');
}
}
// Очистка фильтров
clearFilters() {
$('#categoryFilter').val('');
$('#minPrice').val('');
$('#maxPrice').val('');
$('#startDate').val('');
$('#endDate').val('');
this.initializeFilters();
}
// Экспорт результатов
async exportResults() {
try {
const totalResults = this.searchResults.items.length + this.searchResults.operations.length;
if (totalResults === 0) {
auth.showNotification('Нет результатов для экспорта', 'warning');
return;
}
let csv = 'Результаты поиска\n';
csv += `Запрос: ${this.currentQuery}\n`;
csv += `Дата поиска: ${new Date().toLocaleDateString('ru-RU')}\n\n`;
// Экспорт позиций
if (this.searchResults.items.length > 0) {
csv += 'ТОВАРЫ\n';
csv += 'ID,Название,Категория,Цена,Количество,Описание\n';
this.searchResults.items.forEach(item => {
csv += `${item.id},"${item.name}","${this.getCategoryInfo(item.category).name}",${item.price},${item.quantity},"${item.description}"\n`;
});
csv += '\n';
}
// Экспорт операций
if (this.searchResults.operations.length > 0) {
csv += 'ОПЕРАЦИИ\n';
csv += 'Дата,Тип,Товар,Количество,Сотрудник,Статус\n';
this.searchResults.operations.forEach(op => {
csv += `${this.formatDate(op.date)},"${op.type === 'incoming' ? 'Приход' : 'Расход'}","${op.itemName}",${op.quantity},"${op.employeeName}","${this.getStatusText(op.status)}"\n`;
});
}
// Скачиваем файл
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `поиск_${this.currentQuery}_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
auth.showNotification('Результаты поиска экспортированы', 'success');
} catch (error) {
console.error('Ошибка при экспорте:', error);
auth.showNotification('Ошибка при экспорте результатов', 'danger');
}
}
// Просмотр деталей позиции
async viewItemDetails(itemId) {
try {
const item = await this.warehouse.getItemById(itemId);
if (!item) return;
const category = this.getCategoryInfo(item.category);
const stockStatus = this.getStockStatus(item);
const detailsHtml = `
<div class="row">
<div class="col-md-6">
<h6>Основная информация</h6>
<table class="table table-sm">
<tr>
<td><strong>ID:</strong></td>
<td>${item.id}</td>
</tr>
<tr>
<td><strong>Название:</strong></td>
<td>${item.name}</td>
</tr>
<tr>
<td><strong>Категория:</strong></td>
<td>${category.name}</td>
</tr>
<tr>
<td><strong>Цена:</strong></td>
<td>${item.price.toLocaleString()} ₽</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6>Складская информация</h6>
<table class="table table-sm">
<tr>
<td><strong>Количество:</strong></td>
<td>${item.quantity} шт.</td>
</tr>
<tr>
<td><strong>Статус:</strong></td>
<td><span class="badge ${stockStatus.class}">${stockStatus.text}</span></td>
</tr>
<tr>
<td><strong>Местоположение:</strong></td>
<td>${item.location}</td>
</tr>
<tr>
<td><strong>Поставщик:</strong></td>
<td>${item.supplier || 'Не указан'}</td>
</tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>Описание</h6>
<p>${item.description || 'Описание отсутствует'}</p>
</div>
</div>
${item.barcode ? `
<div class="row mt-3">
<div class="col-12">
<h6>Штрих-код</h6>
<p class="font-monospace">${item.barcode}</p>
</div>
</div>
` : ''}
`;
$('#itemDetailsContent').html(detailsHtml);
$('#itemDetailsModal').modal('show');
} catch (error) {
console.error('Ошибка при загрузке деталей позиции:', error);
auth.showNotification('Ошибка при загрузке деталей позиции', 'danger');
}
}
// Получение информации о категории
getCategoryInfo(categoryId) {
const categories = [
{ id: 'electronics', name: 'Электроника', icon: 'fas fa-laptop' },
{ id: 'clothing', name: 'Одежда', icon: 'fas fa-tshirt' },
{ id: 'tools', name: 'Инструменты', icon: 'fas fa-tools' },
{ id: 'office', name: 'Офисные принадлежности', icon: 'fas fa-briefcase' },
{ id: 'furniture', name: 'Мебель', icon: 'fas fa-couch' },
{ id: 'books', name: 'Книги', icon: 'fas fa-book' }
];
return categories.find(cat => cat.id === categoryId) ||
{ id: 'unknown', name: 'Неизвестная категория', icon: 'fas fa-box' };
}
// Получение статуса запаса
getStockStatus(item) {
if (item.quantity === 0) {
return { class: 'bg-danger', text: 'Нет в наличии' };
} else if (item.quantity <= item.minQuantity) {
return { class: 'bg-warning', text: 'Заканчивается' };
} else {
return { class: 'bg-success', text: 'В наличии' };
}
}
// Получение класса статуса
getStatusClass(status) {
switch (status) {
case 'completed': return 'status-completed';
case 'pending': return 'status-pending';
case 'cancelled': return 'status-cancelled';
default: return 'status-pending';
}
}
// Получение текста статуса
getStatusText(status) {
switch (status) {
case 'completed': return 'Завершено';
case 'pending': return 'В обработке';
case 'cancelled': return 'Отменено';
default: return 'Неизвестно';
}
}
// Форматирование даты
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Показать индикатор загрузки
showLoading() {
$('#emptyState').hide();
$('#searchResults').hide();
$('#loadingState').show();
}
// Скрыть индикатор загрузки
hideLoading() {
$('#loadingState').hide();
}
// Обновление информации о пользователе
updateUserInfo() {
const user = auth.getCurrentUser();
if (user) {
$('#currentUserInfo').text(user.name);
$('#loginLink').hide();
$('#profileDropdown').show();
// Показываем админ панель только для администраторов
if (auth.hasRole('admin')) {
$('#adminDropdown').show();
} else {
$('#adminDropdown').hide();
}
} else {
$('#currentUserInfo').text('Гость');
$('#loginLink').show();
$('#profileDropdown').hide();
$('#adminDropdown').hide();
}
// Обновляем права доступа для элементов интерфейса
auth.updateButtonPermissions();
}
// Загрузка категорий в навигацию
async loadCategoriesNav() {
try {
const categories = await this.warehouse.getCategories();
const categoriesNav = $('#categoriesNav');
// Очищаем существующие категории (кроме "Главная")
categoriesNav.find('li:not(:first-child)').remove();
// Добавляем активные категории
categories.forEach(category => {
if (category.active) {
const categoryLink = `
<li class="nav-item">
<a class="nav-link" href="catalog.html?category=${category.id}" title="${category.description || ''}">
<i class="${category.icon}" style="color: ${category.color || '#2c5aa0'}"></i>
${category.name}
</a>
</li>
`;
categoriesNav.append(categoryLink);
}
});
} catch (error) {
console.error('Ошибка загрузки категорий:', error);
}
}
// Загрузка категорий в выпадающее меню
async loadCategoriesDropdown() {
try {
const categories = await this.warehouse.getCategories();
const dropdown = $('#catalogCategoriesDropdown');
// Очищаем существующие категории (кроме "Все позиции" и разделителя)
dropdown.find('li:not(:first-child):not(:nth-child(2))').remove();
// Добавляем активные категории
categories.forEach(category => {
if (category.active) {
const isEmpty = category.itemCount === 0;
const categoryLink = `
<li>
<a class="dropdown-item ${isEmpty ? 'empty-category' : ''}" href="catalog.html?category=${category.id}" title="${category.description || ''}">
<div class="category-info">
<i class="${category.icon}" style="color: ${category.color || '#2c5aa0'}"></i>
<span>${category.name}</span>
</div>
<span class="category-count">${category.itemCount || 0}</span>
</a>
</li>
`;
dropdown.append(categoryLink);
}
});
// Инициализируем hover-функциональность
this.initHoverDropdown();
} catch (error) {
console.error('Ошибка загрузки категорий в выпадающее меню:', error);
}
}
// Инициализация hover-функциональности для выпадающего меню
initHoverDropdown() {
const $dropdown = $('.nav-item.dropdown');
const $dropdownMenu = $dropdown.find('.dropdown-menu');
let hoverTimeout;
// Показываем меню при наведении
$dropdown.on('mouseenter', function() {
clearTimeout(hoverTimeout);
$(this).find('.dropdown-menu').addClass('show');
});
// Скрываем меню при уходе мыши
$dropdown.on('mouseleave', function() {
const $menu = $(this).find('.dropdown-menu');
hoverTimeout = setTimeout(() => {
$menu.removeClass('show');
}, 150); // Небольшая задержка для плавности
});
// Предотвращаем скрытие при наведении на само меню
$dropdownMenu.on('mouseenter', function() {
clearTimeout(hoverTimeout);
});
$dropdownMenu.on('mouseleave', function() {
hoverTimeout = setTimeout(() => {
$(this).removeClass('show');
}, 150);
});
}
}
// Инициализация менеджера поиска
const searchManager = new SearchManager();