Add files via upload
v0.2(css + js)
This commit is contained in:
532
js/warehouse.js
Normal file
532
js/warehouse.js
Normal file
@ -0,0 +1,532 @@
|
||||
// Модуль управления складом
|
||||
|
||||
class WarehouseManager {
|
||||
constructor() {
|
||||
this.items = this.loadItems();
|
||||
this.operations = this.loadOperations();
|
||||
this.categories = this.loadCategories();
|
||||
}
|
||||
|
||||
// Загрузка товаров из localStorage
|
||||
loadItems() {
|
||||
const items = localStorage.getItem('warehouse_items');
|
||||
return items ? JSON.parse(items) : this.getDefaultItems();
|
||||
}
|
||||
|
||||
// Загрузка операций из localStorage
|
||||
loadOperations() {
|
||||
const operations = localStorage.getItem('warehouse_operations');
|
||||
return operations ? JSON.parse(operations) : [];
|
||||
}
|
||||
|
||||
// Загрузка категорий
|
||||
loadCategories() {
|
||||
return [
|
||||
{ id: 'electronics', name: 'Электроника', icon: 'fas fa-laptop' },
|
||||
{ id: 'clothing', name: 'Одежда', icon: 'fas fa-tshirt' },
|
||||
{ id: 'tools', name: 'Инструменты', icon: 'fas fa-tools' },
|
||||
{ id: 'office', name: 'Офисные принадлежности', icon: 'fas fa-briefcase' },
|
||||
{ id: 'furniture', name: 'Мебель', icon: 'fas fa-couch' },
|
||||
{ id: 'books', name: 'Книги', icon: 'fas fa-book' }
|
||||
];
|
||||
}
|
||||
|
||||
// Сохранение данных в localStorage
|
||||
saveItems() {
|
||||
localStorage.setItem('warehouse_items', JSON.stringify(this.items));
|
||||
}
|
||||
|
||||
saveOperations() {
|
||||
localStorage.setItem('warehouse_operations', JSON.stringify(this.operations));
|
||||
}
|
||||
|
||||
// Получение товаров по умолчанию
|
||||
getDefaultItems() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Ноутбук Dell Inspiron',
|
||||
category: 'electronics',
|
||||
price: 45000,
|
||||
quantity: 15,
|
||||
description: '15.6" Full HD, Intel Core i5, 8GB RAM, 256GB SSD',
|
||||
barcode: '1234567890123',
|
||||
minQuantity: 5,
|
||||
location: 'Стеллаж A-1',
|
||||
supplier: 'Dell Inc.',
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
updatedAt: '2024-01-15T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Принтер HP LaserJet',
|
||||
category: 'electronics',
|
||||
price: 12000,
|
||||
quantity: 8,
|
||||
description: 'Лазерный принтер, монохромный, 20 стр/мин',
|
||||
barcode: '1234567890124',
|
||||
minQuantity: 3,
|
||||
location: 'Стеллаж A-2',
|
||||
supplier: 'HP Inc.',
|
||||
createdAt: '2024-01-10T14:30:00Z',
|
||||
updatedAt: '2024-01-10T14:30:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Офисный стул',
|
||||
category: 'furniture',
|
||||
price: 3500,
|
||||
quantity: 25,
|
||||
description: 'Офисный стул с подлокотниками, черный',
|
||||
barcode: '1234567890125',
|
||||
minQuantity: 10,
|
||||
location: 'Стеллаж B-1',
|
||||
supplier: 'МебельСтрой',
|
||||
createdAt: '2024-01-05T09:15:00Z',
|
||||
updatedAt: '2024-01-05T09:15:00Z'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Бумага А4',
|
||||
category: 'office',
|
||||
price: 250,
|
||||
quantity: 100,
|
||||
description: 'Бумага А4, 80 г/м², 500 листов',
|
||||
barcode: '1234567890126',
|
||||
minQuantity: 20,
|
||||
location: 'Стеллаж C-1',
|
||||
supplier: 'ОфисМаркет',
|
||||
createdAt: '2024-01-12T11:45:00Z',
|
||||
updatedAt: '2024-01-12T11:45:00Z'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Отвертка крестовая',
|
||||
category: 'tools',
|
||||
price: 150,
|
||||
quantity: 50,
|
||||
description: 'Отвертка крестовая PH2, 150мм',
|
||||
barcode: '1234567890127',
|
||||
minQuantity: 15,
|
||||
location: 'Стеллаж D-1',
|
||||
supplier: 'ИнструментСтрой',
|
||||
createdAt: '2024-01-08T16:20:00Z',
|
||||
updatedAt: '2024-01-08T16:20:00Z'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Получение статистики
|
||||
async getStatistics() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const totalItems = this.items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalValue = this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const lowStockItems = this.items.filter(item => item.quantity <= item.minQuantity).length;
|
||||
const todayOperations = this.operations.filter(op =>
|
||||
op.date.startsWith(today)
|
||||
).length;
|
||||
|
||||
return {
|
||||
totalItems,
|
||||
totalValue,
|
||||
lowStockItems,
|
||||
todayOperations,
|
||||
totalCategories: this.categories.length,
|
||||
totalOperations: this.operations.length
|
||||
};
|
||||
}
|
||||
|
||||
// Получение всех товаров
|
||||
async getAllItems() {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
// Получение товара по ID
|
||||
async getItemById(id) {
|
||||
return this.items.find(item => item.id === parseInt(id));
|
||||
}
|
||||
|
||||
// Добавление нового товара
|
||||
async addItem(itemData) {
|
||||
const newItem = {
|
||||
id: this.getNextItemId(),
|
||||
name: itemData.itemName,
|
||||
category: itemData.itemCategory,
|
||||
price: parseFloat(itemData.itemPrice),
|
||||
quantity: parseInt(itemData.itemQuantity),
|
||||
description: itemData.itemDescription || '',
|
||||
barcode: itemData.itemBarcode || '',
|
||||
minQuantity: parseInt(itemData.itemQuantity) * 0.2, // 20% от начального количества
|
||||
location: 'Новый стеллаж',
|
||||
supplier: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.items.push(newItem);
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию прихода для нового товара
|
||||
await this.createOperation({
|
||||
type: 'incoming',
|
||||
itemId: newItem.id,
|
||||
quantity: newItem.quantity,
|
||||
notes: 'Добавление нового товара'
|
||||
});
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// Обновление товара
|
||||
async updateItem(id, itemData) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === parseInt(id));
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
this.items[itemIndex] = {
|
||||
...this.items[itemIndex],
|
||||
...itemData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveItems();
|
||||
return this.items[itemIndex];
|
||||
}
|
||||
|
||||
// Удаление товара
|
||||
async deleteItem(id) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === parseInt(id));
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
this.items.splice(itemIndex, 1);
|
||||
this.saveItems();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обработка прихода
|
||||
async processIncoming(operationData) {
|
||||
const itemId = parseInt(operationData.incomingItem);
|
||||
const quantity = parseInt(operationData.incomingQuantity);
|
||||
|
||||
const item = await this.getItemById(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
// Обновляем количество товара
|
||||
item.quantity += quantity;
|
||||
item.updatedAt = new Date().toISOString();
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию
|
||||
const operation = await this.createOperation({
|
||||
type: 'incoming',
|
||||
itemId: itemId,
|
||||
quantity: quantity,
|
||||
supplier: operationData.incomingSupplier || '',
|
||||
notes: operationData.incomingNotes || ''
|
||||
});
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Обработка расхода
|
||||
async processOutgoing(operationData) {
|
||||
const itemId = parseInt(operationData.outgoingItem);
|
||||
const quantity = parseInt(operationData.outgoingQuantity);
|
||||
|
||||
const item = await this.getItemById(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Товар не найден');
|
||||
}
|
||||
|
||||
// Проверяем достаточность товара
|
||||
if (item.quantity < quantity) {
|
||||
throw new Error(`Недостаточно товара. Доступно: ${item.quantity} шт.`);
|
||||
}
|
||||
|
||||
// Обновляем количество товара
|
||||
item.quantity -= quantity;
|
||||
item.updatedAt = new Date().toISOString();
|
||||
this.saveItems();
|
||||
|
||||
// Создаем операцию
|
||||
const operation = await this.createOperation({
|
||||
type: 'outgoing',
|
||||
itemId: itemId,
|
||||
quantity: quantity,
|
||||
recipient: operationData.outgoingRecipient || '',
|
||||
notes: operationData.outgoingNotes || ''
|
||||
});
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Создание операции
|
||||
async createOperation(operationData) {
|
||||
const item = await this.getItemById(operationData.itemId);
|
||||
const currentUser = auth.getCurrentUser();
|
||||
|
||||
const operation = {
|
||||
id: this.getNextOperationId(),
|
||||
type: operationData.type,
|
||||
itemId: operationData.itemId,
|
||||
itemName: item.name,
|
||||
quantity: operationData.quantity,
|
||||
employeeId: currentUser.id,
|
||||
employeeName: currentUser.name,
|
||||
date: new Date().toISOString(),
|
||||
status: 'completed',
|
||||
supplier: operationData.supplier || '',
|
||||
recipient: operationData.recipient || '',
|
||||
notes: operationData.notes || ''
|
||||
};
|
||||
|
||||
this.operations.push(operation);
|
||||
this.saveOperations();
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
// Получение последних операций
|
||||
async getRecentOperations(limit = 10) {
|
||||
return this.operations
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
// Получение операций по фильтрам
|
||||
async getOperations(filters = {}) {
|
||||
let filteredOperations = [...this.operations];
|
||||
|
||||
// Фильтр по типу
|
||||
if (filters.type) {
|
||||
filteredOperations = filteredOperations.filter(op => op.type === filters.type);
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
if (filters.startDate) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
new Date(op.date) >= new Date(filters.startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
new Date(op.date) <= new Date(filters.endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// Фильтр по товару
|
||||
if (filters.itemId) {
|
||||
filteredOperations = filteredOperations.filter(op =>
|
||||
op.itemId === parseInt(filters.itemId)
|
||||
);
|
||||
}
|
||||
|
||||
// Сортировка
|
||||
const sortField = filters.sortBy || 'date';
|
||||
const sortOrder = filters.sortOrder || 'desc';
|
||||
|
||||
filteredOperations.sort((a, b) => {
|
||||
let aValue = a[sortField];
|
||||
let bValue = b[sortField];
|
||||
|
||||
if (sortField === 'date') {
|
||||
aValue = new Date(aValue);
|
||||
bValue = new Date(bValue);
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
return filteredOperations;
|
||||
}
|
||||
|
||||
// Поиск товаров
|
||||
async searchItems(query, filters = {}) {
|
||||
let results = [...this.items];
|
||||
|
||||
// Поиск по названию и описанию
|
||||
if (query) {
|
||||
const searchQuery = query.toLowerCase();
|
||||
results = results.filter(item =>
|
||||
item.name.toLowerCase().includes(searchQuery) ||
|
||||
item.description.toLowerCase().includes(searchQuery) ||
|
||||
item.barcode.includes(searchQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Фильтр по категории
|
||||
if (filters.category) {
|
||||
results = results.filter(item => item.category === filters.category);
|
||||
}
|
||||
|
||||
// Фильтр по цене
|
||||
if (filters.minPrice) {
|
||||
results = results.filter(item => item.price >= filters.minPrice);
|
||||
}
|
||||
|
||||
if (filters.maxPrice) {
|
||||
results = results.filter(item => item.price <= filters.maxPrice);
|
||||
}
|
||||
|
||||
// Фильтр по наличию
|
||||
if (filters.inStock) {
|
||||
results = results.filter(item => item.quantity > 0);
|
||||
}
|
||||
|
||||
if (filters.lowStock) {
|
||||
results = results.filter(item => item.quantity <= item.minQuantity);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Получение товаров с низким запасом
|
||||
async getLowStockItems() {
|
||||
return this.items.filter(item => item.quantity <= item.minQuantity);
|
||||
}
|
||||
|
||||
// Получение категорий
|
||||
async getCategories() {
|
||||
return this.categories;
|
||||
}
|
||||
|
||||
// Добавление категории
|
||||
async addCategory(categoryData) {
|
||||
const newCategory = {
|
||||
id: categoryData.id || this.generateCategoryId(),
|
||||
name: categoryData.name,
|
||||
icon: categoryData.icon || 'fas fa-box'
|
||||
};
|
||||
|
||||
this.categories.push(newCategory);
|
||||
return newCategory;
|
||||
}
|
||||
|
||||
// Генерация следующего ID для товара
|
||||
getNextItemId() {
|
||||
const maxId = Math.max(...this.items.map(item => item.id), 0);
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// Генерация следующего ID для операции
|
||||
getNextOperationId() {
|
||||
const maxId = Math.max(...this.operations.map(op => op.id), 0);
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// Генерация ID для категории
|
||||
generateCategoryId() {
|
||||
return 'category_' + Date.now();
|
||||
}
|
||||
|
||||
// Экспорт данных
|
||||
async exportData(type = 'items') {
|
||||
switch (type) {
|
||||
case 'items':
|
||||
return this.exportItems();
|
||||
case 'operations':
|
||||
return this.exportOperations();
|
||||
case 'statistics':
|
||||
return this.exportStatistics();
|
||||
default:
|
||||
throw new Error('Неизвестный тип экспорта');
|
||||
}
|
||||
}
|
||||
|
||||
// Экспорт товаров
|
||||
exportItems() {
|
||||
const headers = ['ID', 'Название', 'Категория', 'Цена', 'Количество', 'Описание', 'Штрих-код', 'Местоположение'];
|
||||
const rows = this.items.map(item => [
|
||||
item.id,
|
||||
item.name,
|
||||
this.getCategoryName(item.category),
|
||||
item.price,
|
||||
item.quantity,
|
||||
item.description,
|
||||
item.barcode,
|
||||
item.location
|
||||
]);
|
||||
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
// Экспорт операций
|
||||
exportOperations() {
|
||||
const headers = ['ID', 'Дата', 'Тип', 'Товар', 'Количество', 'Сотрудник', 'Статус', 'Примечания'];
|
||||
const rows = this.operations.map(op => [
|
||||
op.id,
|
||||
new Date(op.date).toLocaleDateString('ru-RU'),
|
||||
op.type === 'incoming' ? 'Приход' : 'Расход',
|
||||
op.itemName,
|
||||
op.quantity,
|
||||
op.employeeName,
|
||||
op.status === 'completed' ? 'Завершено' : 'В обработке',
|
||||
op.notes
|
||||
]);
|
||||
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
// Экспорт статистики
|
||||
exportStatistics() {
|
||||
const stats = this.getStatistics();
|
||||
return {
|
||||
headers: ['Показатель', 'Значение'],
|
||||
rows: [
|
||||
['Всего товаров', stats.totalItems],
|
||||
['Общая стоимость', stats.totalValue + ' ₽'],
|
||||
['Товары с низким запасом', stats.lowStockItems],
|
||||
['Операций сегодня', stats.todayOperations],
|
||||
['Всего категорий', stats.totalCategories],
|
||||
['Всего операций', stats.totalOperations]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Получение названия категории
|
||||
getCategoryName(categoryId) {
|
||||
const category = this.categories.find(cat => cat.id === categoryId);
|
||||
return category ? category.name : 'Неизвестная категория';
|
||||
}
|
||||
|
||||
// Получение отчета по движению товаров
|
||||
async getMovementReport(startDate, endDate, itemId = null) {
|
||||
let operations = this.operations.filter(op => {
|
||||
const opDate = new Date(op.date);
|
||||
return opDate >= new Date(startDate) && opDate <= new Date(endDate);
|
||||
});
|
||||
|
||||
if (itemId) {
|
||||
operations = operations.filter(op => op.itemId === parseInt(itemId));
|
||||
}
|
||||
|
||||
const report = {
|
||||
period: { startDate, endDate },
|
||||
totalIncoming: operations.filter(op => op.type === 'incoming').length,
|
||||
totalOutgoing: operations.filter(op => op.type === 'outgoing').length,
|
||||
totalIncomingQuantity: operations
|
||||
.filter(op => op.type === 'incoming')
|
||||
.reduce((sum, op) => sum + op.quantity, 0),
|
||||
totalOutgoingQuantity: operations
|
||||
.filter(op => op.type === 'outgoing')
|
||||
.reduce((sum, op) => sum + op.quantity, 0),
|
||||
operations: operations
|
||||
};
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
// Экспорт для использования в других модулях
|
||||
window.WarehouseManager = WarehouseManager;
|
||||
Reference in New Issue
Block a user