feat: модульная архитектура sidebar и улучшения навигации

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-30 15:51:41 +03:00
parent 8391f40e87
commit b40ac083ab
128 changed files with 9366 additions and 17283 deletions

View File

@ -0,0 +1,249 @@
import { prisma } from '@/lib/prisma'
/**
* СИСТЕМА УПРАВЛЕНИЯ СКЛАДСКИМИ ОСТАТКАМИ V2
*
* Автоматически обновляет инвентарь при:
* - Приемке поставок (увеличивает остатки)
* - Отгрузке селлерам (уменьшает остатки)
* - Списании брака (уменьшает остатки)
*/
export interface InventoryMovement {
fulfillmentCenterId: string
productId: string
quantity: number
type: 'INCOMING' | 'OUTGOING' | 'DEFECT'
sourceId: string // ID поставки или отгрузки
sourceType: 'SUPPLY_ORDER' | 'SELLER_SHIPMENT' | 'DEFECT_WRITEOFF'
unitCost?: number // Себестоимость для расчета средней цены
notes?: string
}
/**
* Основная функция обновления инвентаря
*/
export async function updateInventory(movement: InventoryMovement): Promise<void> {
const { fulfillmentCenterId, productId, quantity, type, unitCost } = movement
// Находим или создаем запись в инвентаре
const inventory = await prisma.fulfillmentConsumableInventory.upsert({
where: {
fulfillmentCenterId_productId: {
fulfillmentCenterId,
productId,
},
},
create: {
fulfillmentCenterId,
productId,
currentStock: type === 'INCOMING' ? quantity : -quantity,
totalReceived: type === 'INCOMING' ? quantity : 0,
totalShipped: type === 'OUTGOING' ? quantity : 0,
averageCost: unitCost || 0,
lastSupplyDate: type === 'INCOMING' ? new Date() : undefined,
lastUsageDate: type === 'OUTGOING' ? new Date() : undefined,
},
update: {
// Обновляем остатки в зависимости от типа движения
currentStock: {
increment: type === 'INCOMING' ? quantity : -quantity,
},
totalReceived: {
increment: type === 'INCOMING' ? quantity : 0,
},
totalShipped: {
increment: type === 'OUTGOING' ? quantity : 0,
},
lastSupplyDate: type === 'INCOMING' ? new Date() : undefined,
lastUsageDate: type === 'OUTGOING' ? new Date() : undefined,
},
include: {
product: true,
},
})
// Пересчитываем среднюю себестоимость при поступлении
if (type === 'INCOMING' && unitCost) {
await recalculateAverageCost(inventory.id, quantity, unitCost)
}
console.log('✅ Inventory updated:', {
productName: inventory.product.name,
movement: `${type === 'INCOMING' ? '+' : '-'}${quantity}`,
newStock: inventory.currentStock,
fulfillmentCenter: fulfillmentCenterId,
})
}
/**
* Пересчет средней себестоимости по методу взвешенной средней
*/
async function recalculateAverageCost(inventoryId: string, newQuantity: number, newUnitCost: number): Promise<void> {
const inventory = await prisma.fulfillmentConsumableInventory.findUnique({
where: { id: inventoryId },
})
if (!inventory) return
// Рассчитываем новую среднюю стоимость
const oldTotalCost = parseFloat(inventory.averageCost.toString()) * (inventory.currentStock - newQuantity)
const newTotalCost = newUnitCost * newQuantity
const totalQuantity = inventory.currentStock
const newAverageCost = totalQuantity > 0 ? (oldTotalCost + newTotalCost) / totalQuantity : newUnitCost
await prisma.fulfillmentConsumableInventory.update({
where: { id: inventoryId },
data: {
averageCost: newAverageCost,
},
})
}
/**
* Обработка приемки поставки V2
*/
export async function processSupplyOrderReceipt(
supplyOrderId: string,
items: Array<{
productId: string
receivedQuantity: number
unitPrice: number
}>,
): Promise<void> {
console.log(`🔄 Processing supply order receipt: ${supplyOrderId}`)
// Получаем информацию о поставке
const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: supplyOrderId },
include: { fulfillmentCenter: true },
})
if (!supplyOrder) {
throw new Error(`Supply order not found: ${supplyOrderId}`)
}
// Обрабатываем каждую позицию
for (const item of items) {
await updateInventory({
fulfillmentCenterId: supplyOrder.fulfillmentCenterId,
productId: item.productId,
quantity: item.receivedQuantity,
type: 'INCOMING',
sourceId: supplyOrderId,
sourceType: 'SUPPLY_ORDER',
unitCost: item.unitPrice,
notes: `Приемка заказа ${supplyOrderId}`,
})
}
console.log(`✅ Supply order ${supplyOrderId} processed successfully`)
}
/**
* Обработка отгрузки селлеру
*/
export async function processSellerShipment(
fulfillmentCenterId: string,
sellerId: string,
items: Array<{
productId: string
shippedQuantity: number
}>,
): Promise<void> {
console.log(`🔄 Processing seller shipment to ${sellerId}`)
// Обрабатываем каждую позицию
for (const item of items) {
// Проверяем достаточность остатков
const inventory = await prisma.fulfillmentConsumableInventory.findUnique({
where: {
fulfillmentCenterId_productId: {
fulfillmentCenterId,
productId: item.productId,
},
},
include: { product: true },
})
if (!inventory || inventory.currentStock < item.shippedQuantity) {
throw new Error(
`Insufficient stock for product ${inventory?.product.name}. ` +
`Available: ${inventory?.currentStock || 0}, Required: ${item.shippedQuantity}`,
)
}
await updateInventory({
fulfillmentCenterId,
productId: item.productId,
quantity: item.shippedQuantity,
type: 'OUTGOING',
sourceId: sellerId,
sourceType: 'SELLER_SHIPMENT',
notes: `Отгрузка селлеру ${sellerId}`,
})
}
console.log(`✅ Seller shipment to ${sellerId} processed successfully`)
}
/**
* Проверка критически низких остатков
*/
export async function checkLowStockAlerts(fulfillmentCenterId: string): Promise<Array<{
productId: string
productName: string
currentStock: number
minStock: number
}>> {
const lowStockItems = await prisma.fulfillmentConsumableInventory.findMany({
where: {
fulfillmentCenterId,
OR: [
{ currentStock: { lte: prisma.fulfillmentConsumableInventory.fields.minStock } },
{ currentStock: 0 },
],
},
include: {
product: true,
},
})
return lowStockItems.map(item => ({
productId: item.productId,
productName: item.product.name,
currentStock: item.currentStock,
minStock: item.minStock,
}))
}
/**
* Получение статистики склада
*/
export async function getInventoryStats(fulfillmentCenterId: string) {
const stats = await prisma.fulfillmentConsumableInventory.aggregate({
where: { fulfillmentCenterId },
_count: { id: true },
_sum: {
currentStock: true,
totalReceived: true,
totalShipped: true,
},
})
const lowStockCount = await prisma.fulfillmentConsumableInventory.count({
where: {
fulfillmentCenterId,
currentStock: { lte: prisma.fulfillmentConsumableInventory.fields.minStock },
},
})
return {
totalProducts: stats._count.id,
totalStock: stats._sum.currentStock || 0,
totalReceived: stats._sum.totalReceived || 0,
totalShipped: stats._sum.totalShipped || 0,
lowStockCount,
}
}