import { permissions } from "@mrp/shared"; import type { SalesDocumentDetailDto, SalesDocumentStatus, SalesOrderPlanningDto, SalesOrderPlanningNodeDto } from "@mrp/shared/dist/sales/types.js"; import type { ShipmentSummaryDto } from "@mrp/shared/dist/shipping/types.js"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { DocumentRevisionComparison } from "../../components/DocumentRevisionComparison"; import { salesConfigs, salesStatusOptions, type SalesDocumentEntity } from "./config"; import { SalesStatusBadge } from "./SalesStatusBadge"; import { ShipmentStatusBadge } from "../shipping/ShipmentStatusBadge"; function PlanningNodeCard({ node }: { node: SalesOrderPlanningNodeDto }) { return (
{node.itemSku} {node.itemName}
Demand {node.grossDemand} {node.unitOfMeasure} · Type {node.itemType} {node.bomQuantityPerParent !== null ? ` · Qty/parent ${node.bomQuantityPerParent}` : ""}
Linked WO {node.linkedWorkOrderSupply}
Linked PO {node.linkedPurchaseSupply}
Stock {node.supplyFromStock}
Open WO {node.supplyFromOpenWorkOrders}
Open PO {node.supplyFromOpenPurchaseOrders}
Build {node.recommendedBuildQuantity}
Buy {node.recommendedPurchaseQuantity}
{node.uncoveredQuantity > 0 ?
Uncovered {node.uncoveredQuantity}
: null}
{node.children.length > 0 ? (
{node.children.map((child) => ( ))}
) : null}
); } function formatCurrency(value: number) { return `$${value.toFixed(2)}`; } function mapSalesDocumentForComparison( document: Pick< SalesDocumentDetailDto, | "documentNumber" | "customerName" | "status" | "issueDate" | "expiresAt" | "approvedAt" | "approvedByName" | "discountAmount" | "discountPercent" | "taxAmount" | "taxPercent" | "freightAmount" | "subtotal" | "total" | "notes" | "lines" > ) { return { title: document.documentNumber, subtitle: document.customerName, status: document.status, metaFields: [ { label: "Issue Date", value: new Date(document.issueDate).toLocaleDateString() }, { label: "Expires", value: document.expiresAt ? new Date(document.expiresAt).toLocaleDateString() : "N/A" }, { label: "Approval", value: document.approvedAt ? new Date(document.approvedAt).toLocaleDateString() : "Pending" }, { label: "Approver", value: document.approvedByName ?? "No approver recorded" }, ], totalFields: [ { label: "Subtotal", value: formatCurrency(document.subtotal) }, { label: "Discount", value: `${formatCurrency(document.discountAmount)} (${document.discountPercent.toFixed(2)}%)` }, { 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.unitPrice), totalLabel: formatCurrency(line.lineTotal), })), }; } export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) { const { token, user } = useAuth(); const navigate = useNavigate(); const { quoteId, orderId } = useParams(); const config = salesConfigs[entity]; const documentId = entity === "quote" ? quoteId : orderId; const [document, setDocument] = useState(null); const [status, setStatus] = useState(`Loading ${config.singularLabel.toLowerCase()}...`); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isConverting, setIsConverting] = useState(false); const [isOpeningPdf, setIsOpeningPdf] = useState(false); const [isApproving, setIsApproving] = useState(false); const [shipments, setShipments] = useState([]); const [planning, setPlanning] = useState(null); const [pendingConfirmation, setPendingConfirmation] = useState< | { kind: "status" | "approve" | "convert"; title: string; description: string; impact: string; recovery: string; confirmLabel: string; confirmationLabel?: string; confirmationValue?: string; nextStatus?: SalesDocumentStatus; } | null >(null); const canManage = user?.permissions.includes(permissions.salesWrite) ?? false; const canManageShipping = user?.permissions.includes(permissions.shippingWrite) ?? false; const canReadShipping = user?.permissions.includes(permissions.shippingRead) ?? false; const canManageManufacturing = user?.permissions.includes(permissions.manufacturingWrite) ?? false; const canManagePurchasing = user?.permissions.includes(permissions.purchasingWrite) ?? false; useEffect(() => { if (!token || !documentId) { return; } const loader = entity === "quote" ? api.getQuote(token, documentId) : api.getSalesOrder(token, documentId); const planningLoader = entity === "order" ? api.getSalesOrderPlanning(token, documentId) : Promise.resolve(null); Promise.all([loader, planningLoader]) .then(([nextDocument, nextPlanning]) => { setDocument(nextDocument); setPlanning(nextPlanning); setStatus(`${config.singularLabel} loaded.`); if (entity === "order" && canReadShipping) { api.getShipments(token, { salesOrderId: nextDocument.id }).then(setShipments).catch(() => setShipments([])); } }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : `Unable to load ${config.singularLabel.toLowerCase()}.`; setStatus(message); }); }, [canReadShipping, config.singularLabel, documentId, entity, token]); if (!document) { return
{status}
; } const activeDocument = document; function buildWorkOrderRecommendationLink(itemId: string, quantity: number) { const params = new URLSearchParams({ itemId, salesOrderId: activeDocument.id, quantity: quantity.toString(), status: "DRAFT", notes: `Generated from sales order ${activeDocument.documentNumber} demand planning.`, }); return `/manufacturing/work-orders/new?${params.toString()}`; } function buildPurchaseRecommendationLink(itemId?: string, vendorId?: string | null) { const params = new URLSearchParams(); params.set("planningOrderId", activeDocument.id); if (itemId) { params.set("itemId", itemId); } if (vendorId) { params.set("vendorId", vendorId); } return `/purchasing/orders/new?${params.toString()}`; } async function applyStatusChange(nextStatus: SalesDocumentStatus) { if (!token) { return; } setIsUpdatingStatus(true); setStatus(`Updating ${config.singularLabel.toLowerCase()} status...`); try { const nextDocument = entity === "quote" ? await api.updateQuoteStatus(token, activeDocument.id, nextStatus) : await api.updateSalesOrderStatus(token, activeDocument.id, nextStatus); setDocument(nextDocument); setStatus(`${config.singularLabel} status updated. Review revisions and downstream workflows if the document moved into a terminal or customer-visible state.`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : `Unable to update ${config.singularLabel.toLowerCase()} status.`; setStatus(message); } finally { setIsUpdatingStatus(false); } } async function applyConvert() { if (!token || entity !== "quote") { return; } setIsConverting(true); setStatus("Converting quote to sales order..."); try { const order = await api.convertQuoteToSalesOrder(token, activeDocument.id); navigate(`/sales/orders/${order.id}`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to convert quote to sales order."; setStatus(message); setIsConverting(false); } } async function handleOpenPdf() { if (!token) { return; } setIsOpeningPdf(true); setStatus(`Rendering ${config.singularLabel.toLowerCase()} PDF...`); try { const blob = entity === "quote" ? await api.getQuotePdf(token, activeDocument.id) : await api.getSalesOrderPdf(token, activeDocument.id); const objectUrl = URL.createObjectURL(blob); window.open(objectUrl, "_blank", "noopener,noreferrer"); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000); setStatus(`${config.singularLabel} PDF ready.`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : `Unable to render ${config.singularLabel.toLowerCase()} PDF.`; setStatus(message); } finally { setIsOpeningPdf(false); } } async function applyApprove() { if (!token) { return; } setIsApproving(true); setStatus(`Approving ${config.singularLabel.toLowerCase()}...`); try { const nextDocument = entity === "quote" ? await api.approveQuote(token, activeDocument.id) : await api.approveSalesOrder(token, activeDocument.id); setDocument(nextDocument); setStatus(`${config.singularLabel} approved. The approval stamp is now part of the document history and downstream teams can act on it immediately.`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : `Unable to approve ${config.singularLabel.toLowerCase()}.`; setStatus(message); } finally { setIsApproving(false); } } function handleStatusChange(nextStatus: SalesDocumentStatus) { const label = salesStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus; setPendingConfirmation({ kind: "status", title: `Set ${config.singularLabel.toLowerCase()} to ${label}`, description: `Update ${activeDocument.documentNumber} from ${activeDocument.status} to ${nextStatus}.`, impact: nextStatus === "CLOSED" ? "This closes the document operationally and can change customer-facing execution assumptions and downstream follow-up expectations." : nextStatus === "APPROVED" ? "This marks the document ready for downstream action and becomes part of the approval history." : "This changes the operational state used by downstream workflows and audit/revision history.", recovery: "If this status is set in error, return the document to the correct state and verify the latest revision history.", confirmLabel: `Set ${label}`, confirmationLabel: nextStatus === "CLOSED" ? "Type document number to confirm:" : undefined, confirmationValue: nextStatus === "CLOSED" ? activeDocument.documentNumber : undefined, nextStatus, }); } function handleApprove() { setPendingConfirmation({ kind: "approve", title: `Approve ${config.singularLabel.toLowerCase()}`, description: `Approve ${activeDocument.documentNumber} for ${activeDocument.customerName}.`, impact: "Approval records the approver and timestamp and signals that downstream execution can proceed.", recovery: "If approval was granted by mistake, change the document status and review the revision trail for follow-up.", confirmLabel: "Approve document", }); } function handleConvert() { setPendingConfirmation({ kind: "convert", title: "Convert quote to sales order", description: `Create a sales order from quote ${activeDocument.documentNumber}.`, impact: "This creates a new sales order record and can trigger planning, purchasing, manufacturing, and shipping follow-up work.", recovery: "Review the new order immediately after creation. If conversion was premature, move the resulting order to the correct status and coordinate with downstream teams.", confirmLabel: "Convert quote", confirmationLabel: "Type quote number to confirm:", confirmationValue: activeDocument.documentNumber, }); } return (

{config.detailEyebrow}

{activeDocument.documentNumber}

{activeDocument.customerName}

Rev {activeDocument.currentRevisionNumber}
Back to {config.collectionLabel.toLowerCase()} {canManage ? ( <> Edit {config.singularLabel.toLowerCase()} {activeDocument.status !== "APPROVED" ? ( ) : null} {entity === "quote" ? ( ) : null} {entity === "order" && canManageShipping ? ( New shipment ) : null} ) : null}
{canManage ? (

Quick Actions

Update document status without opening the full editor.

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

Issue Date

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

Expires

{activeDocument.expiresAt ? new Date(activeDocument.expiresAt).toLocaleDateString() : "N/A"}

Lines

{activeDocument.lineCount}

Approval

{activeDocument.approvedAt ? new Date(activeDocument.approvedAt).toLocaleDateString() : "Pending"}
{activeDocument.approvedByName ?? "No approver recorded"}

Discount

-${activeDocument.discountAmount.toFixed(2)}
{activeDocument.discountPercent.toFixed(2)}%

Tax

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

Freight

${activeDocument.freightAmount.toFixed(2)}

Total

${activeDocument.total.toFixed(2)}

Revision History

Automatic snapshots are recorded when the document changes status, content, or approval state.

{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 mapSalesDocumentForComparison(activeDocument); } const revision = activeDocument.revisions.find((entry) => entry.id === revisionId); if (!revision) { return mapSalesDocumentForComparison(activeDocument); } return mapSalesDocumentForComparison({ ...revision.snapshot, lines: revision.snapshot.lines.map((line) => ({ id: `${line.itemId}-${line.position}`, ...line, })), }); }} /> ) : null}

Customer

Account
{activeDocument.customerName}
Email
{activeDocument.customerEmail}

Notes

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

Line Items

{activeDocument.lines.length === 0 ? (
No line items have been added yet.
) : (
{activeDocument.lines.map((line: SalesDocumentDetailDto["lines"][number]) => ( ))}
Item Description Qty UOM Unit Price Total
{line.itemSku}
{line.itemName}
{line.description} {line.quantity} {line.unitOfMeasure} ${line.unitPrice.toFixed(2)} ${line.lineTotal.toFixed(2)}
)}
{entity === "order" && planning ? (

Demand Planning

Net build and buy requirements

Sales-order demand is netted against available stock, active reservations, open work orders, and open purchase orders before new build or buy quantities are recommended.

Generated {new Date(planning.generatedAt).toLocaleString()}
Status {planning.status}

Build Recommendations

{planning.summary.totalBuildQuantity}
{planning.summary.buildRecommendationCount} items

Purchase Recommendations

{planning.summary.totalPurchaseQuantity}
{planning.summary.purchaseRecommendationCount} items

Uncovered

{planning.summary.totalUncoveredQuantity}
{planning.summary.uncoveredItemCount} items

Planned Items

{planning.summary.itemCount}
{planning.summary.lineCount} sales lines
{planning.items.map((item) => ( ))}
Item Gross Linked WO Linked PO Available Open WO Open PO Build Buy Uncovered Actions
{item.itemSku}
{item.itemName}
{item.grossDemand} {item.linkedWorkOrderSupply} {item.linkedPurchaseSupply} {item.availableQuantity} {item.openWorkOrderSupply} {item.openPurchaseSupply} {item.recommendedBuildQuantity} {item.recommendedPurchaseQuantity} {item.uncoveredQuantity}
{canManageManufacturing && item.recommendedBuildQuantity > 0 ? ( Draft WO ) : null} {canManagePurchasing && item.recommendedPurchaseQuantity > 0 ? ( Draft PO ) : null}
{canManagePurchasing && planning.summary.purchaseRecommendationCount > 0 ? (
Draft purchase order from recommendations
) : null}
{planning.lines.map((line) => (
{line.itemSku} {line.itemName}
Sales-order line demand: {line.quantity} {line.unitOfMeasure}
))}
) : null} {entity === "order" && canReadShipping ? (

Shipping

Shipment records currently tied to this sales order.

{canManageShipping ? ( Create shipment ) : null}
{shipments.length === 0 ? (
No shipments have been created for this sales order yet.
) : (
{shipments.map((shipment) => (
{shipment.shipmentNumber}
{shipment.carrier || "Carrier not set"} · {shipment.trackingNumber || "No tracking"}
))}
)}
) : null} { if (!isUpdatingStatus && !isApproving && !isConverting) { setPendingConfirmation(null); } }} onConfirm={async () => { if (!pendingConfirmation) { return; } if (pendingConfirmation.kind === "status" && pendingConfirmation.nextStatus) { await applyStatusChange(pendingConfirmation.nextStatus); setPendingConfirmation(null); return; } if (pendingConfirmation.kind === "approve") { await applyApprove(); setPendingConfirmation(null); return; } if (pendingConfirmation.kind === "convert") { await applyConvert(); setPendingConfirmation(null); } }} />
); }