chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/db";
|
||||
|
||||
interface SourceInfo {
|
||||
product_name: string;
|
||||
sku: string;
|
||||
qty_per_unit: number;
|
||||
batch_size: number;
|
||||
subtotal: number;
|
||||
}
|
||||
|
||||
interface AcceptItem {
|
||||
component_id: number;
|
||||
part_number: string;
|
||||
component_name: string;
|
||||
category_name: string | null;
|
||||
package_type: string | null;
|
||||
shelf_location: string | null;
|
||||
unit_price: number;
|
||||
stock_quantity: number;
|
||||
total_needed: number;
|
||||
shortage: number;
|
||||
is_internal: boolean;
|
||||
manufacturer: string | null;
|
||||
supplier: string | null;
|
||||
supplier_part_number: string | null;
|
||||
sources: SourceInfo[];
|
||||
}
|
||||
|
||||
interface AcceptProduct {
|
||||
product_name: string;
|
||||
sku: string;
|
||||
batch_size: number;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const sql = getDb();
|
||||
const body = await request.json();
|
||||
const { name, items, products, summary, link_po_ids } = body as {
|
||||
name: string;
|
||||
items: AcceptItem[];
|
||||
products: AcceptProduct[];
|
||||
summary: {
|
||||
totalProducts: number;
|
||||
totalComponents: number;
|
||||
totalParts: number;
|
||||
totalCost: number;
|
||||
shortageCount: number;
|
||||
};
|
||||
link_po_ids?: number[];
|
||||
};
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No items to accept" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const planName =
|
||||
name ||
|
||||
`Production Plan ${new Date().toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" })}`;
|
||||
|
||||
// 1. Create the planning history record
|
||||
const phRows = await sql(
|
||||
`INSERT INTO planning_history (name, status, products_json, summary_json)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[planName, "accepted", JSON.stringify(products || []), JSON.stringify(summary || {})]
|
||||
);
|
||||
const plan = phRows[0];
|
||||
|
||||
// 2. Insert all items with full data snapshots
|
||||
for (const item of items) {
|
||||
await sql(
|
||||
`INSERT INTO planning_history_items
|
||||
(planning_id, component_id, part_number, component_name, category_name,
|
||||
package_type, shelf_location, unit_price, stock_at_time,
|
||||
quantity_needed, shortage, is_internal,
|
||||
manufacturer, supplier, supplier_part_number, sources_json)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,
|
||||
[
|
||||
plan.id,
|
||||
item.component_id,
|
||||
item.part_number,
|
||||
item.component_name,
|
||||
item.category_name || null,
|
||||
item.package_type || null,
|
||||
item.shelf_location || null,
|
||||
item.unit_price || 0,
|
||||
item.stock_quantity || 0,
|
||||
item.total_needed,
|
||||
item.shortage || 0,
|
||||
item.is_internal,
|
||||
item.manufacturer || null,
|
||||
item.supplier || null,
|
||||
item.supplier_part_number || null,
|
||||
JSON.stringify(item.sources || []),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Decrement stock for ALL components (both internal and external)
|
||||
for (const item of items) {
|
||||
await sql(
|
||||
`UPDATE components SET stock_quantity = GREATEST(0, stock_quantity - $1) WHERE id = $2`,
|
||||
[item.total_needed, item.component_id]
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Link selected Supplier POs to this planning record
|
||||
if (link_po_ids && link_po_ids.length > 0) {
|
||||
for (const poId of link_po_ids) {
|
||||
await sql(
|
||||
`UPDATE supplier_pos SET planning_id = $1 WHERE id = $2`,
|
||||
[plan.id, poId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
plan,
|
||||
message: "Planning accepted. History saved and stock decremented.",
|
||||
}, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error accepting production plan:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to accept production plan" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user