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.recipient ? `` : ''} + ${operation.notes ? `` : ''} +
    Поставщик:${operation.supplier}
    Получатель:${operation.recipient}
    Примечания:${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