import { permissions } from "@mrp/shared"; import type { PurchaseOrderDetailDto, PurchaseOrderStatus } from "@mrp/shared"; import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js"; import type { PurchaseReceiptInput } from "@mrp/shared/dist/purchasing/types.js"; import type { DemandPlanningRollupDto } from "@mrp/shared/dist/sales/types.js"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { DocumentRevisionComparison } from "../../components/DocumentRevisionComparison"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { api, ApiError } from "../../lib/api"; import { emptyPurchaseReceiptInput, purchaseStatusOptions } from "./config"; import { PurchaseStatusBadge } from "./PurchaseStatusBadge"; function formatCurrency(value: number) { return `$${value.toFixed(2)}`; } function mapPurchaseDocumentForComparison( document: Pick< PurchaseOrderDetailDto, | "documentNumber" | "vendorName" | "status" | "issueDate" | "taxPercent" | "taxAmount" | "freightAmount" | "subtotal" | "total" | "notes" | "paymentTerms" | "currencyCode" | "lines" | "receipts" > ) { return { title: document.documentNumber, subtitle: document.vendorName, status: document.status, metaFields: [ { label: "Issue Date", value: new Date(document.issueDate).toLocaleDateString() }, { label: "Payment Terms", value: document.paymentTerms || "N/A" }, { label: "Currency", value: document.currencyCode || "USD" }, { label: "Receipts", value: document.receipts.length.toString() }, ], totalFields: [ { label: "Subtotal", value: formatCurrency(document.subtotal) }, { label: "Tax", value: `${formatCurrency(document.taxAmount)} (${document.taxPercent.toFixed(2)}%)` }, { label: "Freight", value: formatCurrency(document.freightAmount) }, { label: "Total", value: formatCurrency(document.total) }, ], notes: document.notes, lines: document.lines.map((line) => ({ key: line.id || `${line.itemId}-${line.position}`, title: `${line.itemSku} | ${line.itemName}`, subtitle: line.description, quantity: `${line.quantity} ${line.unitOfMeasure}`, unitLabel: line.unitOfMeasure, amountLabel: formatCurrency(line.unitCost), totalLabel: formatCurrency(line.lineTotal), extraLabel: `${line.receivedQuantity} received | ${line.remainingQuantity} remaining` + (line.salesOrderNumber ? ` | Demand ${line.salesOrderNumber}` : ""), })), }; } export function PurchaseDetailPage() { const { token, user } = useAuth(); const { orderId } = useParams(); const [document, setDocument] = useState(null); const [locationOptions, setLocationOptions] = useState([]); const [receiptForm, setReceiptForm] = useState(emptyPurchaseReceiptInput); const [receiptQuantities, setReceiptQuantities] = useState>({}); const [receiptStatus, setReceiptStatus] = useState("Receive ordered material into inventory against this purchase order."); const [isSavingReceipt, setIsSavingReceipt] = useState(false); const [status, setStatus] = useState("Loading purchase order..."); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isOpeningPdf, setIsOpeningPdf] = useState(false); const [planningRollup, setPlanningRollup] = useState(null); const [pendingConfirmation, setPendingConfirmation] = useState< | { kind: "status" | "receipt"; title: string; description: string; impact: string; recovery: string; confirmLabel: string; confirmationLabel?: string; confirmationValue?: string; nextStatus?: PurchaseOrderStatus; } | null >(null); const canManage = user?.permissions.includes("purchasing.write") ?? false; const canReceive = canManage && (user?.permissions.includes(permissions.inventoryWrite) ?? false); useEffect(() => { if (!token || !orderId) { return; } api.getPurchaseOrder(token, orderId) .then((nextDocument) => { setDocument(nextDocument); setStatus("Purchase order loaded."); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load purchase order."; setStatus(message); }); api.getDemandPlanningRollup(token).then(setPlanningRollup).catch(() => setPlanningRollup(null)); if (!canReceive) { return; } api.getWarehouseLocationOptions(token) .then((options) => { setLocationOptions(options); setReceiptForm((current: PurchaseReceiptInput) => { if (current.locationId) { return current; } const firstOption = options[0]; return firstOption ? { ...current, warehouseId: firstOption.warehouseId, locationId: firstOption.locationId, } : current; }); }) .catch(() => setLocationOptions([])); }, [canReceive, orderId, token]); useEffect(() => { if (!document) { return; } setReceiptQuantities((current) => { const next: Record = {}; for (const line of document.lines) { if (line.remainingQuantity > 0) { next[line.id] = current[line.id] ?? 0; } } return next; }); }, [document]); if (!document) { return
{status}
; } const activeDocument = document; const openLines = activeDocument.lines.filter((line) => line.remainingQuantity > 0); const demandContextItems = planningRollup?.items.filter((item) => activeDocument.lines.some((line) => line.itemId === item.itemId) && (item.recommendedPurchaseQuantity > 0 || item.uncoveredQuantity > 0)) ?? []; function updateReceiptField(key: Key, value: PurchaseReceiptInput[Key]) { setReceiptForm((current: PurchaseReceiptInput) => ({ ...current, [key]: value })); } function updateReceiptQuantity(lineId: string, quantity: number) { setReceiptQuantities((current: Record) => ({ ...current, [lineId]: quantity, })); } async function applyStatusChange(nextStatus: PurchaseOrderStatus) { if (!token) { return; } setIsUpdatingStatus(true); setStatus("Updating purchase order status..."); try { const nextDocument = await api.updatePurchaseOrderStatus(token, activeDocument.id, nextStatus); setDocument(nextDocument); setStatus("Purchase order status updated. Confirm vendor communication and receiving expectations if this moved the order into a terminal state."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update purchase order status."; setStatus(message); } finally { setIsUpdatingStatus(false); } } async function applyReceipt() { if (!token || !canReceive) { return; } setIsSavingReceipt(true); setReceiptStatus("Posting purchase receipt..."); try { const payload: PurchaseReceiptInput = { ...receiptForm, lines: openLines .map((line) => ({ purchaseOrderLineId: line.id, quantity: Math.max(0, Math.floor(receiptQuantities[line.id] ?? 0)), })) .filter((line) => line.quantity > 0), }; const nextDocument = await api.createPurchaseReceipt(token, activeDocument.id, payload); setDocument(nextDocument); setReceiptQuantities({}); setReceiptForm((current: PurchaseReceiptInput) => ({ ...current, receivedAt: new Date().toISOString(), notes: "", })); setReceiptStatus("Purchase receipt recorded. Inventory has been increased; verify stock balances and post a correcting movement if quantities were overstated."); setStatus("Purchase order updated after receipt."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to record purchase receipt."; setReceiptStatus(message); } finally { setIsSavingReceipt(false); } } function handleStatusChange(nextStatus: PurchaseOrderStatus) { const label = purchaseStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus; setPendingConfirmation({ kind: "status", title: `Set purchase order to ${label}`, description: `Update ${activeDocument.documentNumber} from ${activeDocument.status} to ${nextStatus}.`, impact: nextStatus === "CLOSED" ? "This closes the order operationally and can change inbound supply expectations, shortage coverage, and vendor follow-up." : "This changes the purchasing state used by receiving, planning, and audit review.", recovery: "If the status is wrong, set the order back to the correct state and verify any downstream receiving or planning assumptions.", confirmLabel: `Set ${label}`, confirmationLabel: nextStatus === "CLOSED" ? "Type purchase order number to confirm:" : undefined, confirmationValue: nextStatus === "CLOSED" ? activeDocument.documentNumber : undefined, nextStatus, }); } function handleReceiptSubmit(event: React.FormEvent) { event.preventDefault(); const totalReceiptQuantity = openLines.reduce((sum, line) => sum + Math.max(0, Math.floor(receiptQuantities[line.id] ?? 0)), 0); setPendingConfirmation({ kind: "receipt", title: "Post purchase receipt", description: `Receive ${totalReceiptQuantity} total units into ${receiptForm.warehouseId && receiptForm.locationId ? "the selected stock location" : "inventory"} for ${activeDocument.documentNumber}.`, impact: "This increases inventory immediately and becomes part of the PO receipt history.", recovery: "If quantities are wrong, post the correcting inventory movement and review the remaining quantities on the purchase order.", confirmLabel: "Post receipt", confirmationLabel: totalReceiptQuantity > 0 ? "Type purchase order number to confirm:" : undefined, confirmationValue: totalReceiptQuantity > 0 ? activeDocument.documentNumber : undefined, }); } async function handleOpenPdf() { if (!token) { return; } setIsOpeningPdf(true); setStatus("Rendering purchase order PDF..."); try { const blob = await api.getPurchaseOrderPdf(token, activeDocument.id); const objectUrl = URL.createObjectURL(blob); window.open(objectUrl, "_blank", "noopener,noreferrer"); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000); setStatus("Purchase order PDF ready."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to render purchase order PDF."; setStatus(message); } finally { setIsOpeningPdf(false); } } return (

Purchase Order

{activeDocument.documentNumber}

{activeDocument.vendorName}

Rev {activeDocument.revisions[0]?.revisionNumber ?? 0}
Back to purchase orders {canManage ? ( Edit purchase order ) : null}
{canManage ? (

Quick Actions

Update purchase-order status without opening the full editor.

{purchaseStatusOptions.map((option) => ( ))}
) : null}

Issue Date

{new Date(activeDocument.issueDate).toLocaleDateString()}

Lines

{activeDocument.lineCount}

Receipts

{activeDocument.receipts.length}

Qty Remaining

{activeDocument.lines.reduce((sum, line) => sum + line.remainingQuantity, 0)}

Subtotal

${activeDocument.subtotal.toFixed(2)}

Total

${activeDocument.total.toFixed(2)}

Tax

${activeDocument.taxAmount.toFixed(2)}
{activeDocument.taxPercent.toFixed(2)}%

Freight

${activeDocument.freightAmount.toFixed(2)}

Payment Terms

{activeDocument.paymentTerms || "N/A"}

Currency

{activeDocument.currencyCode || "USD"}

Revision History

Automatic snapshots are recorded when the purchase order changes or receipts are posted.

{activeDocument.revisions.length === 0 ? (
No revisions have been recorded yet.
) : (
{activeDocument.revisions.map((revision) => (
Rev {revision.revisionNumber}
{revision.reason}
{new Date(revision.createdAt).toLocaleString()}
{revision.createdByName ?? "System"}
))}
)}
{activeDocument.revisions.length > 0 ? ( ({ id: revision.id, label: `Rev ${revision.revisionNumber}`, meta: `${new Date(revision.createdAt).toLocaleString()} | ${revision.createdByName ?? "System"}`, }))} getRevisionDocument={(revisionId) => { if (revisionId === "current") { return mapPurchaseDocumentForComparison(activeDocument); } const revision = activeDocument.revisions.find((entry) => entry.id === revisionId); if (!revision) { return mapPurchaseDocumentForComparison(activeDocument); } return mapPurchaseDocumentForComparison({ ...revision.snapshot, lines: revision.snapshot.lines.map((line) => ({ id: `${line.itemId}-${line.position}`, ...line, })), receipts: revision.snapshot.receipts, }); }} /> ) : null}

Vendor

Account
{activeDocument.vendorName}
Email
{activeDocument.vendorEmail}

Notes

{activeDocument.notes || "No notes recorded for this document."}

Demand Context

{demandContextItems.length === 0 ? (
No active shared shortage or buy-signal records currently point at items on this purchase order.
) : (
{demandContextItems.map((item) => (
{item.itemSku}
{item.itemName}
Buy {item.recommendedPurchaseQuantity} · Uncovered {item.uncoveredQuantity}
))}
)}

Line Items

{activeDocument.lines.length === 0 ? (
No line items have been added yet.
) : (
{activeDocument.lines.map((line: PurchaseOrderDetailDto["lines"][number]) => ( ))}
ItemDescriptionDemand SourceOrderedReceivedRemainingUOMUnit CostTotal
{line.itemSku}
{line.itemName}
{line.description} {line.salesOrderId && line.salesOrderNumber ? {line.salesOrderNumber} : "Unlinked"} {line.quantity} {line.receivedQuantity} {line.remainingQuantity} {line.unitOfMeasure} ${line.unitCost.toFixed(2)} ${line.lineTotal.toFixed(2)}
)}
{canReceive ? (

Purchase Receiving

Receive material

Post received quantities to inventory and retain a receipt record against this order.

{openLines.length === 0 ? (
All ordered quantities have been received for this purchase order.
) : (