315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getDb } from "@/lib/db";
|
|
|
|
export async function GET(
|
|
_request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const sql = getDb();
|
|
const { id } = await params;
|
|
|
|
const poRows = await sql(
|
|
`SELECT sp.*, s.name AS supplier_name, s.code AS supplier_code,
|
|
s.email AS supplier_email, s.phone AS supplier_phone,
|
|
s.address AS supplier_address, s.contact_person AS supplier_contact,
|
|
s.country AS supplier_country, s.currency AS supplier_currency
|
|
FROM supplier_pos sp
|
|
JOIN suppliers s ON s.id = sp.supplier_id
|
|
WHERE sp.id = $1`,
|
|
[Number(id)]
|
|
);
|
|
|
|
if (poRows.length === 0) {
|
|
return NextResponse.json({ error: "Supplier PO not found" }, { status: 404 });
|
|
}
|
|
|
|
const items = await sql(
|
|
`SELECT * FROM supplier_po_items WHERE supplier_po_id = $1 ORDER BY id ASC`,
|
|
[Number(id)]
|
|
);
|
|
|
|
return NextResponse.json({ ...poRows[0], items });
|
|
} catch (error) {
|
|
console.error("Supplier PO GET error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to fetch supplier PO" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function PUT(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const sql = getDb();
|
|
const { id } = await params;
|
|
const body = await request.json();
|
|
const { status, notes, received_items } = body;
|
|
|
|
// Validate forward-only status transitions (plus cancelled from ordered)
|
|
if (status) {
|
|
const STATUS_ORDER = ["draft", "ordered", "partially_received", "received"];
|
|
const currentRows = await sql(`SELECT status FROM supplier_pos WHERE id = $1`, [Number(id)]);
|
|
if (currentRows.length > 0) {
|
|
const currentStatus = currentRows[0].status as string;
|
|
|
|
// Special case: cancellation is only allowed from "ordered"
|
|
if (status === "cancelled") {
|
|
if (currentStatus !== "ordered") {
|
|
return NextResponse.json(
|
|
{ error: `Cannot cancel a PO with status "${currentStatus}". Only ordered POs (with no received quantities) can be cancelled.` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
// Verify no quantities have been received
|
|
const receivedCheck = await sql(
|
|
`SELECT COALESCE(SUM(received_quantity), 0) AS total_received FROM supplier_po_items WHERE supplier_po_id = $1`,
|
|
[Number(id)]
|
|
);
|
|
if (Number(receivedCheck[0].total_received) > 0) {
|
|
return NextResponse.json(
|
|
{ error: "Cannot cancel a PO that has already received quantities." },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
} else {
|
|
const currentIdx = STATUS_ORDER.indexOf(currentStatus);
|
|
const newIdx = STATUS_ORDER.indexOf(status);
|
|
if (newIdx < 0 || newIdx <= currentIdx) {
|
|
return NextResponse.json(
|
|
{ error: `Cannot change status from "${currentStatus}" to "${status}". Status can only move forward.` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update received quantities per item if provided, adjust stock and log transactions
|
|
if (received_items && Array.isArray(received_items)) {
|
|
// Fetch PO info for transaction records
|
|
const poInfo = await sql(
|
|
`SELECT sp.po_number, s.name AS supplier_name
|
|
FROM supplier_pos sp JOIN suppliers s ON s.id = sp.supplier_id
|
|
WHERE sp.id = $1`,
|
|
[Number(id)]
|
|
);
|
|
const poNumber = poInfo[0]?.po_number || "";
|
|
const supplierName = poInfo[0]?.supplier_name || "";
|
|
|
|
for (const ri of received_items) {
|
|
const newReceivedQty = Number(ri.received_quantity);
|
|
|
|
// Get current received qty and component_id before updating
|
|
const currentItem = await sql(
|
|
`SELECT received_quantity, component_id, unit_price FROM supplier_po_items WHERE id = $1 AND supplier_po_id = $2`,
|
|
[Number(ri.id), Number(id)]
|
|
);
|
|
if (currentItem.length === 0) continue;
|
|
|
|
const prevReceivedQty = Number(currentItem[0].received_quantity) || 0;
|
|
const componentId = currentItem[0].component_id;
|
|
const unitPrice = Number(currentItem[0].unit_price) || 0;
|
|
const delta = newReceivedQty - prevReceivedQty;
|
|
|
|
// Update the received quantity on the PO item
|
|
await sql(
|
|
`UPDATE supplier_po_items SET received_quantity = $1 WHERE id = $2 AND supplier_po_id = $3`,
|
|
[newReceivedQty, Number(ri.id), Number(id)]
|
|
);
|
|
|
|
// If there's a positive delta and a valid component, update stock and log transaction
|
|
if (delta > 0 && componentId) {
|
|
console.log(`[PO Receive] Adding stock: component=${componentId}, delta=${delta}, prevReceived=${prevReceivedQty}, newReceived=${newReceivedQty}, PO=${poNumber}`);
|
|
|
|
await sql(
|
|
`UPDATE components SET stock_quantity = stock_quantity + $1, updated_at = NOW() WHERE id = $2`,
|
|
[delta, Number(componentId)]
|
|
);
|
|
|
|
await sql(
|
|
`INSERT INTO stock_transactions (component_id, transaction_type, quantity, unit_cost, total_cost, supplier, supplier_order_ref, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[
|
|
Number(componentId),
|
|
"purchase",
|
|
delta,
|
|
unitPrice,
|
|
delta * unitPrice,
|
|
supplierName,
|
|
poNumber,
|
|
`Received from PO ${poNumber}`,
|
|
]
|
|
);
|
|
} else if (delta < 0 && componentId) {
|
|
// If quantity was reduced (correction), reverse the stock
|
|
await sql(
|
|
`UPDATE components SET stock_quantity = GREATEST(0, stock_quantity + $1), updated_at = NOW() WHERE id = $2`,
|
|
[delta, Number(componentId)]
|
|
);
|
|
|
|
await sql(
|
|
`INSERT INTO stock_transactions (component_id, transaction_type, quantity, unit_cost, total_cost, supplier, supplier_order_ref, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[
|
|
Number(componentId),
|
|
"purchase",
|
|
delta,
|
|
unitPrice,
|
|
delta * unitPrice,
|
|
supplierName,
|
|
poNumber,
|
|
`Correction on PO ${poNumber}`,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// When status changes to "received", auto-receive all remaining items
|
|
// This only triggers when NO received_items array is provided (e.g., status dropdown change)
|
|
if (status === "received" && !(received_items && Array.isArray(received_items) && received_items.length > 0)) {
|
|
console.log(`[PO Auto-Receive] Status changed to received without received_items, auto-completing remaining items for PO ${id}`);
|
|
const poInfo = await sql(
|
|
`SELECT sp.po_number, s.name AS supplier_name
|
|
FROM supplier_pos sp JOIN suppliers s ON s.id = sp.supplier_id
|
|
WHERE sp.id = $1`,
|
|
[Number(id)]
|
|
);
|
|
const poNumber = poInfo[0]?.po_number || "";
|
|
const supplierName = poInfo[0]?.supplier_name || "";
|
|
|
|
const allItems = await sql(
|
|
`SELECT id, component_id, quantity, received_quantity, unit_price FROM supplier_po_items WHERE supplier_po_id = $1`,
|
|
[Number(id)]
|
|
);
|
|
|
|
for (const item of allItems) {
|
|
const ordered = Number(item.quantity);
|
|
const prevReceived = Number(item.received_quantity) || 0;
|
|
const delta = ordered - prevReceived;
|
|
const componentId = item.component_id;
|
|
const unitPrice = Number(item.unit_price) || 0;
|
|
|
|
if (delta > 0) {
|
|
console.log(`[PO Auto-Receive] Item ${item.id}: ordered=${ordered}, prevReceived=${prevReceived}, delta=${delta}, component=${componentId}, PO=${poNumber}`);
|
|
|
|
// Mark as fully received
|
|
await sql(
|
|
`UPDATE supplier_po_items SET received_quantity = $1 WHERE id = $2`,
|
|
[ordered, Number(item.id)]
|
|
);
|
|
|
|
if (componentId) {
|
|
await sql(
|
|
`UPDATE components SET stock_quantity = stock_quantity + $1, updated_at = NOW() WHERE id = $2`,
|
|
[delta, Number(componentId)]
|
|
);
|
|
|
|
await sql(
|
|
`INSERT INTO stock_transactions (component_id, transaction_type, quantity, unit_cost, total_cost, supplier, supplier_order_ref, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[
|
|
Number(componentId),
|
|
"purchase",
|
|
delta,
|
|
unitPrice,
|
|
delta * unitPrice,
|
|
supplierName,
|
|
poNumber,
|
|
`Received from PO ${poNumber}`,
|
|
]
|
|
);
|
|
}
|
|
} else {
|
|
console.log(`[PO Auto-Receive] Item ${item.id}: no delta (ordered=${ordered}, prevReceived=${prevReceived}), skipping stock update`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace all items on a draft PO (full item edit)
|
|
const { draft_items } = body;
|
|
if (draft_items && Array.isArray(draft_items)) {
|
|
const currentRows = await sql(`SELECT status FROM supplier_pos WHERE id = $1`, [Number(id)]);
|
|
if (currentRows.length > 0 && currentRows[0].status !== "draft") {
|
|
return NextResponse.json({ error: "Items can only be edited on draft POs" }, { status: 400 });
|
|
}
|
|
await sql(`DELETE FROM supplier_po_items WHERE supplier_po_id = $1`, [Number(id)]);
|
|
for (const item of draft_items) {
|
|
await sql(
|
|
`INSERT INTO supplier_po_items (supplier_po_id, component_id, part_number, component_name, supplier_part_number, quantity, unit_price, received_quantity)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 0)`,
|
|
[
|
|
Number(id),
|
|
Number(item.component_id) || null,
|
|
item.part_number || "",
|
|
item.component_name || "",
|
|
item.supplier_part_number || "",
|
|
Number(item.quantity),
|
|
Number(item.unit_price) || 0,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
const { shipping_cost } = body;
|
|
const rows = await sql(
|
|
`UPDATE supplier_pos SET status = COALESCE($1, status), notes = COALESCE($2, notes),
|
|
shipping_cost = COALESCE($3, shipping_cost)
|
|
WHERE id = $4 RETURNING *`,
|
|
[status || null, notes !== undefined ? notes : null, shipping_cost !== undefined ? Number(shipping_cost) : null, Number(id)]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return NextResponse.json({ error: "Supplier PO not found" }, { status: 404 });
|
|
}
|
|
|
|
// Return full PO with items
|
|
const items = await sql(
|
|
`SELECT * FROM supplier_po_items WHERE supplier_po_id = $1 ORDER BY id ASC`,
|
|
[Number(id)]
|
|
);
|
|
|
|
return NextResponse.json({ ...rows[0], items });
|
|
} catch (error) {
|
|
console.error("Supplier PO PUT error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to update supplier PO" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function DELETE(
|
|
_request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const sql = getDb();
|
|
const { id } = await params;
|
|
|
|
// Only allow deleting draft POs
|
|
const currentRows = await sql(`SELECT status FROM supplier_pos WHERE id = $1`, [Number(id)]);
|
|
if (currentRows.length > 0 && currentRows[0].status !== "draft") {
|
|
return NextResponse.json(
|
|
{ error: "Only draft POs can be deleted" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
await sql(`DELETE FROM supplier_po_items WHERE supplier_po_id = $1`, [Number(id)]);
|
|
await sql(`DELETE FROM supplier_pos WHERE id = $1`, [Number(id)]);
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Supplier PO DELETE error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to delete supplier PO" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|