Add files via upload
v0.2(css + js)
This commit is contained in:
340
js/auth.js
Normal file
340
js/auth.js
Normal file
@ -0,0 +1,340 @@
|
||||
// Система авторизации и управления пользователями
|
||||
|
||||
class AuthSystem {
|
||||
constructor() {
|
||||
this.currentUser = null;
|
||||
this.isAuthenticated = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Проверяем, есть ли сохраненная сессия
|
||||
this.checkSession();
|
||||
|
||||
// Обработчики событий
|
||||
this.bindEvents();
|
||||
|
||||
// Проверяем права доступа для текущей страницы
|
||||
this.checkPageAccess();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Обработчик выхода из системы
|
||||
$(document).on('click', '#logoutBtn', (e) => {
|
||||
e.preventDefault();
|
||||
this.logout();
|
||||
});
|
||||
|
||||
// Обработчик формы входа
|
||||
$(document).on('submit', '#loginForm', (e) => {
|
||||
e.preventDefault();
|
||||
this.login();
|
||||
});
|
||||
}
|
||||
|
||||
// Проверка сессии
|
||||
checkSession() {
|
||||
const userData = localStorage.getItem('warehouse_user');
|
||||
if (userData) {
|
||||
try {
|
||||
this.currentUser = JSON.parse(userData);
|
||||
this.isAuthenticated = true;
|
||||
this.updateUI();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке данных пользователя:', error);
|
||||
this.logout();
|
||||
}
|
||||
} else {
|
||||
// Если нет сессии и мы не на странице входа, перенаправляем
|
||||
if (!window.location.pathname.includes('login.html')) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Вход в систему
|
||||
login() {
|
||||
const username = $('#username').val();
|
||||
const password = $('#password').val();
|
||||
|
||||
if (!username || !password) {
|
||||
this.showNotification('Пожалуйста, заполните все поля', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Имитация проверки учетных данных
|
||||
const users = this.getUsers();
|
||||
const user = users.find(u => u.username === username && u.password === password);
|
||||
|
||||
if (user) {
|
||||
// Удаляем пароль из объекта пользователя перед сохранением
|
||||
const { password, ...userData } = user;
|
||||
this.currentUser = userData;
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// Сохраняем в localStorage
|
||||
localStorage.setItem('warehouse_user', JSON.stringify(userData));
|
||||
|
||||
this.showNotification('Успешный вход в систему', 'success');
|
||||
|
||||
// Перенаправляем на главную страницу
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 1000);
|
||||
} else {
|
||||
this.showNotification('Неверное имя пользователя или пароль', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Выход из системы
|
||||
logout() {
|
||||
this.currentUser = null;
|
||||
this.isAuthenticated = false;
|
||||
localStorage.removeItem('warehouse_user');
|
||||
|
||||
this.showNotification('Вы вышли из системы', 'info');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Обновление интерфейса в зависимости от прав доступа
|
||||
updateUI() {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
// Обновляем информацию о пользователе в навигации
|
||||
$('#profileDropdown .dropdown-toggle').html(`
|
||||
<i class="fas fa-user me-1"></i>${this.currentUser.name}
|
||||
`);
|
||||
|
||||
// Показываем/скрываем элементы администрирования
|
||||
if (this.hasRole('admin')) {
|
||||
$('#adminDropdown').show();
|
||||
} else {
|
||||
$('#adminDropdown').hide();
|
||||
}
|
||||
|
||||
// Обновляем права доступа для кнопок
|
||||
this.updateButtonPermissions();
|
||||
}
|
||||
|
||||
// Проверка прав доступа к странице
|
||||
checkPageAccess() {
|
||||
const currentPage = window.location.pathname;
|
||||
|
||||
// Страницы, требующие авторизации
|
||||
const protectedPages = [
|
||||
'/index.html', '/catalog.html', '/history.html', '/search.html',
|
||||
'/profile.html', '/admin/'
|
||||
];
|
||||
|
||||
// Страницы администрирования
|
||||
const adminPages = ['/admin/'];
|
||||
|
||||
// Проверяем, нужна ли авторизация для текущей страницы
|
||||
const needsAuth = protectedPages.some(page => currentPage.includes(page));
|
||||
|
||||
if (needsAuth && !this.isAuthenticated) {
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем права администратора
|
||||
if (adminPages.some(page => currentPage.includes(page)) && !this.hasRole('admin')) {
|
||||
this.showNotification('У вас нет прав для доступа к этой странице', 'danger');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление прав доступа для кнопок
|
||||
updateButtonPermissions() {
|
||||
// Кнопки, доступные только администраторам
|
||||
const adminButtons = [
|
||||
'#addItemBtn',
|
||||
'#editItemBtn',
|
||||
'#deleteItemBtn',
|
||||
'#manageUsersBtn'
|
||||
];
|
||||
|
||||
adminButtons.forEach(buttonId => {
|
||||
const button = $(buttonId);
|
||||
if (button.length) {
|
||||
if (this.hasRole('admin')) {
|
||||
button.prop('disabled', false);
|
||||
} else {
|
||||
button.prop('disabled', true);
|
||||
button.attr('title', 'Требуются права администратора');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Проверка роли пользователя
|
||||
hasRole(role) {
|
||||
return this.currentUser && this.currentUser.roles && this.currentUser.roles.includes(role);
|
||||
}
|
||||
|
||||
// Проверка разрешения
|
||||
hasPermission(permission) {
|
||||
return this.currentUser && this.currentUser.permissions && this.currentUser.permissions.includes(permission);
|
||||
}
|
||||
|
||||
// Получение текущего пользователя
|
||||
getCurrentUser() {
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
// Проверка аутентификации
|
||||
isUserAuthenticated() {
|
||||
return this.isAuthenticated;
|
||||
}
|
||||
|
||||
// Получение списка пользователей (имитация базы данных)
|
||||
getUsers() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
name: 'Администратор',
|
||||
email: 'admin@company.com',
|
||||
roles: ['admin'],
|
||||
permissions: ['read', 'write', 'delete', 'admin'],
|
||||
department: 'IT',
|
||||
position: 'Системный администратор'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'manager',
|
||||
password: 'manager123',
|
||||
name: 'Менеджер склада',
|
||||
email: 'manager@company.com',
|
||||
roles: ['manager'],
|
||||
permissions: ['read', 'write'],
|
||||
department: 'Склад',
|
||||
position: 'Менеджер склада'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'operator',
|
||||
password: 'operator123',
|
||||
name: 'Оператор',
|
||||
email: 'operator@company.com',
|
||||
roles: ['operator'],
|
||||
permissions: ['read'],
|
||||
department: 'Склад',
|
||||
position: 'Оператор склада'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'viewer',
|
||||
password: 'viewer123',
|
||||
name: 'Наблюдатель',
|
||||
email: 'viewer@company.com',
|
||||
roles: ['viewer'],
|
||||
permissions: ['read'],
|
||||
department: 'Бухгалтерия',
|
||||
position: 'Бухгалтер'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Создание нового пользователя (только для админов)
|
||||
createUser(userData) {
|
||||
if (!this.hasRole('admin')) {
|
||||
throw new Error('Недостаточно прав для создания пользователей');
|
||||
}
|
||||
|
||||
const users = this.getUsers();
|
||||
const newUser = {
|
||||
id: users.length + 1,
|
||||
...userData,
|
||||
roles: userData.roles || ['viewer'],
|
||||
permissions: userData.permissions || ['read']
|
||||
};
|
||||
|
||||
// В реальном приложении здесь был бы запрос к серверу
|
||||
console.log('Создан новый пользователь:', newUser);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
// Обновление пользователя
|
||||
updateUser(userId, userData) {
|
||||
if (!this.hasRole('admin')) {
|
||||
throw new Error('Недостаточно прав для редактирования пользователей');
|
||||
}
|
||||
|
||||
// В реальном приложении здесь был бы запрос к серверу
|
||||
console.log('Обновлен пользователь:', userId, userData);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Удаление пользователя
|
||||
deleteUser(userId) {
|
||||
if (!this.hasRole('admin')) {
|
||||
throw new Error('Недостаточно прав для удаления пользователей');
|
||||
}
|
||||
|
||||
// В реальном приложении здесь был бы запрос к серверу
|
||||
console.log('Удален пользователь:', userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Показать уведомление
|
||||
showNotification(message, type = 'info') {
|
||||
const toast = $(`
|
||||
<div class="toast align-items-center text-white bg-${type} border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Создаем контейнер для уведомлений, если его нет
|
||||
let toastContainer = $('#toastContainer');
|
||||
if (toastContainer.length === 0) {
|
||||
toastContainer = $('<div id="toastContainer" class="toast-container position-fixed top-0 end-0 p-3"></div>');
|
||||
$('body').append(toastContainer);
|
||||
}
|
||||
|
||||
toastContainer.append(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast[0]);
|
||||
bsToast.show();
|
||||
|
||||
// Удаляем уведомление после скрытия
|
||||
toast.on('hidden.bs.toast', function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Получение информации о правах доступа
|
||||
getAccessInfo() {
|
||||
if (!this.currentUser) return null;
|
||||
|
||||
return {
|
||||
user: this.currentUser,
|
||||
roles: this.currentUser.roles,
|
||||
permissions: this.currentUser.permissions,
|
||||
canRead: this.hasPermission('read'),
|
||||
canWrite: this.hasPermission('write'),
|
||||
canDelete: this.hasPermission('delete'),
|
||||
canAdmin: this.hasPermission('admin'),
|
||||
isAdmin: this.hasRole('admin'),
|
||||
isManager: this.hasRole('manager'),
|
||||
isOperator: this.hasRole('operator'),
|
||||
isViewer: this.hasRole('viewer')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация системы авторизации
|
||||
const auth = new AuthSystem();
|
||||
|
||||
// Экспорт для использования в других модулях
|
||||
window.auth = auth;
|
||||
638
js/catalog.js
Normal file
638
js/catalog.js
Normal file
@ -0,0 +1,638 @@
|
||||
// JavaScript для страницы каталога товаров
|
||||
|
||||
class CatalogManager {
|
||||
constructor() {
|
||||
this.warehouse = new WarehouseManager();
|
||||
this.currentItems = [];
|
||||
this.filteredItems = [];
|
||||
this.currentPage = 1;
|
||||
this.itemsPerPage = 12;
|
||||
this.currentView = 'grid';
|
||||
this.filters = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
$(document).ready(() => {
|
||||
this.loadCatalog();
|
||||
this.bindEvents();
|
||||
this.initializeFilters();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Поиск и фильтры
|
||||
$('#searchInput').on('input', (e) => this.handleSearch(e.target.value));
|
||||
$('#categoryFilter').on('change', (e) => this.handleCategoryFilter(e.target.value));
|
||||
$('#priceMin, #priceMax').on('input', () => this.handlePriceFilter());
|
||||
$('#inStockFilter, #lowStockFilter').on('change', () => this.applyFilters());
|
||||
$('#clearFiltersBtn').on('click', () => this.clearFilters());
|
||||
|
||||
// Переключение вида
|
||||
$('input[name="viewMode"]').on('change', (e) => {
|
||||
this.currentView = e.target.id === 'gridView' ? 'grid' : 'list';
|
||||
this.renderCatalog();
|
||||
});
|
||||
|
||||
// Модальные окна
|
||||
$('#saveItemBtn').on('click', () => this.addNewItem());
|
||||
$('#updateItemBtn').on('click', () => this.updateItem());
|
||||
|
||||
// Экспорт
|
||||
$('#exportBtn').on('click', () => this.exportCatalog());
|
||||
}
|
||||
|
||||
// Загрузка каталога
|
||||
async loadCatalog() {
|
||||
try {
|
||||
this.showLoading();
|
||||
|
||||
// Загружаем товары
|
||||
this.currentItems = await this.warehouse.getAllItems();
|
||||
|
||||
// Загружаем категории
|
||||
const categories = await this.warehouse.getCategories();
|
||||
this.populateCategoryFilters(categories);
|
||||
|
||||
// Применяем фильтры и рендерим
|
||||
this.applyFilters();
|
||||
this.updateStatistics();
|
||||
|
||||
this.hideLoading();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке каталога:', error);
|
||||
auth.showNotification('Ошибка при загрузке каталога', 'danger');
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация фильтров
|
||||
initializeFilters() {
|
||||
// Устанавливаем значения по умолчанию
|
||||
this.filters = {
|
||||
search: '',
|
||||
category: '',
|
||||
minPrice: null,
|
||||
maxPrice: null,
|
||||
inStock: false,
|
||||
lowStock: false
|
||||
};
|
||||
}
|
||||
|
||||
// Заполнение фильтра категорий
|
||||
populateCategoryFilters(categories) {
|
||||
const categoryFilter = $('#categoryFilter');
|
||||
const addItemCategory = $('#itemCategory');
|
||||
const editItemCategory = $('#editItemCategory');
|
||||
|
||||
// Очищаем существующие опции (кроме первой)
|
||||
categoryFilter.find('option:not(:first)').remove();
|
||||
addItemCategory.find('option:not(:first)').remove();
|
||||
editItemCategory.find('option:not(:first)').remove();
|
||||
|
||||
// Добавляем категории
|
||||
categories.forEach(category => {
|
||||
const option = `<option value="${category.id}">${category.name}</option>`;
|
||||
categoryFilter.append(option);
|
||||
addItemCategory.append(option);
|
||||
editItemCategory.append(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка поиска
|
||||
handleSearch(query) {
|
||||
this.filters.search = query;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// Обработка фильтра категории
|
||||
handleCategoryFilter(category) {
|
||||
this.filters.category = category;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// Обработка фильтра цены
|
||||
handlePriceFilter() {
|
||||
this.filters.minPrice = $('#priceMin').val() ? parseFloat($('#priceMin').val()) : null;
|
||||
this.filters.maxPrice = $('#priceMax').val() ? parseFloat($('#priceMax').val()) : null;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// Применение всех фильтров
|
||||
applyFilters() {
|
||||
this.filters.inStock = $('#inStockFilter').is(':checked');
|
||||
this.filters.lowStock = $('#lowStockFilter').is(':checked');
|
||||
|
||||
// Фильтруем товары
|
||||
this.filteredItems = this.currentItems.filter(item => {
|
||||
// Поиск по названию, описанию и штрих-коду
|
||||
if (this.filters.search) {
|
||||
const searchQuery = this.filters.search.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.name.toLowerCase().includes(searchQuery) ||
|
||||
item.description.toLowerCase().includes(searchQuery) ||
|
||||
item.barcode.includes(searchQuery);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Фильтр по категории
|
||||
if (this.filters.category && item.category !== this.filters.category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по цене
|
||||
if (this.filters.minPrice && item.price < this.filters.minPrice) {
|
||||
return false;
|
||||
}
|
||||
if (this.filters.maxPrice && item.price > this.filters.maxPrice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по наличию
|
||||
if (this.filters.inStock && item.quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по низкому запасу
|
||||
if (this.filters.lowStock && item.quantity > item.minQuantity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.currentPage = 1;
|
||||
this.renderCatalog();
|
||||
this.updateStatistics();
|
||||
}
|
||||
|
||||
// Очистка фильтров
|
||||
clearFilters() {
|
||||
$('#searchInput').val('');
|
||||
$('#categoryFilter').val('');
|
||||
$('#priceMin').val('');
|
||||
$('#priceMax').val('');
|
||||
$('#inStockFilter').prop('checked', false);
|
||||
$('#lowStockFilter').prop('checked', false);
|
||||
|
||||
this.initializeFilters();
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// Рендеринг каталога
|
||||
renderCatalog() {
|
||||
const container = $('#catalogContainer');
|
||||
container.empty();
|
||||
|
||||
if (this.filteredItems.length === 0) {
|
||||
container.html(`
|
||||
<div class="col-12">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-search"></i>
|
||||
<h4>Товары не найдены</h4>
|
||||
<p>Попробуйте изменить параметры поиска или фильтры</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Пагинация
|
||||
const totalPages = Math.ceil(this.filteredItems.length / this.itemsPerPage);
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const endIndex = startIndex + this.itemsPerPage;
|
||||
const pageItems = this.filteredItems.slice(startIndex, endIndex);
|
||||
|
||||
// Рендерим товары
|
||||
pageItems.forEach(item => {
|
||||
const itemHtml = this.currentView === 'grid' ?
|
||||
this.createGridItem(item) : this.createListItem(item);
|
||||
container.append(itemHtml);
|
||||
});
|
||||
|
||||
// Рендерим пагинацию
|
||||
this.renderPagination(totalPages);
|
||||
}
|
||||
|
||||
// Создание карточки товара (сетка)
|
||||
createGridItem(item) {
|
||||
const category = this.getCategoryInfo(item.category);
|
||||
const stockStatus = this.getStockStatus(item);
|
||||
|
||||
return `
|
||||
<div class="col-lg-3 col-md-4 col-sm-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">${item.name}</h6>
|
||||
<p class="card-text text-muted small">${item.description.substring(0, 60)}${item.description.length > 60 ? '...' : ''}</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 gap-2">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="catalogManager.viewItem(${item.id})">
|
||||
<i class="fas fa-eye me-1"></i>Просмотр
|
||||
</button>
|
||||
${auth.hasPermission('write') ? `
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="catalogManager.editItem(${item.id})">
|
||||
<i class="fas fa-edit me-1"></i>Редактировать
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Создание строки товара (список)
|
||||
createListItem(item) {
|
||||
const category = this.getCategoryInfo(item.category);
|
||||
const stockStatus = this.getStockStatus(item);
|
||||
|
||||
return `
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2">
|
||||
<div class="text-center">
|
||||
<i class="${category.icon} fa-2x text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-1">${item.name}</h6>
|
||||
<small class="text-muted">${item.description.substring(0, 50)}${item.description.length > 50 ? '...' : ''}</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="badge bg-secondary">${category.name}</span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="text-center">
|
||||
<div class="fw-bold">${item.price.toLocaleString()} ₽</div>
|
||||
<small class="text-muted">${item.quantity} шт.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="badge ${stockStatus.class}">${stockStatus.text}</span>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="catalogManager.viewItem(${item.id})">
|
||||
<i class="fas fa-eye me-2"></i>Просмотр
|
||||
</a></li>
|
||||
${auth.hasPermission('write') ? `
|
||||
<li><a class="dropdown-item" href="#" onclick="catalogManager.editItem(${item.id})">
|
||||
<i class="fas fa-edit me-2"></i>Редактировать
|
||||
</a></li>
|
||||
` : ''}
|
||||
${auth.hasPermission('delete') ? `
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="#" onclick="catalogManager.deleteItem(${item.id})">
|
||||
<i class="fas fa-trash me-2"></i>Удалить
|
||||
</a></li>
|
||||
` : ''}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Рендеринг пагинации
|
||||
renderPagination(totalPages) {
|
||||
const pagination = $('#pagination');
|
||||
pagination.empty();
|
||||
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
// Кнопка "Предыдущая"
|
||||
const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${prevDisabled}">
|
||||
<a class="page-link" href="#" onclick="catalogManager.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="catalogManager.goToPage(${i})">${i}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// Кнопка "Следующая"
|
||||
const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${nextDisabled}">
|
||||
<a class="page-link" href="#" onclick="catalogManager.goToPage(${this.currentPage + 1})">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// Переход на страницу
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.renderCatalog();
|
||||
}
|
||||
|
||||
// Обновление статистики
|
||||
updateStatistics() {
|
||||
const totalItems = this.filteredItems.length;
|
||||
const inStockItems = this.filteredItems.filter(item => item.quantity > 0).length;
|
||||
const lowStockItems = this.filteredItems.filter(item => item.quantity <= item.minQuantity).length;
|
||||
const categories = [...new Set(this.filteredItems.map(item => item.category))].length;
|
||||
|
||||
$('#totalItemsCount').text(totalItems);
|
||||
$('#inStockCount').text(inStockItems);
|
||||
$('#lowStockCount').text(lowStockItems);
|
||||
$('#categoriesCount').text(categories);
|
||||
}
|
||||
|
||||
// Получение информации о категории
|
||||
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: 'В наличии' };
|
||||
}
|
||||
}
|
||||
|
||||
// Просмотр товара
|
||||
viewItem(itemId) {
|
||||
const item = this.currentItems.find(i => i.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
// Здесь можно открыть модальное окно с подробной информацией
|
||||
auth.showNotification(`Просмотр товара: ${item.name}`, 'info');
|
||||
}
|
||||
|
||||
// Редактирование товара
|
||||
async editItem(itemId) {
|
||||
const item = this.currentItems.find(i => i.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
// Заполняем форму редактирования
|
||||
$('#editItemId').val(item.id);
|
||||
$('#editItemName').val(item.name);
|
||||
$('#editItemCategory').val(item.category);
|
||||
$('#editItemPrice').val(item.price);
|
||||
$('#editItemQuantity').val(item.quantity);
|
||||
$('#editItemDescription').val(item.description);
|
||||
$('#editItemBarcode').val(item.barcode);
|
||||
$('#editItemLocation').val(item.location);
|
||||
$('#editItemSupplier').val(item.supplier);
|
||||
$('#editItemMinQuantity').val(item.minQuantity);
|
||||
|
||||
// Открываем модальное окно
|
||||
$('#editItemModal').modal('show');
|
||||
}
|
||||
|
||||
// Обновление товара
|
||||
async updateItem() {
|
||||
const formData = this.getFormData('#editItemForm');
|
||||
|
||||
if (!this.validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemId = parseInt(formData.editItemId);
|
||||
const updateData = {
|
||||
name: formData.editItemName,
|
||||
category: formData.editItemCategory,
|
||||
price: parseFloat(formData.editItemPrice),
|
||||
quantity: parseInt(formData.editItemQuantity),
|
||||
description: formData.editItemDescription || '',
|
||||
barcode: formData.editItemBarcode || '',
|
||||
location: formData.editItemLocation || '',
|
||||
supplier: formData.editItemSupplier || '',
|
||||
minQuantity: parseInt(formData.editItemMinQuantity) || 0
|
||||
};
|
||||
|
||||
await this.warehouse.updateItem(itemId, updateData);
|
||||
|
||||
// Обновляем локальные данные
|
||||
const itemIndex = this.currentItems.findIndex(i => i.id === itemId);
|
||||
if (itemIndex !== -1) {
|
||||
this.currentItems[itemIndex] = { ...this.currentItems[itemIndex], ...updateData };
|
||||
}
|
||||
|
||||
auth.showNotification('Товар успешно обновлен', 'success');
|
||||
|
||||
// Закрываем модальное окно и обновляем каталог
|
||||
$('#editItemModal').modal('hide');
|
||||
this.applyFilters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении товара:', error);
|
||||
auth.showNotification('Ошибка при обновлении товара', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление нового товара
|
||||
async addNewItem() {
|
||||
const formData = this.getFormData('#addItemForm');
|
||||
|
||||
if (!this.validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newItem = await this.warehouse.addItem(formData);
|
||||
|
||||
// Добавляем в локальные данные
|
||||
this.currentItems.push(newItem);
|
||||
|
||||
auth.showNotification('Товар успешно добавлен', 'success');
|
||||
|
||||
// Закрываем модальное окно и обновляем каталог
|
||||
$('#addItemModal').modal('hide');
|
||||
this.applyFilters();
|
||||
|
||||
// Очищаем форму
|
||||
$('#addItemForm')[0].reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при добавлении товара:', error);
|
||||
auth.showNotification('Ошибка при добавлении товара', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление товара
|
||||
async deleteItem(itemId) {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот товар?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.warehouse.deleteItem(itemId);
|
||||
|
||||
// Удаляем из локальных данных
|
||||
this.currentItems = this.currentItems.filter(i => i.id !== itemId);
|
||||
|
||||
auth.showNotification('Товар успешно удален', 'success');
|
||||
this.applyFilters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении товара:', error);
|
||||
auth.showNotification('Ошибка при удалении товара', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Получение данных формы
|
||||
getFormData(formSelector) {
|
||||
const form = $(formSelector);
|
||||
const formData = {};
|
||||
|
||||
form.find('input, select, textarea').each(function() {
|
||||
const field = $(this);
|
||||
const name = field.attr('id');
|
||||
const value = field.val();
|
||||
|
||||
if (name) {
|
||||
formData[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
// Валидация формы
|
||||
validateForm(formData) {
|
||||
const requiredFields = ['itemName', 'itemCategory', 'itemPrice', 'itemQuantity'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field] || formData[field].trim() === '') {
|
||||
auth.showNotification(`Поле "${this.getFieldLabel(field)}" обязательно для заполнения`, 'warning');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка числовых значений
|
||||
if (formData.itemPrice && parseFloat(formData.itemPrice) < 0) {
|
||||
auth.showNotification('Цена не может быть отрицательной', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.itemQuantity && parseInt(formData.itemQuantity) < 0) {
|
||||
auth.showNotification('Количество не может быть отрицательным', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Получение метки поля
|
||||
getFieldLabel(fieldName) {
|
||||
const labels = {
|
||||
'itemName': 'Название товара',
|
||||
'itemCategory': 'Категория',
|
||||
'itemPrice': 'Цена',
|
||||
'itemQuantity': 'Количество',
|
||||
'editItemName': 'Название товара',
|
||||
'editItemCategory': 'Категория',
|
||||
'editItemPrice': 'Цена',
|
||||
'editItemQuantity': 'Количество'
|
||||
};
|
||||
|
||||
return labels[fieldName] || fieldName;
|
||||
}
|
||||
|
||||
// Экспорт каталога
|
||||
async exportCatalog() {
|
||||
try {
|
||||
const exportData = await this.warehouse.exportData('items');
|
||||
|
||||
// Создаем CSV
|
||||
let csv = exportData.headers.join(',') + '\n';
|
||||
exportData.rows.forEach(row => {
|
||||
csv += row.map(cell => `"${cell}"`).join(',') + '\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', `каталог_товаров_${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');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать индикатор загрузки
|
||||
showLoading() {
|
||||
$('#catalogContainer').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="loading-spinner text-primary mb-3"></div>
|
||||
<p class="text-muted">Загрузка каталога...</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Скрыть индикатор загрузки
|
||||
hideLoading() {
|
||||
// Загрузка завершается в renderCatalog()
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация менеджера каталога
|
||||
const catalogManager = new CatalogManager();
|
||||
620
js/history.js
Normal file
620
js/history.js
Normal file
@ -0,0 +1,620 @@
|
||||
// JavaScript для страницы истории операций
|
||||
|
||||
class HistoryManager {
|
||||
constructor() {
|
||||
this.warehouse = new WarehouseManager();
|
||||
this.operations = [];
|
||||
this.filteredOperations = [];
|
||||
this.currentPage = 1;
|
||||
this.operationsPerPage = 20;
|
||||
this.filters = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
$(document).ready(() => {
|
||||
this.loadHistory();
|
||||
this.bindEvents();
|
||||
this.initializeFilters();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Фильтры
|
||||
$('#applyFiltersBtn').on('click', () => this.applyFilters());
|
||||
$('#clearFiltersBtn').on('click', () => this.clearFilters());
|
||||
|
||||
// Сортировка
|
||||
$('#sortBy, #sortOrder').on('change', () => this.applySorting());
|
||||
|
||||
// Экспорт и отчеты
|
||||
$('#exportBtn').on('click', () => this.exportOperations());
|
||||
$('#generateReportBtn').on('click', () => this.generateReport());
|
||||
|
||||
// Установка дат по умолчанию
|
||||
this.setDefaultDates();
|
||||
}
|
||||
|
||||
// Загрузка истории операций
|
||||
async loadHistory() {
|
||||
try {
|
||||
this.showLoading();
|
||||
|
||||
// Загружаем операции
|
||||
this.operations = await this.warehouse.getOperations();
|
||||
|
||||
// Загружаем товары для фильтра
|
||||
const items = await this.warehouse.getAllItems();
|
||||
this.populateItemFilter(items);
|
||||
|
||||
// Загружаем сотрудников для фильтра
|
||||
this.populateEmployeeFilter();
|
||||
|
||||
// Применяем фильтры и рендерим
|
||||
this.applyFilters();
|
||||
this.updateStatistics();
|
||||
|
||||
this.hideLoading();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке истории:', error);
|
||||
auth.showNotification('Ошибка при загрузке истории операций', 'danger');
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация фильтров
|
||||
initializeFilters() {
|
||||
this.filters = {
|
||||
type: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
itemId: '',
|
||||
employeeId: '',
|
||||
status: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Установка дат по умолчанию
|
||||
setDefaultDates() {
|
||||
const today = new Date();
|
||||
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
|
||||
|
||||
$('#startDate').val(lastMonth.toISOString().split('T')[0]);
|
||||
$('#endDate').val(today.toISOString().split('T')[0]);
|
||||
|
||||
// Устанавливаем даты для отчета
|
||||
$('#reportStartDate').val(lastMonth.toISOString().split('T')[0]);
|
||||
$('#reportEndDate').val(today.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
// Заполнение фильтра товаров
|
||||
populateItemFilter(items) {
|
||||
const itemFilter = $('#itemFilter');
|
||||
const reportItem = $('#reportItem');
|
||||
|
||||
items.forEach(item => {
|
||||
const option = `<option value="${item.id}">${item.name}</option>`;
|
||||
itemFilter.append(option);
|
||||
reportItem.append(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Заполнение фильтра сотрудников
|
||||
populateEmployeeFilter() {
|
||||
const employeeFilter = $('#employeeFilter');
|
||||
const employees = auth.getUsers();
|
||||
|
||||
employees.forEach(employee => {
|
||||
const option = `<option value="${employee.id}">${employee.name}</option>`;
|
||||
employeeFilter.append(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Применение фильтров
|
||||
applyFilters() {
|
||||
// Собираем значения фильтров
|
||||
this.filters.type = $('#operationType').val();
|
||||
this.filters.startDate = $('#startDate').val();
|
||||
this.filters.endDate = $('#endDate').val();
|
||||
this.filters.itemId = $('#itemFilter').val();
|
||||
this.filters.employeeId = $('#employeeFilter').val();
|
||||
this.filters.status = $('#statusFilter').val();
|
||||
|
||||
// Фильтруем операции
|
||||
this.filteredOperations = this.operations.filter(operation => {
|
||||
// Фильтр по типу
|
||||
if (this.filters.type && operation.type !== this.filters.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
if (this.filters.startDate) {
|
||||
const operationDate = new Date(operation.date);
|
||||
const startDate = new Date(this.filters.startDate);
|
||||
if (operationDate < startDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.filters.endDate) {
|
||||
const operationDate = new Date(operation.date);
|
||||
const endDate = new Date(this.filters.endDate);
|
||||
endDate.setHours(23, 59, 59); // Конец дня
|
||||
if (operationDate > endDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтр по товару
|
||||
if (this.filters.itemId && operation.itemId !== parseInt(this.filters.itemId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по сотруднику
|
||||
if (this.filters.employeeId && operation.employeeId !== parseInt(this.filters.employeeId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Фильтр по статусу
|
||||
if (this.filters.status && operation.status !== this.filters.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.currentPage = 1;
|
||||
this.applySorting();
|
||||
this.renderOperations();
|
||||
this.updateStatistics();
|
||||
}
|
||||
|
||||
// Применение сортировки
|
||||
applySorting() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
const sortOrder = $('#sortOrder').val();
|
||||
|
||||
this.filteredOperations.sort((a, b) => {
|
||||
let aValue = a[sortBy];
|
||||
let bValue = b[sortBy];
|
||||
|
||||
// Специальная обработка для дат
|
||||
if (sortBy === 'date') {
|
||||
aValue = new Date(aValue);
|
||||
bValue = new Date(bValue);
|
||||
}
|
||||
|
||||
// Специальная обработка для количества
|
||||
if (sortBy === 'quantity') {
|
||||
aValue = parseInt(aValue);
|
||||
bValue = parseInt(bValue);
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
this.renderOperations();
|
||||
}
|
||||
|
||||
// Очистка фильтров
|
||||
clearFilters() {
|
||||
$('#operationType').val('');
|
||||
$('#startDate').val('');
|
||||
$('#endDate').val('');
|
||||
$('#itemFilter').val('');
|
||||
$('#employeeFilter').val('');
|
||||
$('#statusFilter').val('');
|
||||
|
||||
this.initializeFilters();
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
// Рендеринг операций
|
||||
renderOperations() {
|
||||
const tbody = $('#operationsTable tbody');
|
||||
tbody.empty();
|
||||
|
||||
if (this.filteredOperations.length === 0) {
|
||||
tbody.html(`
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<i class="fas fa-search fa-2x mb-3"></i>
|
||||
<br>Операции не найдены
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Пагинация
|
||||
const totalPages = Math.ceil(this.filteredOperations.length / this.operationsPerPage);
|
||||
const startIndex = (this.currentPage - 1) * this.operationsPerPage;
|
||||
const endIndex = startIndex + this.operationsPerPage;
|
||||
const pageOperations = this.filteredOperations.slice(startIndex, endIndex);
|
||||
|
||||
// Рендерим операции
|
||||
pageOperations.forEach(operation => {
|
||||
const row = this.createOperationRow(operation);
|
||||
tbody.append(row);
|
||||
});
|
||||
|
||||
// Рендерим пагинацию
|
||||
this.renderPagination(totalPages);
|
||||
}
|
||||
|
||||
// Создание строки операции
|
||||
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>${operation.itemName}</td>
|
||||
<td>${operation.quantity}</td>
|
||||
<td>${operation.employeeName}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="historyManager.viewOperationDetails(${operation.id})">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Рендеринг пагинации
|
||||
renderPagination(totalPages) {
|
||||
const pagination = $('#pagination');
|
||||
pagination.empty();
|
||||
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
// Кнопка "Предыдущая"
|
||||
const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${prevDisabled}">
|
||||
<a class="page-link" href="#" onclick="historyManager.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="historyManager.goToPage(${i})">${i}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// Кнопка "Следующая"
|
||||
const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${nextDisabled}">
|
||||
<a class="page-link" href="#" onclick="historyManager.goToPage(${this.currentPage + 1})">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// Переход на страницу
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.renderOperations();
|
||||
}
|
||||
|
||||
// Обновление статистики
|
||||
updateStatistics() {
|
||||
const totalOperations = this.filteredOperations.length;
|
||||
const incomingOperations = this.filteredOperations.filter(op => op.type === 'incoming').length;
|
||||
const outgoingOperations = this.filteredOperations.filter(op => op.type === 'outgoing').length;
|
||||
const totalQuantity = this.filteredOperations.reduce((sum, op) => sum + op.quantity, 0);
|
||||
|
||||
$('#totalOperations').text(totalOperations);
|
||||
$('#incomingOperations').text(incomingOperations);
|
||||
$('#outgoingOperations').text(outgoingOperations);
|
||||
$('#totalQuantity').text(totalQuantity);
|
||||
}
|
||||
|
||||
// Получение класса статуса
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Просмотр деталей операции
|
||||
viewOperationDetails(operationId) {
|
||||
const operation = this.operations.find(op => op.id === operationId);
|
||||
if (!operation) return;
|
||||
|
||||
const typeText = operation.type === 'incoming' ? 'Приход' : 'Расход';
|
||||
const statusText = this.getStatusText(operation.status);
|
||||
const statusClass = this.getStatusClass(operation.status);
|
||||
|
||||
const detailsHtml = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Основная информация</h6>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>ID операции:</strong></td>
|
||||
<td>${operation.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Тип:</strong></td>
|
||||
<td>
|
||||
<i class="fas ${operation.type === 'incoming' ? 'fa-box-open text-success' : 'fa-truck text-warning'} me-1"></i>
|
||||
${typeText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Дата:</strong></td>
|
||||
<td>${this.formatDate(operation.date)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Статус:</strong></td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Детали товара</h6>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Товар:</strong></td>
|
||||
<td>${operation.itemName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Количество:</strong></td>
|
||||
<td>${operation.quantity} шт.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Сотрудник:</strong></td>
|
||||
<td>${operation.employeeName}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
${operation.supplier ? `
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>Дополнительная информация</h6>
|
||||
<table class="table table-sm">
|
||||
${operation.supplier ? `<tr><td><strong>Поставщик:</strong></td><td>${operation.supplier}</td></tr>` : ''}
|
||||
${operation.recipient ? `<tr><td><strong>Получатель:</strong></td><td>${operation.recipient}</td></tr>` : ''}
|
||||
${operation.notes ? `<tr><td><strong>Примечания:</strong></td><td>${operation.notes}</td></tr>` : ''}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
$('#operationDetailsContent').html(detailsHtml);
|
||||
$('#operationDetailsModal').modal('show');
|
||||
}
|
||||
|
||||
// Экспорт операций
|
||||
async exportOperations() {
|
||||
try {
|
||||
const exportData = await this.warehouse.exportData('operations');
|
||||
|
||||
// Создаем CSV
|
||||
let csv = exportData.headers.join(',') + '\n';
|
||||
exportData.rows.forEach(row => {
|
||||
csv += row.map(cell => `"${cell}"`).join(',') + '\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', `операции_${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 generateReport() {
|
||||
const startDate = $('#reportStartDate').val();
|
||||
const endDate = $('#reportEndDate').val();
|
||||
const reportType = $('#reportType').val();
|
||||
const reportItem = $('#reportItem').val();
|
||||
const reportFormat = $('input[name="reportFormat"]:checked').val();
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
auth.showNotification('Пожалуйста, укажите период для отчета', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let reportData;
|
||||
|
||||
switch (reportType) {
|
||||
case 'movement':
|
||||
reportData = await this.warehouse.getMovementReport(startDate, endDate, reportItem || null);
|
||||
break;
|
||||
case 'detailed':
|
||||
reportData = await this.warehouse.getOperations({
|
||||
startDate,
|
||||
endDate,
|
||||
itemId: reportItem || null
|
||||
});
|
||||
break;
|
||||
default:
|
||||
reportData = await this.warehouse.getStatistics();
|
||||
}
|
||||
|
||||
if (reportFormat === 'csv') {
|
||||
this.downloadCsvReport(reportData, reportType);
|
||||
} else {
|
||||
this.downloadPdfReport(reportData, reportType);
|
||||
}
|
||||
|
||||
auth.showNotification('Отчет успешно создан', 'success');
|
||||
$('#reportModal').modal('hide');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании отчета:', error);
|
||||
auth.showNotification('Ошибка при создании отчета', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Скачивание CSV отчета
|
||||
downloadCsvReport(reportData, reportType) {
|
||||
let csv = '';
|
||||
let filename = '';
|
||||
|
||||
switch (reportType) {
|
||||
case 'movement':
|
||||
csv = this.generateMovementCsv(reportData);
|
||||
filename = `отчет_движения_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
break;
|
||||
case 'detailed':
|
||||
csv = this.generateDetailedCsv(reportData);
|
||||
filename = `детальный_отчет_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
break;
|
||||
default:
|
||||
csv = this.generateSummaryCsv(reportData);
|
||||
filename = `сводный_отчет_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
}
|
||||
|
||||
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', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
// Скачивание PDF отчета (заглушка)
|
||||
downloadPdfReport(reportData, reportType) {
|
||||
// В реальном приложении здесь была бы генерация PDF
|
||||
auth.showNotification('Функция PDF отчета находится в разработке', 'info');
|
||||
}
|
||||
|
||||
// Генерация CSV для отчета движения
|
||||
generateMovementCsv(reportData) {
|
||||
let csv = 'Отчет по движению товаров\n';
|
||||
csv += `Период: ${reportData.period.startDate} - ${reportData.period.endDate}\n\n`;
|
||||
csv += 'Показатель,Значение\n';
|
||||
csv += `Всего приходов,${reportData.totalIncoming}\n`;
|
||||
csv += `Всего расходов,${reportData.totalOutgoing}\n`;
|
||||
csv += `Количество прихода,${reportData.totalIncomingQuantity}\n`;
|
||||
csv += `Количество расхода,${reportData.totalOutgoingQuantity}\n\n`;
|
||||
|
||||
csv += 'Детали операций\n';
|
||||
csv += 'Дата,Тип,Товар,Количество,Сотрудник\n';
|
||||
|
||||
reportData.operations.forEach(op => {
|
||||
csv += `${this.formatDate(op.date)},${op.type === 'incoming' ? 'Приход' : 'Расход'},${op.itemName},${op.quantity},${op.employeeName}\n`;
|
||||
});
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Генерация CSV для детального отчета
|
||||
generateDetailedCsv(reportData) {
|
||||
let csv = 'Детальный отчет по операциям\n\n';
|
||||
csv += 'Дата,Тип,Товар,Количество,Сотрудник,Статус,Примечания\n';
|
||||
|
||||
reportData.forEach(op => {
|
||||
csv += `${this.formatDate(op.date)},${op.type === 'incoming' ? 'Приход' : 'Расход'},${op.itemName},${op.quantity},${op.employeeName},${this.getStatusText(op.status)},${op.notes || ''}\n`;
|
||||
});
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Генерация CSV для сводного отчета
|
||||
generateSummaryCsv(reportData) {
|
||||
let csv = 'Сводный отчет по складу\n\n';
|
||||
csv += 'Показатель,Значение\n';
|
||||
csv += `Всего товаров,${reportData.totalItems}\n`;
|
||||
csv += `Общая стоимость,${reportData.totalValue} ₽\n`;
|
||||
csv += `Товары с низким запасом,${reportData.lowStockItems}\n`;
|
||||
csv += `Операций сегодня,${reportData.todayOperations}\n`;
|
||||
csv += `Всего категорий,${reportData.totalCategories}\n`;
|
||||
csv += `Всего операций,${reportData.totalOperations}\n`;
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Показать индикатор загрузки
|
||||
showLoading() {
|
||||
$('#operationsTable tbody').html(`
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5">
|
||||
<div class="loading-spinner text-primary mb-3"></div>
|
||||
<p class="text-muted">Загрузка истории операций...</p>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
|
||||
// Скрыть индикатор загрузки
|
||||
hideLoading() {
|
||||
// Загрузка завершается в renderOperations()
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация менеджера истории
|
||||
const historyManager = new HistoryManager();
|
||||
430
js/main.js
Normal file
430
js/main.js
Normal file
@ -0,0 +1,430 @@
|
||||
// Основной JavaScript файл для главной страницы
|
||||
|
||||
class MainApp {
|
||||
constructor() {
|
||||
this.warehouse = new WarehouseManager();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Инициализация после загрузки DOM
|
||||
$(document).ready(() => {
|
||||
this.loadDashboardData();
|
||||
this.bindEvents();
|
||||
this.initializeModals();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Обработчики для модальных окон
|
||||
$('#saveItemBtn').on('click', () => this.addNewItem());
|
||||
$('#saveIncomingBtn').on('click', () => this.processIncoming());
|
||||
$('#saveOutgoingBtn').on('click', () => this.processOutgoing());
|
||||
|
||||
// Обработчики для фильтров и поиска
|
||||
$('#searchInput').on('input', (e) => this.handleSearch(e.target.value));
|
||||
|
||||
// Обработчики для сортировки
|
||||
$('.sortable').on('click', (e) => this.handleSort(e));
|
||||
|
||||
// Обработчики для экспорта
|
||||
$('#exportBtn').on('click', () => this.exportData());
|
||||
}
|
||||
|
||||
// Загрузка данных для дашборда
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
this.showLoading();
|
||||
|
||||
// Загружаем статистику
|
||||
const stats = await this.warehouse.getStatistics();
|
||||
this.updateStatistics(stats);
|
||||
|
||||
// Загружаем последние операции
|
||||
const operations = await this.warehouse.getRecentOperations(10);
|
||||
this.updateOperationsTable(operations);
|
||||
|
||||
// Загружаем товары для модальных окон
|
||||
const items = await this.warehouse.getAllItems();
|
||||
this.populateItemSelects(items);
|
||||
|
||||
// Скрываем индикатор загрузки
|
||||
this.hideLoading();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке данных:', error);
|
||||
auth.showNotification('Ошибка при загрузке данных', 'danger');
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление статистики
|
||||
updateStatistics(stats) {
|
||||
$('#totalItems').text(stats.totalItems.toLocaleString());
|
||||
$('#totalValue').text(stats.totalValue.toLocaleString() + ' ₽');
|
||||
$('#lowStock').text(stats.lowStockItems);
|
||||
$('#todayOperations').text(stats.todayOperations);
|
||||
}
|
||||
|
||||
// Обновление таблицы операций
|
||||
updateOperationsTable(operations) {
|
||||
const tbody = $('#recentOperationsTable tbody');
|
||||
tbody.empty();
|
||||
|
||||
if (operations.length === 0) {
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<br>Нет операций для отображения
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
operations.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>${operation.itemName}</td>
|
||||
<td>${operation.quantity}</td>
|
||||
<td>${operation.employeeName}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Получение класса статуса
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Заполнение селектов товаров
|
||||
populateItemSelects(items) {
|
||||
const selects = ['#incomingItem', '#outgoingItem'];
|
||||
|
||||
selects.forEach(selectId => {
|
||||
const select = $(selectId);
|
||||
select.find('option:not(:first)').remove();
|
||||
|
||||
items.forEach(item => {
|
||||
select.append(`<option value="${item.id}">${item.name} (${item.quantity} шт.)</option>`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Добавление нового товара
|
||||
async addNewItem() {
|
||||
const formData = this.getFormData('#addItemForm');
|
||||
|
||||
if (!this.validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newItem = await this.warehouse.addItem(formData);
|
||||
auth.showNotification('Товар успешно добавлен', 'success');
|
||||
|
||||
// Закрываем модальное окно
|
||||
$('#addItemModal').modal('hide');
|
||||
|
||||
// Обновляем данные
|
||||
this.loadDashboardData();
|
||||
|
||||
// Очищаем форму
|
||||
$('#addItemForm')[0].reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при добавлении товара:', error);
|
||||
auth.showNotification('Ошибка при добавлении товара', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка прихода
|
||||
async processIncoming() {
|
||||
const formData = this.getFormData('#incomingForm');
|
||||
|
||||
if (!this.validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const operation = await this.warehouse.processIncoming(formData);
|
||||
auth.showNotification('Приход успешно оформлен', 'success');
|
||||
|
||||
// Закрываем модальное окно
|
||||
$('#incomingModal').modal('hide');
|
||||
|
||||
// Обновляем данные
|
||||
this.loadDashboardData();
|
||||
|
||||
// Очищаем форму
|
||||
$('#incomingForm')[0].reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при оформлении прихода:', error);
|
||||
auth.showNotification('Ошибка при оформлении прихода', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка расхода
|
||||
async processOutgoing() {
|
||||
const formData = this.getFormData('#outgoingForm');
|
||||
|
||||
if (!this.validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const operation = await this.warehouse.processOutgoing(formData);
|
||||
auth.showNotification('Расход успешно оформлен', 'success');
|
||||
|
||||
// Закрываем модальное окно
|
||||
$('#outgoingModal').modal('hide');
|
||||
|
||||
// Обновляем данные
|
||||
this.loadDashboardData();
|
||||
|
||||
// Очищаем форму
|
||||
$('#outgoingForm')[0].reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при оформлении расхода:', error);
|
||||
auth.showNotification('Ошибка при оформлении расхода', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Получение данных формы
|
||||
getFormData(formSelector) {
|
||||
const form = $(formSelector);
|
||||
const formData = {};
|
||||
|
||||
form.find('input, select, textarea').each(function() {
|
||||
const field = $(this);
|
||||
const name = field.attr('id');
|
||||
const value = field.val();
|
||||
|
||||
if (name) {
|
||||
formData[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
// Валидация формы
|
||||
validateForm(formData) {
|
||||
const requiredFields = ['itemName', 'itemCategory', 'itemPrice', 'itemQuantity'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field] || formData[field].trim() === '') {
|
||||
auth.showNotification(`Поле "${this.getFieldLabel(field)}" обязательно для заполнения`, 'warning');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка числовых значений
|
||||
if (formData.itemPrice && parseFloat(formData.itemPrice) < 0) {
|
||||
auth.showNotification('Цена не может быть отрицательной', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.itemQuantity && parseInt(formData.itemQuantity) < 0) {
|
||||
auth.showNotification('Количество не может быть отрицательным', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Получение метки поля
|
||||
getFieldLabel(fieldName) {
|
||||
const labels = {
|
||||
'itemName': 'Название товара',
|
||||
'itemCategory': 'Категория',
|
||||
'itemPrice': 'Цена',
|
||||
'itemQuantity': 'Количество',
|
||||
'incomingItem': 'Товар',
|
||||
'incomingQuantity': 'Количество',
|
||||
'outgoingItem': 'Товар',
|
||||
'outgoingQuantity': 'Количество'
|
||||
};
|
||||
|
||||
return labels[fieldName] || fieldName;
|
||||
}
|
||||
|
||||
// Инициализация модальных окон
|
||||
initializeModals() {
|
||||
// Очистка форм при закрытии модальных окон
|
||||
$('.modal').on('hidden.bs.modal', function() {
|
||||
$(this).find('form')[0].reset();
|
||||
});
|
||||
|
||||
// Валидация в реальном времени
|
||||
$('.form-control, .form-select').on('input change', function() {
|
||||
const field = $(this);
|
||||
const value = field.val();
|
||||
|
||||
if (field.prop('required') && (!value || value.trim() === '')) {
|
||||
field.addClass('is-invalid');
|
||||
} else {
|
||||
field.removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка поиска
|
||||
handleSearch(query) {
|
||||
// Реализация поиска по таблице
|
||||
const table = $('#recentOperationsTable');
|
||||
const rows = table.find('tbody tr');
|
||||
|
||||
rows.each(function() {
|
||||
const row = $(this);
|
||||
const text = row.text().toLowerCase();
|
||||
const matches = text.includes(query.toLowerCase());
|
||||
|
||||
if (matches || query === '') {
|
||||
row.show();
|
||||
} else {
|
||||
row.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка сортировки
|
||||
handleSort(event) {
|
||||
const column = $(event.target);
|
||||
const table = column.closest('table');
|
||||
const tbody = table.find('tbody');
|
||||
const rows = tbody.find('tr').toArray();
|
||||
|
||||
// Определяем направление сортировки
|
||||
const isAscending = !column.hasClass('sort-asc');
|
||||
|
||||
// Убираем классы сортировки со всех заголовков
|
||||
table.find('th').removeClass('sort-asc sort-desc');
|
||||
|
||||
// Добавляем класс сортировки к текущему заголовку
|
||||
column.addClass(isAscending ? 'sort-asc' : 'sort-desc');
|
||||
|
||||
// Сортируем строки
|
||||
rows.sort((a, b) => {
|
||||
const aText = $(a).find('td').eq(column.index()).text();
|
||||
const bText = $(b).find('td').eq(column.index()).text();
|
||||
|
||||
if (isAscending) {
|
||||
return aText.localeCompare(bText, 'ru');
|
||||
} else {
|
||||
return bText.localeCompare(aText, 'ru');
|
||||
}
|
||||
});
|
||||
|
||||
// Перестраиваем таблицу
|
||||
tbody.empty().append(rows);
|
||||
}
|
||||
|
||||
// Экспорт данных
|
||||
exportData() {
|
||||
try {
|
||||
const table = $('#recentOperationsTable');
|
||||
const rows = table.find('tbody tr');
|
||||
let csv = 'Дата,Тип,Товар,Количество,Сотрудник,Статус\n';
|
||||
|
||||
rows.each(function() {
|
||||
const cells = $(this).find('td');
|
||||
if (cells.length > 1) { // Пропускаем пустые строки
|
||||
const row = [];
|
||||
cells.each(function() {
|
||||
row.push(`"${$(this).text().trim()}"`);
|
||||
});
|
||||
csv += row.join(',') + '\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', `операции_${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');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать индикатор загрузки
|
||||
showLoading() {
|
||||
$('main').append(`
|
||||
<div id="loadingOverlay" class="position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center" style="background: rgba(255,255,255,0.8); z-index: 9999;">
|
||||
<div class="text-center">
|
||||
<div class="loading-spinner text-primary mb-2"></div>
|
||||
<p class="text-muted">Загрузка данных...</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Скрыть индикатор загрузки
|
||||
hideLoading() {
|
||||
$('#loadingOverlay').remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация приложения
|
||||
const mainApp = new MainApp();
|
||||
433
js/profile.js
Normal file
433
js/profile.js
Normal file
@ -0,0 +1,433 @@
|
||||
// JavaScript для страницы профиля пользователя
|
||||
|
||||
class ProfileManager {
|
||||
constructor() {
|
||||
this.warehouse = new WarehouseManager();
|
||||
this.currentUser = null;
|
||||
this.userOperations = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
$(document).ready(() => {
|
||||
this.loadProfile();
|
||||
this.bindEvents();
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Обработчики для настроек
|
||||
$('#saveSettingsBtn').on('click', () => this.saveSettings());
|
||||
|
||||
// Обработчики для форм
|
||||
$('#changePasswordForm').on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.savePassword();
|
||||
});
|
||||
|
||||
$('#editProfileForm').on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveProfile();
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка профиля
|
||||
async loadProfile() {
|
||||
try {
|
||||
this.currentUser = auth.getCurrentUser();
|
||||
if (!this.currentUser) {
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayUserInfo();
|
||||
await this.loadUserStatistics();
|
||||
await this.loadUserOperations();
|
||||
this.loadSettings();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке профиля:', error);
|
||||
auth.showNotification('Ошибка при загрузке профиля', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение информации о пользователе
|
||||
displayUserInfo() {
|
||||
// Основная информация
|
||||
$('#userName').text(this.currentUser.name);
|
||||
$('#userPosition').text(this.currentUser.position);
|
||||
$('#userDepartment').text(this.currentUser.department);
|
||||
$('#userEmail').text(this.currentUser.email);
|
||||
|
||||
// Роли
|
||||
const roles = this.currentUser.roles.map(role => this.getRoleDisplayName(role)).join(', ');
|
||||
$('#userRoles').html(`<span class="badge bg-primary me-1">${roles}</span>`);
|
||||
|
||||
// Разрешения
|
||||
const permissions = this.currentUser.permissions.map(perm => this.getPermissionDisplayName(perm)).join(', ');
|
||||
$('#userPermissions').html(`<span class="badge bg-secondary me-1">${permissions}</span>`);
|
||||
|
||||
// Дата регистрации (имитация)
|
||||
const registrationDate = new Date();
|
||||
registrationDate.setMonth(registrationDate.getMonth() - 3); // 3 месяца назад
|
||||
$('#userRegistrationDate').text(registrationDate.toLocaleDateString('ru-RU'));
|
||||
|
||||
// Последний вход
|
||||
const lastLogin = new Date();
|
||||
lastLogin.setHours(lastLogin.getHours() - 2); // 2 часа назад
|
||||
$('#lastLogin').text(lastLogin.toLocaleDateString('ru-RU') + ' ' + lastLogin.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }));
|
||||
}
|
||||
|
||||
// Получение отображаемого названия роли
|
||||
getRoleDisplayName(role) {
|
||||
const roleNames = {
|
||||
'admin': 'Администратор',
|
||||
'manager': 'Менеджер',
|
||||
'operator': 'Оператор',
|
||||
'viewer': 'Наблюдатель'
|
||||
};
|
||||
return roleNames[role] || role;
|
||||
}
|
||||
|
||||
// Получение отображаемого названия разрешения
|
||||
getPermissionDisplayName(permission) {
|
||||
const permissionNames = {
|
||||
'read': 'Чтение',
|
||||
'write': 'Запись',
|
||||
'delete': 'Удаление',
|
||||
'admin': 'Администрирование'
|
||||
};
|
||||
return permissionNames[permission] || permission;
|
||||
}
|
||||
|
||||
// Загрузка статистики пользователя
|
||||
async loadUserStatistics() {
|
||||
try {
|
||||
const operations = await this.warehouse.getOperations();
|
||||
const userOperations = operations.filter(op => op.employeeId === this.currentUser.id);
|
||||
|
||||
// Общее количество операций
|
||||
$('#totalOperations').text(userOperations.length);
|
||||
|
||||
// Операции в этом месяце
|
||||
const thisMonth = new Date().getMonth();
|
||||
const thisYear = new Date().getFullYear();
|
||||
const thisMonthOperations = userOperations.filter(op => {
|
||||
const opDate = new Date(op.date);
|
||||
return opDate.getMonth() === thisMonth && opDate.getFullYear() === thisYear;
|
||||
});
|
||||
$('#thisMonthOperations').text(thisMonthOperations.length);
|
||||
|
||||
// Операции сегодня
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayOperations = userOperations.filter(op => op.date.startsWith(today));
|
||||
$('#todayOperations').text(todayOperations.length);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке статистики:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка операций пользователя
|
||||
async loadUserOperations() {
|
||||
try {
|
||||
const operations = await this.warehouse.getOperations();
|
||||
this.userOperations = operations
|
||||
.filter(op => op.employeeId === this.currentUser.id)
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, 10); // Последние 10 операций
|
||||
|
||||
this.renderUserOperations();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке операций:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Рендеринг операций пользователя
|
||||
renderUserOperations() {
|
||||
const tbody = $('#userOperationsTable tbody');
|
||||
tbody.empty();
|
||||
|
||||
if (this.userOperations.length === 0) {
|
||||
tbody.html(`
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
<i class="fas fa-inbox fa-2x mb-3"></i>
|
||||
<br>У вас пока нет операций
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.userOperations.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>${operation.itemName}</td>
|
||||
<td>${operation.quantity}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Получение класса статуса
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка настроек
|
||||
loadSettings() {
|
||||
// Загружаем настройки из localStorage
|
||||
const settings = JSON.parse(localStorage.getItem('user_settings') || '{}');
|
||||
|
||||
$('#emailNotifications').prop('checked', settings.emailNotifications !== false);
|
||||
$('#lowStockAlerts').prop('checked', settings.lowStockAlerts !== false);
|
||||
$('#operationReports').prop('checked', settings.operationReports || false);
|
||||
|
||||
$('#themeSelect').val(settings.theme || 'light');
|
||||
$('#languageSelect').val(settings.language || 'ru');
|
||||
}
|
||||
|
||||
// Сохранение настроек
|
||||
saveSettings() {
|
||||
const settings = {
|
||||
emailNotifications: $('#emailNotifications').is(':checked'),
|
||||
lowStockAlerts: $('#lowStockAlerts').is(':checked'),
|
||||
operationReports: $('#operationReports').is(':checked'),
|
||||
theme: $('#themeSelect').val(),
|
||||
language: $('#languageSelect').val()
|
||||
};
|
||||
|
||||
localStorage.setItem('user_settings', JSON.stringify(settings));
|
||||
auth.showNotification('Настройки сохранены', 'success');
|
||||
}
|
||||
|
||||
// Смена пароля
|
||||
changePassword() {
|
||||
$('#changePasswordForm')[0].reset();
|
||||
$('#changePasswordModal').modal('show');
|
||||
}
|
||||
|
||||
// Сохранение пароля
|
||||
savePassword() {
|
||||
const currentPassword = $('#currentPassword').val();
|
||||
const newPassword = $('#newPassword').val();
|
||||
const confirmPassword = $('#confirmPassword').val();
|
||||
|
||||
// Валидация
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
auth.showNotification('Пожалуйста, заполните все поля', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
auth.showNotification('Новые пароли не совпадают', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
auth.showNotification('Новый пароль должен содержать минимум 6 символов', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// В реальном приложении здесь был бы запрос к серверу
|
||||
// Для демонстрации просто показываем уведомление
|
||||
auth.showNotification('Пароль успешно изменен', 'success');
|
||||
$('#changePasswordModal').modal('hide');
|
||||
$('#changePasswordForm')[0].reset();
|
||||
}
|
||||
|
||||
// Редактирование профиля
|
||||
editProfile() {
|
||||
// Заполняем форму текущими данными
|
||||
$('#editName').val(this.currentUser.name);
|
||||
$('#editEmail').val(this.currentUser.email);
|
||||
$('#editDepartment').val(this.currentUser.department);
|
||||
$('#editPosition').val(this.currentUser.position);
|
||||
$('#editPhone').val(this.currentUser.phone || '');
|
||||
|
||||
$('#editProfileModal').modal('show');
|
||||
}
|
||||
|
||||
// Сохранение профиля
|
||||
saveProfile() {
|
||||
const formData = {
|
||||
name: $('#editName').val(),
|
||||
email: $('#editEmail').val(),
|
||||
department: $('#editDepartment').val(),
|
||||
position: $('#editPosition').val(),
|
||||
phone: $('#editPhone').val()
|
||||
};
|
||||
|
||||
// Валидация
|
||||
if (!formData.name || !formData.email) {
|
||||
auth.showNotification('Пожалуйста, заполните обязательные поля', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidEmail(formData.email)) {
|
||||
auth.showNotification('Пожалуйста, введите корректный email', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// В реальном приложении здесь был бы запрос к серверу
|
||||
// Для демонстрации обновляем локальные данные
|
||||
Object.assign(this.currentUser, formData);
|
||||
localStorage.setItem('warehouse_user', JSON.stringify(this.currentUser));
|
||||
|
||||
// Обновляем отображение
|
||||
this.displayUserInfo();
|
||||
|
||||
auth.showNotification('Профиль успешно обновлен', 'success');
|
||||
$('#editProfileModal').modal('hide');
|
||||
}
|
||||
|
||||
// Просмотр активности
|
||||
viewActivity() {
|
||||
// В реальном приложении здесь можно открыть страницу с детальной статистикой
|
||||
auth.showNotification('Функция просмотра активности находится в разработке', 'info');
|
||||
}
|
||||
|
||||
// Валидация email
|
||||
isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Получение статистики активности
|
||||
getActivityStats() {
|
||||
const now = new Date();
|
||||
const stats = {
|
||||
today: 0,
|
||||
thisWeek: 0,
|
||||
thisMonth: 0,
|
||||
total: this.userOperations.length
|
||||
};
|
||||
|
||||
this.userOperations.forEach(operation => {
|
||||
const opDate = new Date(operation.date);
|
||||
const diffTime = now - opDate;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) stats.today++;
|
||||
if (diffDays <= 7) stats.thisWeek++;
|
||||
if (diffDays <= 30) stats.thisMonth++;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Экспорт данных пользователя
|
||||
exportUserData() {
|
||||
try {
|
||||
const userData = {
|
||||
profile: this.currentUser,
|
||||
operations: this.userOperations,
|
||||
statistics: this.getActivityStats(),
|
||||
settings: JSON.parse(localStorage.getItem('user_settings') || '{}')
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(userData, null, 2);
|
||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `profile_${this.currentUser.username}_${new Date().toISOString().split('T')[0]}.json`);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Получение рекомендаций
|
||||
getRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
// Рекомендации на основе роли
|
||||
if (this.currentUser.roles.includes('admin')) {
|
||||
recommendations.push({
|
||||
type: 'info',
|
||||
title: 'Административные функции',
|
||||
message: 'У вас есть доступ ко всем функциям системы. Не забудьте регулярно проверять отчеты и управлять пользователями.'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.currentUser.roles.includes('manager')) {
|
||||
recommendations.push({
|
||||
type: 'warning',
|
||||
title: 'Управление складом',
|
||||
message: 'Следите за товарами с низким запасом и контролируйте операции операторов.'
|
||||
});
|
||||
}
|
||||
|
||||
// Рекомендации на основе активности
|
||||
const stats = this.getActivityStats();
|
||||
if (stats.today === 0) {
|
||||
recommendations.push({
|
||||
type: 'success',
|
||||
title: 'Добро пожаловать!',
|
||||
message: 'Сегодня у вас пока нет операций. Начните работу с добавления товаров или оформления операций.'
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация менеджера профиля
|
||||
const profileManager = new ProfileManager();
|
||||
667
js/search.js
Normal file
667
js/search.js
Normal file
@ -0,0 +1,667 @@
|
||||
// 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(() => {
|
||||
this.loadCategories();
|
||||
this.bindEvents();
|
||||
this.initializeFilters();
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка категорий для фильтра
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация менеджера поиска
|
||||
const searchManager = new SearchManager();
|
||||
532
js/warehouse.js
Normal file
532
js/warehouse.js
Normal file
@ -0,0 +1,532 @@
|
||||
// Модуль управления складом
|
||||
|
||||
class WarehouseManager {
|
||||
constructor() {
|
||||
this.items = this.loadItems();
|
||||
this.operations = this.loadOperations();
|
||||
this.categories = this.loadCategories();
|
||||
}
|
||||
|
||||
// Загрузка товаров из localStorage
|
||||
loadItems() {
|
||||
const items = localStorage.getItem('warehouse_items');
|
||||
return items ? JSON.parse(items) : this.getDefaultItems();
|
||||
}
|
||||
|
||||
// Загрузка операций из localStorage
|
||||
loadOperations() {
|
||||
const operations = localStorage.getItem('warehouse_operations');
|
||||
return operations ? JSON.parse(operations) : [];
|
||||
}
|
||||
|
||||
// Загрузка категорий
|
||||
loadCategories() {
|
||||
return [
|
||||
{ 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' }
|
||||
];
|
||||
}
|
||||
|
||||
// Сохранение данных в localStorage
|
||||
saveItems() {
|
||||
localStorage.setItem('warehouse_items', JSON.stringify(this.items));
|
||||
}
|
||||
|
||||
saveOperations() {
|
||||
localStorage.setItem('warehouse_operations', JSON.stringify(this.operations));
|
||||
}
|
||||
|
||||
// Получение товаров по умолчанию
|
||||
getDefaultItems() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Ноутбук Dell Inspiron',
|
||||
category: 'electronics',
|
||||
price: 45000,
|
||||
quantity: 15,
|
||||
description: '15.6" Full HD, Intel Core i5, 8GB RAM, 256GB SSD',
|
||||
barcode: '1234567890123',
|
||||
minQuantity: 5,
|
||||
location: 'Стеллаж A-1',
|
||||
supplier: 'Dell Inc.',
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
updatedAt: '2024-01-15T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Принтер HP LaserJet',
|
||||
category: 'electronics',
|
||||
price: 12000,
|
||||
quantity: 8,
|
||||
description: 'Лазерный принтер, монохромный, 20 стр/мин',
|
||||
barcode: '1234567890124',
|
||||
minQuantity: 3,
|
||||
location: 'Стеллаж A-2',
|
||||
supplier: 'HP Inc.',
|
||||
createdAt: '2024-01-10T14:30:00Z',
|
||||
updatedAt: '2024-01-10T14:30:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Офисный стул',
|
||||
category: 'furniture',
|
||||
price: 3500,
|
||||
quantity: 25,
|
||||
description: 'Офисный стул с подлокотниками, черный',
|
||||
barcode: '1234567890125',
|
||||
minQuantity: 10,
|
||||
location: 'Стеллаж B-1',
|
||||
supplier: 'МебельСтрой',
|
||||
createdAt: '2024-01-05T09:15:00Z',
|
||||
updatedAt: '2024-01-05T09:15:00Z'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Бумага А4',
|
||||
category: 'office',
|
||||
price: 250,
|
||||
quantity: 100,
|
||||
description: 'Бумага А4, 80 г/м², 500 листов',
|
||||
barcode: '1234567890126',
|
||||
minQuantity: 20,
|
||||
location: 'Стеллаж C-1',
|
||||
supplier: 'ОфисМаркет',
|
||||
createdAt: '2024-01-12T11:45:00Z',
|
||||
updatedAt: '2024-01-12T11:45:00Z'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Отвертка крестовая',
|
||||
category: 'tools',
|
||||
price: 150,
|
||||
quantity: 50,
|
||||
description: 'Отвертка крестовая PH2, 150мм',
|
||||
barcode: '1234567890127',
|
||||
minQuantity: 15,
|
||||
location: 'Стеллаж D-1',
|
||||
supplier: 'ИнструментСтрой',
|
||||
createdAt: '2024-01-08T16:20:00Z',
|
||||
updatedAt: '2024-01-08T16:20:00Z'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Получение статистики
|
||||
async getStatistics() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const totalItems = this.items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalValue = this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const lowStockItems = this.items.filter(item => item.quantity <= item.minQuantity).length;
|
||||
const todayOperations = this.operations.filter(op =>
|
||||
op.date.startsWith(today)
|
||||
).length;
|
||||
|
||||
return {
|
||||
totalItems,
|
||||
totalValue,
|
||||
lowStockItems,
|
||||
todayOperations,
|
||||
totalCategories: this.categories.length,
|
||||
totalOperations: this.operations.length
|
||||
};
|
||||
}
|
||||
|
||||
// Получение всех товаров
|
||||
async getAllItems() {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
// Получение товара по ID
|
||||
async getItemById(id) {
|
||||
return this.items.find(item => item.id === parseInt(id));
|
||||
}
|
||||
|
||||
// Добавление нового товара
|
||||
async addItem(itemData) {
|
||||
const newItem = {
|
||||
id: this.getNextItemId(),
|
||||
name: itemData.itemName,
|
||||
category: itemData.itemCategory,
|
||||
price: parseFloat(itemData.itemPrice),
|
||||
quantity: parseInt(itemData.itemQuantity),
|
||||
description: itemData.itemDescription || '',
|
||||
barcode: itemData.itemBarcode || '',
|
||||
minQuantity: parseInt(itemData.itemQuantity) * 0.2, // 20% от начального количества
|
||||
location: 'Новый стеллаж',
|
||||
supplier: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.items.push(newItem);
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию прихода для нового товара
|
||||
await this.createOperation({
|
||||
type: 'incoming',
|
||||
itemId: newItem.id,
|
||||
quantity: newItem.quantity,
|
||||
notes: 'Добавление нового товара'
|
||||
});
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// Обновление товара
|
||||
async updateItem(id, itemData) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === parseInt(id));
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
this.items[itemIndex] = {
|
||||
...this.items[itemIndex],
|
||||
...itemData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveItems();
|
||||
return this.items[itemIndex];
|
||||
}
|
||||
|
||||
// Удаление товара
|
||||
async deleteItem(id) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === parseInt(id));
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
this.items.splice(itemIndex, 1);
|
||||
this.saveItems();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обработка прихода
|
||||
async processIncoming(operationData) {
|
||||
const itemId = parseInt(operationData.incomingItem);
|
||||
const quantity = parseInt(operationData.incomingQuantity);
|
||||
|
||||
const item = await this.getItemById(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
// Обновляем количество товара
|
||||
item.quantity += quantity;
|
||||
item.updatedAt = new Date().toISOString();
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию
|
||||
const operation = await this.createOperation({
|
||||
type: 'incoming',
|
||||
itemId: itemId,
|
||||
quantity: quantity,
|
||||
supplier: operationData.incomingSupplier || '',
|
||||
notes: operationData.incomingNotes || ''
|
||||
});
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Обработка расхода
|
||||
async processOutgoing(operationData) {
|
||||
const itemId = parseInt(operationData.outgoingItem);
|
||||
const quantity = parseInt(operationData.outgoingQuantity);
|
||||
|
||||
const item = await this.getItemById(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
// Проверяем достаточность товара
|
||||
if (item.quantity < quantity) {
|
||||
throw new Error(`Недостаточно товара. Доступно: ${item.quantity} шт.`);
|
||||
}
|
||||
|
||||
// Обновляем количество товара
|
||||
item.quantity -= quantity;
|
||||
item.updatedAt = new Date().toISOString();
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию
|
||||
const operation = await this.createOperation({
|
||||
type: 'outgoing',
|
||||
itemId: itemId,
|
||||
quantity: quantity,
|
||||
recipient: operationData.outgoingRecipient || '',
|
||||
notes: operationData.outgoingNotes || ''
|
||||
});
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Создание операции
|
||||
async createOperation(operationData) {
|
||||
const item = await this.getItemById(operationData.itemId);
|
||||
const currentUser = auth.getCurrentUser();
|
||||
|
||||
const operation = {
|
||||
id: this.getNextOperationId(),
|
||||
type: operationData.type,
|
||||
itemId: operationData.itemId,
|
||||
itemName: item.name,
|
||||
quantity: operationData.quantity,
|
||||
employeeId: currentUser.id,
|
||||
employeeName: currentUser.name,
|
||||
date: new Date().toISOString(),
|
||||
status: 'completed',
|
||||
supplier: operationData.supplier || '',
|
||||
recipient: operationData.recipient || '',
|
||||
notes: operationData.notes || ''
|
||||
};
|
||||
|
||||
this.operations.push(operation);
|
||||
this.saveOperations();
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Получение последних операций
|
||||
async getRecentOperations(limit = 10) {
|
||||
return this.operations
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
// Получение операций по фильтрам
|
||||
async getOperations(filters = {}) {
|
||||
let filteredOperations = [...this.operations];
|
||||
|
||||
// Фильтр по типу
|
||||
if (filters.type) {
|
||||
filteredOperations = filteredOperations.filter(op => op.type === filters.type);
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
if (filters.startDate) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
new Date(op.date) >= new Date(filters.startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
new Date(op.date) <= new Date(filters.endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// Фильтр по товару
|
||||
if (filters.itemId) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
op.itemId === parseInt(filters.itemId)
|
||||
);
|
||||
}
|
||||
|
||||
// Сортировка
|
||||
const sortField = filters.sortBy || 'date';
|
||||
const sortOrder = filters.sortOrder || 'desc';
|
||||
|
||||
filteredOperations.sort((a, b) => {
|
||||
let aValue = a[sortField];
|
||||
let bValue = b[sortField];
|
||||
|
||||
if (sortField === 'date') {
|
||||
aValue = new Date(aValue);
|
||||
bValue = new Date(bValue);
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
return filteredOperations;
|
||||
}
|
||||
|
||||
// Поиск товаров
|
||||
async searchItems(query, filters = {}) {
|
||||
let results = [...this.items];
|
||||
|
||||
// Поиск по названию и описанию
|
||||
if (query) {
|
||||
const searchQuery = query.toLowerCase();
|
||||
results = results.filter(item =>
|
||||
item.name.toLowerCase().includes(searchQuery) ||
|
||||
item.description.toLowerCase().includes(searchQuery) ||
|
||||
item.barcode.includes(searchQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Фильтр по категории
|
||||
if (filters.category) {
|
||||
results = results.filter(item => item.category === filters.category);
|
||||
}
|
||||
|
||||
// Фильтр по цене
|
||||
if (filters.minPrice) {
|
||||
results = results.filter(item => item.price >= filters.minPrice);
|
||||
}
|
||||
|
||||
if (filters.maxPrice) {
|
||||
results = results.filter(item => item.price <= filters.maxPrice);
|
||||
}
|
||||
|
||||
// Фильтр по наличию
|
||||
if (filters.inStock) {
|
||||
results = results.filter(item => item.quantity > 0);
|
||||
}
|
||||
|
||||
if (filters.lowStock) {
|
||||
results = results.filter(item => item.quantity <= item.minQuantity);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Получение товаров с низким запасом
|
||||
async getLowStockItems() {
|
||||
return this.items.filter(item => item.quantity <= item.minQuantity);
|
||||
}
|
||||
|
||||
// Получение категорий
|
||||
async getCategories() {
|
||||
return this.categories;
|
||||
}
|
||||
|
||||
// Добавление категории
|
||||
async addCategory(categoryData) {
|
||||
const newCategory = {
|
||||
id: categoryData.id || this.generateCategoryId(),
|
||||
name: categoryData.name,
|
||||
icon: categoryData.icon || 'fas fa-box'
|
||||
};
|
||||
|
||||
this.categories.push(newCategory);
|
||||
return newCategory;
|
||||
}
|
||||
|
||||
// Генерация следующего ID для товара
|
||||
getNextItemId() {
|
||||
const maxId = Math.max(...this.items.map(item => item.id), 0);
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// Генерация следующего ID для операции
|
||||
getNextOperationId() {
|
||||
const maxId = Math.max(...this.operations.map(op => op.id), 0);
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// Генерация ID для категории
|
||||
generateCategoryId() {
|
||||
return 'category_' + Date.now();
|
||||
}
|
||||
|
||||
// Экспорт данных
|
||||
async exportData(type = 'items') {
|
||||
switch (type) {
|
||||
case 'items':
|
||||
return this.exportItems();
|
||||
case 'operations':
|
||||
return this.exportOperations();
|
||||
case 'statistics':
|
||||
return this.exportStatistics();
|
||||
default:
|
||||
throw new Error('Неизвестный тип экспорта');
|
||||
}
|
||||
}
|
||||
|
||||
// Экспорт товаров
|
||||
exportItems() {
|
||||
const headers = ['ID', 'Название', 'Категория', 'Цена', 'Количество', 'Описание', 'Штрих-код', 'Местоположение'];
|
||||
const rows = this.items.map(item => [
|
||||
item.id,
|
||||
item.name,
|
||||
this.getCategoryName(item.category),
|
||||
item.price,
|
||||
item.quantity,
|
||||
item.description,
|
||||
item.barcode,
|
||||
item.location
|
||||
]);
|
||||
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
// Экспорт операций
|
||||
exportOperations() {
|
||||
const headers = ['ID', 'Дата', 'Тип', 'Товар', 'Количество', 'Сотрудник', 'Статус', 'Примечания'];
|
||||
const rows = this.operations.map(op => [
|
||||
op.id,
|
||||
new Date(op.date).toLocaleDateString('ru-RU'),
|
||||
op.type === 'incoming' ? 'Приход' : 'Расход',
|
||||
op.itemName,
|
||||
op.quantity,
|
||||
op.employeeName,
|
||||
op.status === 'completed' ? 'Завершено' : 'В обработке',
|
||||
op.notes
|
||||
]);
|
||||
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
// Экспорт статистики
|
||||
exportStatistics() {
|
||||
const stats = this.getStatistics();
|
||||
return {
|
||||
headers: ['Показатель', 'Значение'],
|
||||
rows: [
|
||||
['Всего товаров', stats.totalItems],
|
||||
['Общая стоимость', stats.totalValue + ' ₽'],
|
||||
['Товары с низким запасом', stats.lowStockItems],
|
||||
['Операций сегодня', stats.todayOperations],
|
||||
['Всего категорий', stats.totalCategories],
|
||||
['Всего операций', stats.totalOperations]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Получение названия категории
|
||||
getCategoryName(categoryId) {
|
||||
const category = this.categories.find(cat => cat.id === categoryId);
|
||||
return category ? category.name : 'Неизвестная категория';
|
||||
}
|
||||
|
||||
// Получение отчета по движению товаров
|
||||
async getMovementReport(startDate, endDate, itemId = null) {
|
||||
let operations = this.operations.filter(op => {
|
||||
const opDate = new Date(op.date);
|
||||
return opDate >= new Date(startDate) && opDate <= new Date(endDate);
|
||||
});
|
||||
|
||||
if (itemId) {
|
||||
operations = operations.filter(op => op.itemId === parseInt(itemId));
|
||||
}
|
||||
|
||||
const report = {
|
||||
period: { startDate, endDate },
|
||||
totalIncoming: operations.filter(op => op.type === 'incoming').length,
|
||||
totalOutgoing: operations.filter(op => op.type === 'outgoing').length,
|
||||
totalIncomingQuantity: operations
|
||||
.filter(op => op.type === 'incoming')
|
||||
.reduce((sum, op) => sum + op.quantity, 0),
|
||||
totalOutgoingQuantity: operations
|
||||
.filter(op => op.type === 'outgoing')
|
||||
.reduce((sum, op) => sum + op.quantity, 0),
|
||||
operations: operations
|
||||
};
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
// Экспорт для использования в других модулях
|
||||
window.WarehouseManager = WarehouseManager;
|
||||
Reference in New Issue
Block a user