import { NextRequest, NextResponse } from "next/server"; import { getDb } from "@/lib/db"; export async function POST(request: NextRequest) { const sql = getDb(); const body = await request.json(); const entries: { productId: number; batchSize: number }[] = body.entries; if (!entries || entries.length === 0) { return NextResponse.json( { error: "At least one product entry is required" }, { status: 400 } ); } // Fetch product info for all selected products const productIds = entries.map((e) => e.productId); const products = await sql( `SELECT * FROM products WHERE id = ANY($1::int[])`, [productIds] ); const productMap = new Map( products.map((p: Record) => [Number(p.id), p]) ); // Per-product breakdowns and aggregate component demand const componentDemand = new Map< number, { component_id: number; part_number: string; component_name: string; manufacturer: string | null; supplier: string | null; supplier_part_number: string | null; shelf_location: string | null; unit_price: number; stock_quantity: number; package_type: string | null; category_name: string | null; is_internal: boolean; total_needed: number; sources: { product_name: string; sku: string; qty_per_unit: number; batch_size: number; subtotal: number }[]; } >(); const perProduct: { product: Record; batchSize: number; items: Record[]; totalCost: number; totalParts: number; }[] = []; for (const entry of entries) { const product = productMap.get(entry.productId); if (!product) continue; const bomItems = await sql( `SELECT bi.id as bom_item_id, bi.quantity as quantity_per_unit, bi.reference_designator, c.id as component_id, c.part_number, c.name as component_name, c.manufacturer, c.supplier, c.supplier_part_number, c.shelf_location, c.unit_price, c.stock_quantity, c.package_type, c.is_internal, cat.name as category_name, (bi.quantity * $2) as total_needed, (bi.quantity * $2 * c.unit_price) as line_cost FROM bom_items bi JOIN components c ON bi.component_id = c.id LEFT JOIN categories cat ON c.category_id = cat.id WHERE bi.product_id = $1 ORDER BY c.name ASC`, [entry.productId, entry.batchSize] ); const totalCost = bomItems.reduce( (sum: number, item: Record) => sum + Number(item.line_cost), 0 ); const totalParts = bomItems.reduce( (sum: number, item: Record) => sum + Number(item.total_needed), 0 ); perProduct.push({ product, batchSize: entry.batchSize, items: bomItems, totalCost, totalParts, }); // Aggregate component demand across products for (const item of bomItems) { const cid = Number(item.component_id); const existing = componentDemand.get(cid); const needed = Number(item.total_needed); if (existing) { existing.total_needed += needed; existing.sources.push({ product_name: product.name as string, sku: product.sku as string, qty_per_unit: Number(item.quantity_per_unit), batch_size: entry.batchSize, subtotal: needed, }); } else { componentDemand.set(cid, { component_id: cid, part_number: item.part_number as string, component_name: item.component_name as string, manufacturer: (item.manufacturer as string) || null, supplier: (item.supplier as string) || null, supplier_part_number: (item.supplier_part_number as string) || null, shelf_location: (item.shelf_location as string) || null, unit_price: Number(item.unit_price), stock_quantity: Number(item.stock_quantity), package_type: (item.package_type as string) || null, category_name: (item.category_name as string) || null, is_internal: Boolean(item.is_internal), total_needed: needed, sources: [ { product_name: product.name as string, sku: product.sku as string, qty_per_unit: Number(item.quantity_per_unit), batch_size: entry.batchSize, subtotal: needed, }, ], }); } } } // Compute shortages against shared stock const aggregatedItems = Array.from(componentDemand.values()).map((item) => ({ ...item, shortage: Math.max(0, item.total_needed - item.stock_quantity), line_cost: item.total_needed * item.unit_price, })); aggregatedItems.sort((a, b) => a.component_name.localeCompare(b.component_name)); const shortageItems = aggregatedItems.filter((item) => item.shortage > 0); const totalComponents = aggregatedItems.length; const totalParts = aggregatedItems.reduce((s, i) => s + i.total_needed, 0); const totalCost = aggregatedItems.reduce((s, i) => s + i.line_cost, 0); return NextResponse.json({ entries: perProduct.map((p) => ({ product: p.product, batchSize: p.batchSize, totalCost: p.totalCost, totalParts: p.totalParts, itemCount: p.items.length, })), summary: { totalProducts: perProduct.length, totalComponents, totalParts, totalCost, shortageCount: shortageItems.length, allInStock: shortageItems.length === 0, }, items: aggregatedItems, purchaseList: shortageItems.map((item) => ({ component_id: item.component_id, part_number: item.part_number, component_name: item.component_name, manufacturer: item.manufacturer, supplier: item.supplier || "N/A", supplier_part_number: item.supplier_part_number || "N/A", shelf_location: item.shelf_location || "N/A", unit_price: item.unit_price, stock_quantity: item.stock_quantity, total_needed: item.total_needed, shortage: item.shortage, purchase_cost: item.shortage * item.unit_price, category_name: item.category_name, is_internal: item.is_internal, sources: item.sources, })), }); }