Files
sklad_legacy/js/warehouse.js
2025-08-22 19:26:43 +03:00

880 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Модуль управления складом
class WarehouseManager {
constructor() {
this.apiBase = (window && window.API_BASE) ? window.API_BASE : 'http://localhost:3001/api';
this.items = this.loadItems();
this.operations = this.loadOperations();
this.categories = this.loadCategories();
}
// Универсальный вызов API с таймаутом и graceful fallback
async apiRequest(path, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 5000);
try {
const res = await fetch(`${this.apiBase}${path}`, {
method: options.method || 'GET',
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
credentials: 'omit'
});
if (!res.ok) throw new Error(`API ${res.status}`);
return await res.json();
} finally {
clearTimeout(timeout);
}
}
// Отправка уведомления о низком остатке через API
async sendLowStockEmailNotification(items) {
try {
const response = await this.apiRequest('/notifications/low-stock', {
method: 'POST',
body: { items }
});
console.log('Уведомления о низком остатке отправлены:', response);
return response;
} catch (error) {
console.error('Ошибка отправки уведомлений о низком остатке:', error);
throw error;
}
}
// Отправка уведомления об операции через API
async sendOperationEmailNotificationAPI(operation) {
try {
const response = await this.apiRequest('/notifications/operation', {
method: 'POST',
body: { operation }
});
console.log('Уведомления об операции отправлены:', response);
return response;
} catch (error) {
console.error('Ошибка отправки уведомлений об операции:', error);
throw error;
}
}
// Отправка еженедельного отчета через API
async sendWeeklyReportEmailNotification(report) {
try {
const response = await this.apiRequest('/notifications/weekly-report', {
method: 'POST',
body: { report }
});
console.log('Еженедельные отчеты отправлены:', response);
return response;
} catch (error) {
console.error('Ошибка отправки еженедельных отчетов:', error);
throw error;
}
}
// Тестовая отправка email
async sendTestEmail(email, subject, message) {
try {
const response = await this.apiRequest('/notifications/test', {
method: 'POST',
body: { email, subject, message }
});
console.log('Тестовое уведомление отправлено:', response);
return response;
} catch (error) {
console.error('Ошибка отправки тестового уведомления:', error);
throw error;
}
}
// Загрузка позиций из 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() {
const categories = localStorage.getItem('warehouse_categories');
if (categories) {
this.categories = JSON.parse(categories);
} else {
// Загружаем категории по умолчанию
this.categories = this.getDefaultCategories();
this.saveCategories();
}
// Обновляем количество позиций в каждой категории
this.updateCategoryItemCounts();
}
// Получение категорий по умолчанию
getDefaultCategories() {
return [];
}
// Сохранение категорий
saveCategories() {
localStorage.setItem('warehouse_categories', JSON.stringify(this.categories));
}
// Обновление количества позиций в категориях
updateCategoryItemCounts() {
this.categories.forEach(category => {
const itemsInCategory = this.items.filter(item => item.category === category.id);
category.itemCount = itemsInCategory.length;
category.totalQuantity = itemsInCategory.reduce((sum, item) => sum + (item.quantity || 0), 0);
});
this.saveCategories();
}
// Сохранение данных в localStorage
saveItems() {
localStorage.setItem('warehouse_items', JSON.stringify(this.items));
}
saveOperations() {
localStorage.setItem('warehouse_operations', JSON.stringify(this.operations));
}
// Получение позиций по умолчанию
getDefaultItems() {
return [];
}
// Получение статистики
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() {
try {
const data = await this.apiRequest('/items');
this.items = (data || []).map(row => ({
id: row.id,
name: row.name,
category: row.category_id || 'uncategorized',
price: Number(row.price || 0),
quantity: Number(row.quantity || 0),
description: row.description || '',
barcode: row.barcode || '',
minQuantity: Number(row.min_quantity || 0),
location: row.location || '',
supplier: row.supplier || '',
createdAt: row.created_at,
updatedAt: row.updated_at
}));
this.saveItems();
this.updateCategoryItemCounts();
return this.items;
} catch (e) {
return this.items;
}
}
// Синхронное получение всех позиций
getAllItemsSync() {
return this.items;
}
// Получение позиции по ID
async getItemById(id) {
try {
const row = await this.apiRequest(`/items/${id}`);
return {
id: row.id,
name: row.name,
category: row.category_id || 'uncategorized',
price: Number(row.price || 0),
quantity: Number(row.quantity || 0),
description: row.description || '',
barcode: row.barcode || '',
minQuantity: Number(row.min_quantity || 0),
location: row.location || '',
supplier: row.supplier || '',
createdAt: row.created_at,
updatedAt: row.updated_at
};
} catch (e) {
return this.items.find(item => item.id === parseInt(id));
}
}
// Добавление новой позиции
async addItem(itemData) {
const newItem = {
id: this.getNextItemId(),
name: itemData.itemName,
category: itemData.itemCategory || 'uncategorized',
price: parseFloat(itemData.itemPrice),
quantity: parseInt(itemData.itemQuantity),
description: itemData.itemDescription || '',
barcode: itemData.itemBarcode || '',
minQuantity: parseInt(itemData.itemQuantity) * 0.2, // 20% от начального количества
location: itemData.itemLocation || 'Новый стеллаж',
supplier: itemData.itemSupplier || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// Попытка создать через API
try {
const created = await this.apiRequest('/items', {
method: 'POST',
body: {
name: newItem.name,
category_id: newItem.category === 'uncategorized' ? null : newItem.category,
price: newItem.price,
quantity: newItem.quantity,
description: newItem.description,
barcode: newItem.barcode,
min_quantity: newItem.minQuantity,
location: newItem.location,
supplier: newItem.supplier
}
});
if (created && created.id) newItem.id = created.id;
} catch (e) {}
this.items.push(newItem);
this.saveItems();
this.updateCategoryItemCounts();
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('Позиция не найдена');
}
try {
await this.apiRequest(`/items/${id}`, {
method: 'PUT',
body: {
name: itemData.name,
category_id: itemData.category === 'uncategorized' ? null : itemData.category,
price: itemData.price,
quantity: itemData.quantity,
description: itemData.description,
barcode: itemData.barcode,
min_quantity: itemData.minQuantity,
location: itemData.location,
supplier: itemData.supplier
}
});
} catch (e) {}
this.items[itemIndex] = {
...this.items[itemIndex],
...itemData,
updatedAt: new Date().toISOString()
};
this.saveItems();
// Обновляем количество товаров в категории
this.updateCategoryItemCounts();
return this.items[itemIndex];
}
// Удаление позиции
async deleteItem(id) {
const itemIndex = this.items.findIndex(item => item.id === parseInt(id));
if (itemIndex === -1) {
throw new Error('Позиция не найдена');
}
try { await this.apiRequest(`/items/${id}`, { method: 'DELETE' }); } catch (e) {}
this.items.splice(itemIndex, 1);
this.saveItems();
// Обновляем количество товаров в категории
this.updateCategoryItemCounts();
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();
// Обновляем количество товаров в категории
this.updateCategoryItemCounts();
// Создаем операцию
try {
await this.apiRequest('/operations/incoming', {
method: 'POST',
body: { itemId, quantity, supplier: operationData.incomingSupplier || '', notes: operationData.incomingNotes || '', employeeId: auth.getCurrentUser()?.id }
});
} catch (e) {}
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();
// Обновляем количество товаров в категории
this.updateCategoryItemCounts();
// Создаем операцию
try {
await this.apiRequest('/operations/outgoing', {
method: 'POST',
body: { itemId, quantity, recipient: operationData.outgoingRecipient || '', notes: operationData.outgoingNotes || '', employeeId: auth.getCurrentUser()?.id }
});
} catch (e) {}
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();
// Проверяем и отправляем уведомления
await this.checkAndSendNotifications(operation);
return operation;
}
// Получение последних операций
async getRecentOperations(limit = 10) {
try {
const ops = await this.apiRequest(`/operations?sortBy=date&sortOrder=desc`);
this.syncOperationsFromApi(ops);
return this.operations.slice(0, limit);
} catch (e) {
return this.operations
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, limit);
}
}
// Получение операций по фильтрам
async getOperations(filters = {}) {
try {
const q = new URLSearchParams();
if (filters.type) q.set('type', filters.type);
if (filters.startDate) q.set('startDate', filters.startDate);
if (filters.endDate) q.set('endDate', filters.endDate);
if (filters.itemId) q.set('itemId', filters.itemId);
if (filters.sortBy) q.set('sortBy', filters.sortBy);
if (filters.sortOrder) q.set('sortOrder', filters.sortOrder);
const ops = await this.apiRequest(`/operations?${q.toString()}`);
this.syncOperationsFromApi(ops);
return this.operations;
} catch (e) {}
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;
}
// Приведение формата операций из API и сохранение в localStorage
syncOperationsFromApi(apiOps) {
this.operations = (apiOps || []).map(op => ({
id: op.id,
type: op.type,
itemId: op.itemId || op.item_id,
itemName: op.itemName || op.item_name || op.itemName,
quantity: Number(op.quantity || 0),
employeeId: op.employeeId || op.employee_id,
employeeName: op.employeeName || op.employee_name,
date: op.date,
status: op.status || 'completed',
supplier: op.supplier || '',
recipient: op.recipient || '',
notes: op.notes || ''
})).sort((a, b) => new Date(b.date) - new Date(a.date));
this.saveOperations();
}
// Поиск товаров
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 checkAndSendNotifications(operation = null) {
try {
const settings = JSON.parse(localStorage.getItem('user_settings') || '{}');
const currentUser = JSON.parse(localStorage.getItem('warehouse_user') || '{}');
// Проверяем оповещения о низком остатке
if (settings.lowStockAlerts) {
await this.checkLowStockAlerts(currentUser);
}
// Проверяем email уведомления об операциях
if (settings.emailNotifications && operation) {
await this.sendOperationEmailNotification(operation, currentUser);
}
} catch (error) {
console.error('Ошибка при отправке уведомлений:', error);
}
}
// Проверка товаров с низким запасом
async checkLowStockAlerts(user) {
try {
const lowStockItems = await this.getLowStockItems();
if (lowStockItems.length > 0) {
// Показываем уведомление в интерфейсе
const itemNames = lowStockItems.map(item => item.name).join(', ');
auth.showNotification(
`Внимание! Товары с низким запасом: ${itemNames}`,
'warning'
);
// Отправляем email уведомление через API
await this.sendLowStockEmailNotification(lowStockItems);
}
} catch (error) {
console.error('Ошибка при проверке низкого остатка:', error);
}
}
// Отправка уведомления об операции
async sendOperationEmailNotification(operation, user) {
try {
const operationType = operation.type === 'incoming' ? 'Приход' : 'Расход';
const message = `Операция ${operationType}: ${operation.itemName} - ${operation.quantity} шт.`;
// Отправляем email уведомление через API
await this.sendOperationEmailNotificationAPI(operation);
// Показываем уведомление в интерфейсе
auth.showNotification(`Уведомление об операции отправлено на ${user.email}`, 'info');
} catch (error) {
console.error('Ошибка при отправке уведомления об операции:', error);
}
}
// Генерация и отправка еженедельных отчетов
async generateWeeklyReports() {
try {
const settings = JSON.parse(localStorage.getItem('user_settings') || '{}');
const currentUser = JSON.parse(localStorage.getItem('warehouse_user') || '{}');
if (settings.operationReports) {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const weeklyOperations = this.operations.filter(op =>
new Date(op.date) >= weekAgo
);
if (weeklyOperations.length > 0) {
const report = {
period: 'За последнюю неделю',
totalOperations: weeklyOperations.length,
incoming: weeklyOperations.filter(op => op.type === 'incoming').length,
outgoing: weeklyOperations.filter(op => op.type === 'outgoing').length,
operations: weeklyOperations
};
// Отправляем еженедельный отчет через API
await this.sendWeeklyReportEmailNotification(report);
auth.showNotification('Еженедельный отчет отправлен на email', 'success');
}
}
} catch (error) {
console.error('Ошибка при генерации еженедельного отчета:', error);
}
}
// Получение категорий
async getCategories() {
try {
const rows = await this.apiRequest('/categories');
this.categories = (rows || []).map(r => ({
id: r.id,
name: r.name,
description: r.description || '',
icon: r.icon || 'fas fa-tag',
color: r.color || '#2c5aa0',
active: !!(r.active ?? 1),
itemCount: Number(r.itemCount || 0)
}));
this.saveCategories();
return this.categories;
} catch (e) {
return this.categories;
}
}
// Получение категории по ID
async getCategoryById(id) {
return this.categories.find(cat => cat.id === id);
}
// Добавление категории
async addCategory(categoryData) {
const newCategory = {
id: this.generateCategoryId(),
...categoryData,
itemCount: 0,
createdAt: new Date().toISOString()
};
try {
const created = await this.apiRequest('/categories', { method: 'POST', body: newCategory });
if (created && created.id) newCategory.id = created.id;
} catch (e) {}
this.categories.push(newCategory);
this.saveCategories();
return newCategory;
}
// Обновление категории
async updateCategory(id, categoryData) {
const categoryIndex = this.categories.findIndex(cat => cat.id === id);
if (categoryIndex === -1) {
throw new Error('Категория не найдена');
}
try { await this.apiRequest(`/categories/${id}`, { method: 'PUT', body: categoryData }); } catch (e) {}
this.categories[categoryIndex] = {
...this.categories[categoryIndex],
...categoryData,
updatedAt: new Date().toISOString()
};
this.saveCategories();
return this.categories[categoryIndex];
}
// Удаление категории
async deleteCategory(id) {
const categoryIndex = this.categories.findIndex(cat => cat.id === id);
if (categoryIndex === -1) {
throw new Error('Категория не найдена');
}
try { await this.apiRequest(`/categories/${id}`, { method: 'DELETE' }); } catch (e) {}
// Удаляем все позиции этой категории
this.items = this.items.filter(item => item.category !== id);
this.saveItems();
// Удаляем категорию
this.categories.splice(categoryIndex, 1);
this.saveCategories();
// Обновляем количество позиций в остальных категориях
this.updateCategoryItemCounts();
return true;
}
// Генерация следующего 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;