From 13c64c503eba0713ffe7d97aff5b2a8fcfd4eeac Mon Sep 17 00:00:00 2001
From: cyrteL <167682858+cyrteL@users.noreply.github.com>
Date: Mon, 28 Jul 2025 14:21:39 +0300
Subject: [PATCH] Add files via upload
v0.2(css + js)
---
css/style.css | 443 ++++++++++++++++++++++++++++++++
js/auth.js | 340 ++++++++++++++++++++++++
js/catalog.js | 638 +++++++++++++++++++++++++++++++++++++++++++++
js/history.js | 620 ++++++++++++++++++++++++++++++++++++++++++++
js/main.js | 430 +++++++++++++++++++++++++++++++
js/profile.js | 433 +++++++++++++++++++++++++++++++
js/search.js | 667 ++++++++++++++++++++++++++++++++++++++++++++++++
js/warehouse.js | 532 ++++++++++++++++++++++++++++++++++++++
8 files changed, 4103 insertions(+)
create mode 100644 css/style.css
create mode 100644 js/auth.js
create mode 100644 js/catalog.js
create mode 100644 js/history.js
create mode 100644 js/main.js
create mode 100644 js/profile.js
create mode 100644 js/search.js
create mode 100644 js/warehouse.js
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..1d31fce
--- /dev/null
+++ b/css/style.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/js/auth.js b/js/auth.js
new file mode 100644
index 0000000..f61a5ce
--- /dev/null
+++ b/js/auth.js
@@ -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(`
+ ${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 = $(`
+
+ `);
+
+ // Создаем контейнер для уведомлений, если его нет
+ let toastContainer = $('#toastContainer');
+ if (toastContainer.length === 0) {
+ toastContainer = $('');
+ $('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;
\ No newline at end of file
diff --git a/js/catalog.js b/js/catalog.js
new file mode 100644
index 0000000..d8549bc
--- /dev/null
+++ b/js/catalog.js
@@ -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 = ``;
+ 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(`
+
+
+
+
Товары не найдены
+
Попробуйте изменить параметры поиска или фильтры
+
+
+ `);
+ 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 `
+
+
+
+
+
+
+
${item.name}
+
${item.description.substring(0, 60)}${item.description.length > 60 ? '...' : ''}
+
+ ${category.name}
+ ${stockStatus.text}
+
+
+
+
Цена
+
${item.price.toLocaleString()} ₽
+
+
+
Количество
+
${item.quantity} шт.
+
+
+
+
+ ${auth.hasPermission('write') ? `
+
+ ` : ''}
+
+
+
+
+ `;
+ }
+
+ // Создание строки товара (список)
+ createListItem(item) {
+ const category = this.getCategoryInfo(item.category);
+ const stockStatus = this.getStockStatus(item);
+
+ return `
+
+
+
+
+
+
+
${item.name}
+ ${item.description.substring(0, 50)}${item.description.length > 50 ? '...' : ''}
+
+
+ ${category.name}
+
+
+
+
${item.price.toLocaleString()} ₽
+
${item.quantity} шт.
+
+
+
+ ${stockStatus.text}
+
+
+
+
+
+
+ `;
+ }
+
+ // Рендеринг пагинации
+ renderPagination(totalPages) {
+ const pagination = $('#pagination');
+ pagination.empty();
+
+ if (totalPages <= 1) return;
+
+ // Кнопка "Предыдущая"
+ const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
+ pagination.append(`
+
+
+
+
+
+ `);
+
+ // Номера страниц
+ 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(`
+
+ ${i}
+
+ `);
+ }
+
+ // Кнопка "Следующая"
+ const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
+ pagination.append(`
+
+
+
+
+
+ `);
+ }
+
+ // Переход на страницу
+ 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(`
+
+
+
Загрузка каталога...
+
+ `);
+ }
+
+ // Скрыть индикатор загрузки
+ hideLoading() {
+ // Загрузка завершается в renderCatalog()
+ }
+}
+
+// Инициализация менеджера каталога
+const catalogManager = new CatalogManager();
\ No newline at end of file
diff --git a/js/history.js b/js/history.js
new file mode 100644
index 0000000..890e144
--- /dev/null
+++ b/js/history.js
@@ -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 = ``;
+ itemFilter.append(option);
+ reportItem.append(option);
+ });
+ }
+
+ // Заполнение фильтра сотрудников
+ populateEmployeeFilter() {
+ const employeeFilter = $('#employeeFilter');
+ const employees = auth.getUsers();
+
+ employees.forEach(employee => {
+ const 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(`
+
+
+
+ Операции не найдены
+ |
+
+ `);
+ 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 `
+
+ | ${this.formatDate(operation.date)} |
+
+
+ ${typeText}
+ |
+ ${operation.itemName} |
+ ${operation.quantity} |
+ ${operation.employeeName} |
+
+ ${statusText}
+ |
+
+
+ |
+
+ `;
+ }
+
+ // Рендеринг пагинации
+ renderPagination(totalPages) {
+ const pagination = $('#pagination');
+ pagination.empty();
+
+ if (totalPages <= 1) return;
+
+ // Кнопка "Предыдущая"
+ const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
+ pagination.append(`
+
+
+
+
+
+ `);
+
+ // Номера страниц
+ 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(`
+
+ ${i}
+
+ `);
+ }
+
+ // Кнопка "Следующая"
+ const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
+ pagination.append(`
+
+
+
+
+
+ `);
+ }
+
+ // Переход на страницу
+ 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 = `
+
+
+
Основная информация
+
+
+ | ID операции: |
+ ${operation.id} |
+
+
+ | Тип: |
+
+
+ ${typeText}
+ |
+
+
+ | Дата: |
+ ${this.formatDate(operation.date)} |
+
+
+ | Статус: |
+ ${statusText} |
+
+
+
+
+
Детали товара
+
+
+ | Товар: |
+ ${operation.itemName} |
+
+
+ | Количество: |
+ ${operation.quantity} шт. |
+
+
+ | Сотрудник: |
+ ${operation.employeeName} |
+
+
+
+
+ ${operation.supplier ? `
+
+
+
Дополнительная информация
+
+ ${operation.supplier ? `| Поставщик: | ${operation.supplier} |
` : ''}
+ ${operation.recipient ? `| Получатель: | ${operation.recipient} |
` : ''}
+ ${operation.notes ? `| Примечания: | ${operation.notes} |
` : ''}
+
+
+
+ ` : ''}
+ `;
+
+ $('#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(`
+
+ |
+
+ Загрузка истории операций...
+ |
+
+ `);
+ }
+
+ // Скрыть индикатор загрузки
+ hideLoading() {
+ // Загрузка завершается в renderOperations()
+ }
+}
+
+// Инициализация менеджера истории
+const historyManager = new HistoryManager();
\ No newline at end of file
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..b2411b9
--- /dev/null
+++ b/js/main.js
@@ -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(`
+
+
+
+ Нет операций для отображения
+ |
+
+ `);
+ 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 `
+
+ | ${this.formatDate(operation.date)} |
+
+
+ ${typeText}
+ |
+ ${operation.itemName} |
+ ${operation.quantity} |
+ ${operation.employeeName} |
+
+ ${statusText}
+ |
+
+ `;
+ }
+
+ // Получение класса статуса
+ 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(``);
+ });
+ });
+ }
+
+ // Добавление нового товара
+ 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(`
+
+ `);
+ }
+
+ // Скрыть индикатор загрузки
+ hideLoading() {
+ $('#loadingOverlay').remove();
+ }
+}
+
+// Инициализация приложения
+const mainApp = new MainApp();
\ No newline at end of file
diff --git a/js/profile.js b/js/profile.js
new file mode 100644
index 0000000..b34fd22
--- /dev/null
+++ b/js/profile.js
@@ -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(`${roles}`);
+
+ // Разрешения
+ const permissions = this.currentUser.permissions.map(perm => this.getPermissionDisplayName(perm)).join(', ');
+ $('#userPermissions').html(`${permissions}`);
+
+ // Дата регистрации (имитация)
+ 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(`
+
+
+
+ У вас пока нет операций
+ |
+
+ `);
+ 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 `
+
+ | ${this.formatDate(operation.date)} |
+
+
+ ${typeText}
+ |
+ ${operation.itemName} |
+ ${operation.quantity} |
+
+ ${statusText}
+ |
+
+ `;
+ }
+
+ // Получение класса статуса
+ 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();
\ No newline at end of file
diff --git a/js/search.js b/js/search.js
new file mode 100644
index 0000000..fdbaeee
--- /dev/null
+++ b/js/search.js
@@ -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 = ``;
+ 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(`
+
+ Результаты не найдены
+ По запросу "${this.currentQuery}" ничего не найдено. Попробуйте изменить поисковый запрос или фильтры.
+ `).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(`
+
+
+
+
Товары не найдены
+
Попробуйте изменить поисковый запрос
+
+
+ `);
+ 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 `
+
+
+
+
+
+
+
${this.highlightQuery(item.name)}
+
${this.highlightQuery(item.description.substring(0, 80))}${item.description.length > 80 ? '...' : ''}
+
+ ${category.name}
+ ${stockStatus.text}
+
+
+
+
Цена
+
${item.price.toLocaleString()} ₽
+
+
+
Количество
+
${item.quantity} шт.
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ // Рендеринг результатов операций
+ renderOperationsResults() {
+ const tbody = $('#operationsResultsTable tbody');
+ tbody.empty();
+
+ if (this.searchResults.operations.length === 0) {
+ tbody.html(`
+
+
+
+ Операции не найдены
+ |
+
+ `);
+ 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 `
+
+ | ${this.formatDate(operation.date)} |
+
+
+ ${typeText}
+ |
+ ${this.highlightQuery(operation.itemName)} |
+ ${operation.quantity} |
+ ${this.highlightQuery(operation.employeeName)} |
+
+ ${statusText}
+ |
+
+ `;
+ }
+
+ // Подсветка поискового запроса
+ highlightQuery(text) {
+ if (!this.currentQuery) return text;
+
+ const regex = new RegExp(`(${this.escapeRegex(this.currentQuery)})`, 'gi');
+ return text.replace(regex, '$1');
+ }
+
+ // Экранирование специальных символов для 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(`
+
+
+
+
+
+ `);
+
+ // Номера страниц
+ 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(`
+
+ ${i}
+
+ `);
+ }
+
+ // Кнопка "Следующая"
+ const nextDisabled = this.currentPage === totalPages ? 'disabled' : '';
+ pagination.append(`
+
+
+
+
+
+ `);
+ }
+
+ // Переход на страницу
+ 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('Расширенные фильтры');
+ } else {
+ advancedFilters.slideDown();
+ toggleBtn.html('Скрыть фильтры');
+ }
+ }
+
+ // Очистка фильтров
+ 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 = `
+
+
+
Основная информация
+
+
+ | ID: |
+ ${item.id} |
+
+
+ | Название: |
+ ${item.name} |
+
+
+ | Категория: |
+ ${category.name} |
+
+
+ | Цена: |
+ ${item.price.toLocaleString()} ₽ |
+
+
+
+
+
Складская информация
+
+
+ | Количество: |
+ ${item.quantity} шт. |
+
+
+ | Статус: |
+ ${stockStatus.text} |
+
+
+ | Местоположение: |
+ ${item.location} |
+
+
+ | Поставщик: |
+ ${item.supplier || 'Не указан'} |
+
+
+
+
+
+
+
Описание
+
${item.description || 'Описание отсутствует'}
+
+
+ ${item.barcode ? `
+
+
+
Штрих-код
+
${item.barcode}
+
+
+ ` : ''}
+ `;
+
+ $('#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();
\ No newline at end of file
diff --git a/js/warehouse.js b/js/warehouse.js
new file mode 100644
index 0000000..10543cf
--- /dev/null
+++ b/js/warehouse.js
@@ -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;
\ No newline at end of file