Files
website-secure/app/api/production/route.ts
T
pm 9c14a19a87
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
chore: upload parte 1 - logica
2026-06-01 15:35:53 +01:00

199 lines
6.2 KiB
TypeScript

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<string, unknown>) => [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<string, unknown>;
batchSize: number;
items: Record<string, unknown>[];
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<string, number>) =>
sum + Number(item.line_cost),
0
);
const totalParts = bomItems.reduce(
(sum: number, item: Record<string, number>) =>
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,
})),
});
}