551 lines
19 KiB
JavaScript
551 lines
19 KiB
JavaScript
// Модуль управления складом
|
||
|
||
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() {
|
||
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() {
|
||
return this.items;
|
||
}
|
||
|
||
// Синхронное получение всех позиций
|
||
getAllItemsSync() {
|
||
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 || '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()
|
||
};
|
||
|
||
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('Позиция не найдена');
|
||
}
|
||
|
||
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('Позиция не найдена');
|
||
}
|
||
|
||
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();
|
||
|
||
// Создаем операцию
|
||
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();
|
||
|
||
// Создаем операцию
|
||
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;
|
||
}
|
||
|
||
// Получение категории по 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()
|
||
};
|
||
|
||
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('Категория не найдена');
|
||
}
|
||
|
||
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('Категория не найдена');
|
||
}
|
||
|
||
// Удаляем все позиции этой категории
|
||
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;
|