Add files via upload

v0.2(css + js)
This commit is contained in:
cyrteL
2025-07-28 14:21:39 +03:00
committed by GitHub
parent 0308719223
commit 13c64c503e
8 changed files with 4103 additions and 0 deletions

443
css/style.css Normal file
View File

@ -0,0 +1,443 @@
/* Основные стили для системы управления складом */
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #0dcaf0;
--light-color: #f8f9fa;
--dark-color: #212529;
--border-radius: 0.375rem;
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--transition: all 0.15s ease-in-out;
}
/* Общие стили */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
line-height: 1.6;
}
/* Навигация */
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
}
.navbar-nav .nav-link {
font-weight: 500;
transition: var(--transition);
}
.navbar-nav .nav-link:hover {
color: rgba(255, 255, 255, 0.8) !important;
}
/* Карточки быстрых действий */
.quick-action-card {
transition: var(--transition);
border: 1px solid rgba(0, 0, 0, 0.125);
box-shadow: var(--box-shadow);
}
.quick-action-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.quick-action-card .card-body {
padding: 2rem 1.5rem;
}
/* Градиентный фон для приветственного блока */
.bg-gradient-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
}
/* Статистические карточки */
.card.bg-primary,
.card.bg-success,
.card.bg-warning,
.card.bg-info {
border: none;
box-shadow: var(--box-shadow);
transition: var(--transition);
}
.card.bg-primary:hover,
.card.bg-success:hover,
.card.bg-warning:hover,
.card.bg-info:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
/* Таблицы */
.table {
background-color: white;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--box-shadow);
}
.table thead th {
background-color: var(--light-color);
border-bottom: 2px solid #dee2e6;
font-weight: 600;
text-transform: uppercase;
font-size: 0.875rem;
letter-spacing: 0.5px;
}
.table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
/* Модальные окна */
.modal-content {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.modal-header {
background-color: var(--light-color);
border-bottom: 1px solid #dee2e6;
}
.modal-title {
font-weight: 600;
}
/* Формы */
.form-control,
.form-select {
border-radius: var(--border-radius);
border: 1px solid #ced4da;
transition: var(--transition);
}
.form-control:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.form-label {
font-weight: 500;
color: var(--dark-color);
margin-bottom: 0.5rem;
}
/* Кнопки */
.btn {
border-radius: var(--border-radius);
font-weight: 500;
transition: var(--transition);
text-transform: none;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.btn-warning {
background-color: var(--warning-color);
border-color: var(--warning-color);
color: var(--dark-color);
}
.btn-info {
background-color: var(--info-color);
border-color: var(--info-color);
color: white;
}
/* Статусы операций */
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 50rem;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-completed {
background-color: rgba(25, 135, 84, 0.1);
color: var(--success-color);
}
.status-pending {
background-color: rgba(255, 193, 7, 0.1);
color: var(--warning-color);
}
.status-cancelled {
background-color: rgba(220, 53, 69, 0.1);
color: var(--danger-color);
}
/* Анимации */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
/* Адаптивность */
@media (max-width: 768px) {
.quick-action-card .card-body {
padding: 1.5rem 1rem;
}
.card-body h4 {
font-size: 1.5rem;
}
.table-responsive {
font-size: 0.875rem;
}
}
/* Стили для страницы авторизации */
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: white;
border-radius: 1rem;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.2);
overflow: hidden;
max-width: 400px;
width: 100%;
}
.login-header {
background: var(--primary-color);
color: white;
padding: 2rem;
text-align: center;
}
.login-body {
padding: 2rem;
}
/* Стили для каталога */
.catalog-filters {
background: white;
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--box-shadow);
}
.product-card {
transition: var(--transition);
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: var(--border-radius);
overflow: hidden;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.product-image {
height: 200px;
background-color: var(--light-color);
display: flex;
align-items: center;
justify-content: center;
color: var(--secondary-color);
}
/* Стили для поиска */
.search-container {
background: white;
border-radius: var(--border-radius);
padding: 2rem;
box-shadow: var(--box-shadow);
margin-bottom: 2rem;
}
.search-input {
border-radius: 50rem;
padding-left: 1rem;
padding-right: 1rem;
}
/* Стили для профиля */
.profile-header {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
color: white;
padding: 3rem 0;
margin-bottom: 2rem;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
margin: 0 auto 1rem;
}
/* Стили для администрирования */
.admin-sidebar {
background: white;
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--box-shadow);
height: fit-content;
}
.admin-nav .nav-link {
color: var(--dark-color);
padding: 0.75rem 1rem;
border-radius: var(--border-radius);
transition: var(--transition);
}
.admin-nav .nav-link:hover,
.admin-nav .nav-link.active {
background-color: var(--primary-color);
color: white;
}
/* Утилиты */
.text-muted {
color: var(--secondary-color) !important;
}
.border-light {
border-color: #dee2e6 !important;
}
.shadow-sm {
box-shadow: var(--box-shadow) !important;
}
/* Стили для уведомлений */
.toast {
border-radius: var(--border-radius);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.toast-header {
background-color: var(--light-color);
border-bottom: 1px solid #dee2e6;
}
/* Стили для загрузки */
.loading-spinner {
display: inline-block;
width: 2rem;
height: 2rem;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spinner-border 0.75s linear infinite;
}
@keyframes spinner-border {
to {
transform: rotate(360deg);
}
}
/* Стили для пустых состояний */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--secondary-color);
}
.empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Стили для пагинации */
.pagination .page-link {
border-radius: var(--border-radius);
margin: 0 0.125rem;
border: 1px solid #dee2e6;
color: var(--primary-color);
}
.pagination .page-link:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* Стили для подвала */
footer {
margin-top: auto;
}
/* Стили для мобильной навигации */
@media (max-width: 991.98px) {
.navbar-collapse {
background-color: rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius);
padding: 1rem;
margin-top: 1rem;
}
}
/* Стили для печати */
@media print {
.navbar,
.btn,
.modal,
footer {
display: none !important;
}
.card {
border: 1px solid #000 !important;
box-shadow: none !important;
}
}

340
js/auth.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;