2026-03-14 23:03:17 -05:00
import { permissions } from "@mrp/shared" ;
2026-03-15 15:45:29 -05:00
import type { SalesDocumentDetailDto , SalesDocumentStatus , SalesOrderPlanningDto , SalesOrderPlanningNodeDto } from "@mrp/shared/dist/sales/types.js" ;
2026-03-14 23:48:27 -05:00
import type { ShipmentSummaryDto } from "@mrp/shared/dist/shipping/types.js" ;
2026-03-14 23:03:17 -05:00
import { useEffect , useState } from "react" ;
2026-03-14 23:16:42 -05:00
import { Link , useNavigate , useParams } from "react-router-dom" ;
2026-03-14 23:03:17 -05:00
import { useAuth } from "../../auth/AuthProvider" ;
import { api , ApiError } from "../../lib/api" ;
2026-03-15 19:22:20 -05:00
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog" ;
2026-03-15 21:07:28 -05:00
import { DocumentRevisionComparison } from "../../components/DocumentRevisionComparison" ;
2026-03-14 23:16:42 -05:00
import { salesConfigs , salesStatusOptions , type SalesDocumentEntity } from "./config" ;
2026-03-14 23:03:17 -05:00
import { SalesStatusBadge } from "./SalesStatusBadge" ;
2026-03-14 23:48:27 -05:00
import { ShipmentStatusBadge } from "../shipping/ShipmentStatusBadge" ;
2026-03-14 23:03:17 -05:00
2026-03-15 15:45:29 -05:00
function PlanningNodeCard ( { node } : { node : SalesOrderPlanningNodeDto } ) {
return (
2026-03-15 20:07:48 -05:00
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3" style = { { marginLeft : node.level * 12 } } >
2026-03-15 15:45:29 -05:00
< div className = "flex flex-wrap items-start justify-between gap-3" >
< div >
< div className = "font-semibold text-text" >
{ node . itemSku } < span className = "text-muted" > { node . itemName } < / span >
< / div >
< div className = "mt-1 text-xs text-muted" >
Demand { node . grossDemand } { node . unitOfMeasure } · Type { node . itemType }
{ node . bomQuantityPerParent !== null ? ` · Qty/parent ${ node . bomQuantityPerParent } ` : "" }
< / div >
< / div >
< div className = "text-right text-xs text-muted" >
2026-03-15 16:40:25 -05:00
< div > Linked WO { node . linkedWorkOrderSupply } < / div >
< div > Linked PO { node . linkedPurchaseSupply } < / div >
2026-03-15 15:45:29 -05:00
< div > Stock { node . supplyFromStock } < / div >
< div > Open WO { node . supplyFromOpenWorkOrders } < / div >
< div > Open PO { node . supplyFromOpenPurchaseOrders } < / div >
< div > Build { node . recommendedBuildQuantity } < / div >
< div > Buy { node . recommendedPurchaseQuantity } < / div >
{ node . uncoveredQuantity > 0 ? < div > Uncovered { node . uncoveredQuantity } < / div > : null }
< / div >
< / div >
{ node . children . length > 0 ? (
< div className = "mt-3 space-y-3" >
{ node . children . map ( ( child ) = > (
< PlanningNodeCard key = { ` ${ child . itemId } - ${ child . level } - ${ child . itemSku } - ${ child . grossDemand } ` } node = { child } / >
) ) }
< / div >
) : null }
< / div >
) ;
}
2026-03-15 21:07:28 -05:00
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 ) ,
} ) ) ,
} ;
}
2026-03-14 23:03:17 -05:00
export function SalesDetailPage ( { entity } : { entity : SalesDocumentEntity } ) {
const { token , user } = useAuth ( ) ;
2026-03-14 23:16:42 -05:00
const navigate = useNavigate ( ) ;
2026-03-14 23:03:17 -05:00
const { quoteId , orderId } = useParams ( ) ;
const config = salesConfigs [ entity ] ;
const documentId = entity === "quote" ? quoteId : orderId ;
const [ document , setDocument ] = useState < SalesDocumentDetailDto | null > ( null ) ;
const [ status , setStatus ] = useState ( ` Loading ${ config . singularLabel . toLowerCase ( ) } ... ` ) ;
2026-03-14 23:16:42 -05:00
const [ isUpdatingStatus , setIsUpdatingStatus ] = useState ( false ) ;
const [ isConverting , setIsConverting ] = useState ( false ) ;
2026-03-15 09:22:39 -05:00
const [ isOpeningPdf , setIsOpeningPdf ] = useState ( false ) ;
2026-03-15 11:44:14 -05:00
const [ isApproving , setIsApproving ] = useState ( false ) ;
2026-03-14 23:48:27 -05:00
const [ shipments , setShipments ] = useState < ShipmentSummaryDto [ ] > ( [ ] ) ;
2026-03-15 15:45:29 -05:00
const [ planning , setPlanning ] = useState < SalesOrderPlanningDto | null > ( null ) ;
2026-03-15 19:22:20 -05:00
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 ) ;
2026-03-14 23:03:17 -05:00
const canManage = user ? . permissions . includes ( permissions . salesWrite ) ? ? false ;
2026-03-14 23:48:27 -05:00
const canManageShipping = user ? . permissions . includes ( permissions . shippingWrite ) ? ? false ;
const canReadShipping = user ? . permissions . includes ( permissions . shippingRead ) ? ? false ;
2026-03-15 16:40:25 -05:00
const canManageManufacturing = user ? . permissions . includes ( permissions . manufacturingWrite ) ? ? false ;
const canManagePurchasing = user ? . permissions . includes ( permissions . purchasingWrite ) ? ? false ;
2026-03-14 23:03:17 -05:00
useEffect ( ( ) = > {
if ( ! token || ! documentId ) {
return ;
}
const loader = entity === "quote" ? api . getQuote ( token , documentId ) : api . getSalesOrder ( token , documentId ) ;
2026-03-15 15:45:29 -05:00
const planningLoader = entity === "order" ? api . getSalesOrderPlanning ( token , documentId ) : Promise . resolve ( null ) ;
Promise . all ( [ loader , planningLoader ] )
. then ( ( [ nextDocument , nextPlanning ] ) = > {
2026-03-14 23:03:17 -05:00
setDocument ( nextDocument ) ;
2026-03-15 15:45:29 -05:00
setPlanning ( nextPlanning ) ;
2026-03-14 23:03:17 -05:00
setStatus ( ` ${ config . singularLabel } loaded. ` ) ;
2026-03-14 23:48:27 -05:00
if ( entity === "order" && canReadShipping ) {
api . getShipments ( token , { salesOrderId : nextDocument.id } ) . then ( setShipments ) . catch ( ( ) = > setShipments ( [ ] ) ) ;
}
2026-03-14 23:03:17 -05:00
} )
. catch ( ( error : unknown ) = > {
const message = error instanceof ApiError ? error . message : ` Unable to load ${ config . singularLabel . toLowerCase ( ) } . ` ;
setStatus ( message ) ;
} ) ;
2026-03-14 23:48:27 -05:00
} , [ canReadShipping , config . singularLabel , documentId , entity , token ] ) ;
2026-03-14 23:03:17 -05:00
if ( ! document ) {
2026-03-15 20:07:48 -05:00
return < div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel" > { status } < / div > ;
2026-03-14 23:03:17 -05:00
}
2026-03-14 23:16:42 -05:00
const activeDocument = document ;
2026-03-15 16:40:25 -05:00
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. ` ,
} ) ;
2026-03-18 11:54:22 -05:00
if ( activeDocument . linkedProjectId ) {
params . set ( "projectId" , activeDocument . linkedProjectId ) ;
}
2026-03-15 16:40:25 -05:00
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 ) ;
}
2026-03-18 11:54:22 -05:00
if ( activeDocument . linkedProjectId ) {
params . set ( "projectId" , activeDocument . linkedProjectId ) ;
}
if ( activeDocument . linkedProjectNumber ) {
params . set ( "projectNumber" , activeDocument . linkedProjectNumber ) ;
}
if ( activeDocument . linkedProjectName ) {
params . set ( "projectName" , activeDocument . linkedProjectName ) ;
}
2026-03-15 16:40:25 -05:00
return ` /purchasing/orders/new? ${ params . toString ( ) } ` ;
}
2026-03-15 19:22:20 -05:00
async function applyStatusChange ( nextStatus : SalesDocumentStatus ) {
2026-03-14 23:16:42 -05:00
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 ) ;
2026-03-15 19:22:20 -05:00
setStatus ( ` ${ config . singularLabel } status updated. Review revisions and downstream workflows if the document moved into a terminal or customer-visible state. ` ) ;
2026-03-14 23:16:42 -05:00
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : ` Unable to update ${ config . singularLabel . toLowerCase ( ) } status. ` ;
setStatus ( message ) ;
} finally {
setIsUpdatingStatus ( false ) ;
}
}
2026-03-15 19:22:20 -05:00
async function applyConvert() {
2026-03-14 23:16:42 -05:00
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 ) ;
}
}
2026-03-15 09:22:39 -05:00
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 ) ;
}
}
2026-03-15 19:22:20 -05:00
async function applyApprove() {
2026-03-15 11:44:14 -05:00
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 ) ;
2026-03-15 19:22:20 -05:00
setStatus ( ` ${ config . singularLabel } approved. The approval stamp is now part of the document history and downstream teams can act on it immediately. ` ) ;
2026-03-15 11:44:14 -05:00
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : ` Unable to approve ${ config . singularLabel . toLowerCase ( ) } . ` ;
setStatus ( message ) ;
} finally {
setIsApproving ( false ) ;
}
}
2026-03-15 19:22:20 -05:00
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 ,
} ) ;
}
2026-03-14 23:03:17 -05:00
return (
< section className = "space-y-4" >
2026-03-15 20:07:48 -05:00
< div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 23:03:17 -05:00
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > { config . detailEyebrow } < / p >
2026-03-14 23:16:42 -05:00
< h3 className = "mt-2 text-xl font-bold text-text" > { activeDocument . documentNumber } < / h3 >
< p className = "mt-1 text-sm text-text" > { activeDocument . customerName } < / p >
2026-03-14 23:03:17 -05:00
< div className = "mt-3 flex flex-wrap gap-2" >
2026-03-14 23:16:42 -05:00
< SalesStatusBadge status = { activeDocument . status } / >
2026-03-15 11:44:14 -05:00
< span className = "inline-flex items-center rounded-full border border-line/70 px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-muted" >
Rev { activeDocument . currentRevisionNumber }
< / span >
2026-03-14 23:03:17 -05:00
< / div >
< / div >
< div className = "flex flex-wrap gap-3" >
< Link to = { config . routeBase } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
Back to { config . collectionLabel . toLowerCase ( ) }
< / Link >
2026-03-15 09:22:39 -05:00
< button
type = "button"
onClick = { handleOpenPdf }
disabled = { isOpeningPdf }
className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60"
>
{ isOpeningPdf ? "Rendering PDF..." : "Open PDF" }
< / button >
2026-03-14 23:03:17 -05:00
{ canManage ? (
2026-03-14 23:16:42 -05:00
< >
< Link to = { ` ${ config . routeBase } / ${ activeDocument . id } /edit ` } className = "inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white" >
Edit { config . singularLabel . toLowerCase ( ) }
< / Link >
2026-03-15 11:44:14 -05:00
{ activeDocument . status !== "APPROVED" ? (
< button
type = "button"
onClick = { handleApprove }
disabled = { isApproving }
className = "inline-flex items-center justify-center rounded-2xl border border-emerald-400/40 px-2 py-2 text-sm font-semibold text-emerald-700 disabled:cursor-not-allowed disabled:opacity-60 dark:text-emerald-300"
>
{ isApproving ? "Approving..." : "Approve" }
< / button >
) : null }
2026-03-14 23:16:42 -05:00
{ entity === "quote" ? (
< button
type = "button"
onClick = { handleConvert }
disabled = { isConverting }
className = "inline-flex items-center justify-center rounded-2xl border border-emerald-400/40 px-2 py-2 text-sm font-semibold text-emerald-700 disabled:cursor-not-allowed disabled:opacity-60 dark:text-emerald-300"
>
{ isConverting ? "Converting..." : "Convert to sales order" }
< / button >
) : null }
2026-03-14 23:48:27 -05:00
{ entity === "order" && canManageShipping ? (
< Link to = { ` /shipping/shipments/new?orderId= ${ activeDocument . id } ` } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
New shipment
< / Link >
) : null }
2026-03-14 23:16:42 -05:00
< / >
2026-03-14 23:03:17 -05:00
) : null }
< / div >
< / div >
< / div >
2026-03-14 23:16:42 -05:00
{ canManage ? (
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 23:16:42 -05:00
< div className = "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Quick Actions < / p >
< p className = "mt-2 text-sm text-muted" > Update document status without opening the full editor . < / p >
< / div >
< div className = "flex flex-wrap gap-2" >
{ salesStatusOptions . map ( ( option ) = > (
< button
key = { option . value }
type = "button"
onClick = { ( ) = > handleStatusChange ( option . value ) }
disabled = { isUpdatingStatus || activeDocument . status === option . value }
className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60"
>
{ option . label }
< / button >
) ) }
< / div >
< / div >
< / section >
) : null }
2026-03-14 23:03:17 -05:00
< section className = "grid gap-3 xl:grid-cols-4" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:03:17 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Issue Date < / p >
2026-03-14 23:16:42 -05:00
< div className = "mt-2 text-base font-bold text-text" > { new Date ( activeDocument . issueDate ) . toLocaleDateString ( ) } < / div >
2026-03-14 23:03:17 -05:00
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:03:17 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Expires < / p >
2026-03-14 23:16:42 -05:00
< div className = "mt-2 text-base font-bold text-text" > { activeDocument . expiresAt ? new Date ( activeDocument . expiresAt ) . toLocaleDateString ( ) : "N/A" } < / div >
2026-03-14 23:03:17 -05:00
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:03:17 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Lines < / p >
2026-03-14 23:16:42 -05:00
< div className = "mt-2 text-base font-bold text-text" > { activeDocument . lineCount } < / div >
2026-03-14 23:03:17 -05:00
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-15 11:44:14 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Approval < / p >
< div className = "mt-2 text-base font-bold text-text" > { activeDocument . approvedAt ? new Date ( activeDocument . approvedAt ) . toLocaleDateString ( ) : "Pending" } < / div >
< div className = "mt-1 text-xs text-muted" > { activeDocument . approvedByName ? ? "No approver recorded" } < / div >
2026-03-14 23:03:17 -05:00
< / article >
< / section >
2026-03-14 23:39:51 -05:00
< section className = "grid gap-3 xl:grid-cols-4" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:39:51 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Discount < / p >
< div className = "mt-2 text-base font-bold text-text" > - $ { activeDocument . discountAmount . toFixed ( 2 ) } < / div >
< div className = "mt-1 text-xs text-muted" > { activeDocument . discountPercent . toFixed ( 2 ) } % < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:39:51 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Tax < / p >
< div className = "mt-2 text-base font-bold text-text" > $ { activeDocument . taxAmount . toFixed ( 2 ) } < / div >
< div className = "mt-1 text-xs text-muted" > { activeDocument . taxPercent . toFixed ( 2 ) } % < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:39:51 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Freight < / p >
< div className = "mt-2 text-base font-bold text-text" > $ { activeDocument . freightAmount . toFixed ( 2 ) } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 23:39:51 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Total < / p >
< div className = "mt-2 text-base font-bold text-text" > $ { activeDocument . total . toFixed ( 2 ) } < / div >
< / article >
< / section >
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 11:44:14 -05:00
< div className = "flex items-center justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Revision History < / p >
< p className = "mt-2 text-sm text-muted" > Automatic snapshots are recorded when the document changes status , content , or approval state . < / p >
< / div >
< / div >
{ activeDocument . revisions . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-15 11:44:14 -05:00
No revisions have been recorded yet .
< / div >
) : (
< div className = "mt-6 space-y-3" >
{ activeDocument . revisions . map ( ( revision ) = > (
2026-03-15 20:07:48 -05:00
< article key = { revision . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 11:44:14 -05:00
< div className = "flex flex-wrap items-start justify-between gap-3" >
< div >
< div className = "font-semibold text-text" > Rev { revision . revisionNumber } < / div >
< div className = "mt-1 text-sm text-text" > { revision . reason } < / div >
< / div >
< div className = "text-right text-xs text-muted" >
< div > { new Date ( revision . createdAt ) . toLocaleString ( ) } < / div >
< div className = "mt-1" > { revision . createdByName ? ? "System" } < / div >
< / div >
< / div >
< / article >
) ) }
< / div >
) }
< / section >
2026-03-15 21:07:28 -05:00
{ activeDocument . revisions . length > 0 ? (
< DocumentRevisionComparison
title = "Revision Comparison"
description = "Compare a prior revision against the current document or another revision to see commercial and line-level changes."
currentLabel = "Current document"
currentDocument = { mapSalesDocumentForComparison ( activeDocument ) }
revisions = { activeDocument . revisions . map ( ( revision ) = > ( {
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 }
2026-03-14 23:03:17 -05:00
< div className = "grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 23:03:17 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Customer < / p >
< dl className = "mt-5 grid gap-3" >
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Account < / dt >
2026-03-14 23:16:42 -05:00
< dd className = "mt-1 text-sm text-text" > { activeDocument . customerName } < / dd >
2026-03-14 23:03:17 -05:00
< / div >
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Email < / dt >
2026-03-14 23:16:42 -05:00
< dd className = "mt-1 text-sm text-text" > { activeDocument . customerEmail } < / dd >
2026-03-14 23:03:17 -05:00
< / div >
< / dl >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-18 11:54:22 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Project Link < / p >
{ activeDocument . linkedProjectId ? (
< div className = "mt-3 space-y-2" >
< Link to = { ` /projects/ ${ activeDocument . linkedProjectId } ` } className = "inline-flex items-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text hover:bg-page/70" >
{ activeDocument . linkedProjectNumber } / { activeDocument . linkedProjectName }
< / Link >
< p className = "text-sm text-muted" > This { entity === "quote" ? "quote" : "sales order" } is already linked to a project , and downstream WO / PO launches will carry that project context . < / p >
< / div >
) : (
< p className = "mt-3 text-sm text-muted" > No linked project is currently attached to this { entity === "quote" ? "quote" : "sales order" } . < / p >
) }
< p className = "mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Notes < / p >
2026-03-14 23:16:42 -05:00
< p className = "mt-3 whitespace-pre-line text-sm leading-6 text-text" > { activeDocument . notes || "No notes recorded for this document." } < / p >
2026-03-14 23:03:17 -05:00
< / article >
< / div >
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 23:03:17 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Line Items < / p >
2026-03-14 23:16:42 -05:00
{ activeDocument . lines . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-14 23:03:17 -05:00
No line items have been added yet .
< / div >
) : (
< div className = "mt-6 overflow-hidden rounded-2xl border border-line/70" >
< table className = "min-w-full divide-y divide-line/70 text-sm" >
< thead className = "bg-page/80 text-left text-muted" >
< tr >
< th className = "px-2 py-2" > Item < / th >
< th className = "px-2 py-2" > Description < / th >
< th className = "px-2 py-2" > Qty < / th >
< th className = "px-2 py-2" > UOM < / th >
< th className = "px-2 py-2" > Unit Price < / th >
< th className = "px-2 py-2" > Total < / th >
< / tr >
< / thead >
< tbody className = "divide-y divide-line/70 bg-surface" >
2026-03-14 23:16:42 -05:00
{ activeDocument . lines . map ( ( line : SalesDocumentDetailDto [ "lines" ] [ number ] ) = > (
2026-03-14 23:03:17 -05:00
< tr key = { line . id } >
< td className = "px-2 py-2" >
< div className = "font-semibold text-text" > { line . itemSku } < / div >
< div className = "mt-1 text-xs text-muted" > { line . itemName } < / div >
< / td >
< td className = "px-2 py-2 text-muted" > { line . description } < / td >
< td className = "px-2 py-2 text-muted" > { line . quantity } < / td >
< td className = "px-2 py-2 text-muted" > { line . unitOfMeasure } < / td >
< td className = "px-2 py-2 text-muted" > $ { line . unitPrice . toFixed ( 2 ) } < / td >
< td className = "px-2 py-2 text-muted" > $ { line . lineTotal . toFixed ( 2 ) } < / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
) }
< / section >
2026-03-15 15:45:29 -05:00
{ entity === "order" && planning ? (
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 15:45:29 -05:00
< div className = "flex flex-wrap items-start justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Demand Planning < / p >
< h3 className = "mt-2 text-lg font-bold text-text" > Net build and buy requirements < / h3 >
< p className = "mt-2 max-w-3xl text-sm text-muted" >
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 .
< / p >
< / div >
< div className = "text-right text-xs text-muted" >
< div > Generated { new Date ( planning . generatedAt ) . toLocaleString ( ) } < / div >
< div > Status { planning . status } < / div >
< / div >
< / div >
< div className = "mt-5 grid gap-3 xl:grid-cols-4" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-page/70 px-3 py-3" >
2026-03-15 15:45:29 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Build Recommendations < / p >
< div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalBuildQuantity } < / div >
< div className = "mt-1 text-xs text-muted" > { planning . summary . buildRecommendationCount } items < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-page/70 px-3 py-3" >
2026-03-15 15:45:29 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Purchase Recommendations < / p >
< div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalPurchaseQuantity } < / div >
< div className = "mt-1 text-xs text-muted" > { planning . summary . purchaseRecommendationCount } items < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-page/70 px-3 py-3" >
2026-03-15 15:45:29 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Uncovered < / p >
< div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalUncoveredQuantity } < / div >
< div className = "mt-1 text-xs text-muted" > { planning . summary . uncoveredItemCount } items < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-page/70 px-3 py-3" >
2026-03-15 15:45:29 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Planned Items < / p >
< div className = "mt-2 text-base font-bold text-text" > { planning . summary . itemCount } < / div >
< div className = "mt-1 text-xs text-muted" > { planning . summary . lineCount } sales lines < / div >
< / article >
< / div >
< div className = "mt-5 overflow-hidden rounded-2xl border border-line/70" >
< table className = "min-w-full divide-y divide-line/70 text-sm" >
< thead className = "bg-page/80 text-left text-muted" >
< tr >
< th className = "px-2 py-2" > Item < / th >
< th className = "px-2 py-2" > Gross < / th >
2026-03-15 16:40:25 -05:00
< th className = "px-2 py-2" > Linked WO < / th >
< th className = "px-2 py-2" > Linked PO < / th >
2026-03-15 15:45:29 -05:00
< th className = "px-2 py-2" > Available < / th >
< th className = "px-2 py-2" > Open WO < / th >
< th className = "px-2 py-2" > Open PO < / th >
< th className = "px-2 py-2" > Build < / th >
< th className = "px-2 py-2" > Buy < / th >
< th className = "px-2 py-2" > Uncovered < / th >
2026-03-15 16:40:25 -05:00
< th className = "px-2 py-2" > Actions < / th >
2026-03-15 15:45:29 -05:00
< / tr >
< / thead >
< tbody className = "divide-y divide-line/70 bg-surface" >
{ planning . items . map ( ( item ) = > (
< tr key = { item . itemId } >
< td className = "px-2 py-2" >
< div className = "font-semibold text-text" > { item . itemSku } < / div >
< div className = "mt-1 text-xs text-muted" > { item . itemName } < / div >
< / td >
< td className = "px-2 py-2 text-muted" > { item . grossDemand } < / td >
2026-03-15 16:40:25 -05:00
< td className = "px-2 py-2 text-muted" > { item . linkedWorkOrderSupply } < / td >
< td className = "px-2 py-2 text-muted" > { item . linkedPurchaseSupply } < / td >
2026-03-15 15:45:29 -05:00
< td className = "px-2 py-2 text-muted" > { item . availableQuantity } < / td >
< td className = "px-2 py-2 text-muted" > { item . openWorkOrderSupply } < / td >
< td className = "px-2 py-2 text-muted" > { item . openPurchaseSupply } < / td >
< td className = "px-2 py-2 text-muted" > { item . recommendedBuildQuantity } < / td >
< td className = "px-2 py-2 text-muted" > { item . recommendedPurchaseQuantity } < / td >
< td className = "px-2 py-2 text-muted" > { item . uncoveredQuantity } < / td >
2026-03-15 16:40:25 -05:00
< td className = "px-2 py-2" >
< div className = "flex flex-wrap gap-2" >
{ canManageManufacturing && item . recommendedBuildQuantity > 0 ? (
< Link
to = { buildWorkOrderRecommendationLink ( item . itemId , item . recommendedBuildQuantity ) }
className = "rounded-2xl border border-line/70 px-2 py-1 text-xs font-semibold text-text"
>
Draft WO
< / Link >
) : null }
{ canManagePurchasing && item . recommendedPurchaseQuantity > 0 ? (
< Link
to = { buildPurchaseRecommendationLink ( item . itemId ) }
className = "rounded-2xl border border-line/70 px-2 py-1 text-xs font-semibold text-text"
>
Draft PO
< / Link >
) : null }
< / div >
< / td >
2026-03-15 15:45:29 -05:00
< / tr >
) ) }
< / tbody >
< / table >
< / div >
2026-03-15 16:40:25 -05:00
{ canManagePurchasing && planning . summary . purchaseRecommendationCount > 0 ? (
< div className = "mt-4 flex justify-end" >
< Link to = { buildPurchaseRecommendationLink ( ) } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" >
Draft purchase order from recommendations
< / Link >
< / div >
) : null }
2026-03-15 15:45:29 -05:00
< div className = "mt-5 space-y-3" >
{ planning . lines . map ( ( line ) = > (
2026-03-15 20:07:48 -05:00
< div key = { line . lineId } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 15:45:29 -05:00
< div className = "mb-3" >
< div className = "font-semibold text-text" >
{ line . itemSku } < span className = "text-muted" > { line . itemName } < / span >
< / div >
< div className = "mt-1 text-xs text-muted" >
Sales - order line demand : { line . quantity } { line . unitOfMeasure }
< / div >
< / div >
< PlanningNodeCard node = { line . rootNode } / >
< / div >
) ) }
< / div >
< / section >
) : null }
2026-03-14 23:48:27 -05:00
{ entity === "order" && canReadShipping ? (
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 23:48:27 -05:00
< div className = "flex items-center justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Shipping < / p >
< p className = "mt-2 text-sm text-muted" > Shipment records currently tied to this sales order . < / p >
< / div >
{ canManageShipping ? (
< Link to = { ` /shipping/shipments/new?orderId= ${ activeDocument . id } ` } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
Create shipment
< / Link >
) : null }
< / div >
{ shipments . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-14 23:48:27 -05:00
No shipments have been created for this sales order yet .
< / div >
) : (
< div className = "mt-6 space-y-3" >
{ shipments . map ( ( shipment ) = > (
2026-03-15 20:07:48 -05:00
< Link key = { shipment . id } to = { ` /shipping/shipments/ ${ shipment . id } ` } className = "block rounded-[18px] border border-line/70 bg-page/60 p-3 transition hover:bg-page/80" >
2026-03-14 23:48:27 -05:00
< div className = "flex flex-wrap items-center justify-between gap-3" >
< div >
< div className = "font-semibold text-text" > { shipment . shipmentNumber } < / div >
< div className = "mt-1 text-xs text-muted" > { shipment . carrier || "Carrier not set" } · { shipment . trackingNumber || "No tracking" } < / div >
< / div >
< ShipmentStatusBadge status = { shipment . status } / >
< / div >
< / Link >
) ) }
< / div >
) }
< / section >
) : null }
2026-03-15 19:22:20 -05:00
< ConfirmActionDialog
open = { pendingConfirmation != null }
title = { pendingConfirmation ? . title ? ? "Confirm sales action" }
description = { pendingConfirmation ? . description ? ? "" }
impact = { pendingConfirmation ? . impact }
recovery = { pendingConfirmation ? . recovery }
confirmLabel = { pendingConfirmation ? . confirmLabel ? ? "Confirm" }
confirmationLabel = { pendingConfirmation ? . confirmationLabel }
confirmationValue = { pendingConfirmation ? . confirmationValue }
isConfirming = {
( pendingConfirmation ? . kind === "status" && isUpdatingStatus ) ||
( pendingConfirmation ? . kind === "approve" && isApproving ) ||
( pendingConfirmation ? . kind === "convert" && isConverting )
}
onClose = { ( ) = > {
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 ) ;
}
} }
/ >
2026-03-14 23:03:17 -05:00
< / section >
) ;
}
2026-03-15 20:07:48 -05:00