2026-03-15 00:29:41 -05:00
import { permissions } from "@mrp/shared" ;
import type { PurchaseOrderDetailDto , PurchaseOrderStatus } from "@mrp/shared" ;
2026-03-15 09:04:18 -05:00
import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js" ;
import type { PurchaseReceiptInput } from "@mrp/shared/dist/purchasing/types.js" ;
2026-03-15 16:40:25 -05:00
import type { DemandPlanningRollupDto } from "@mrp/shared/dist/sales/types.js" ;
2026-03-15 00:29:41 -05:00
import { useEffect , useState } from "react" ;
import { Link , useParams } from "react-router-dom" ;
import { useAuth } from "../../auth/AuthProvider" ;
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-15 11:30:10 -05:00
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel" ;
2026-03-15 00:29:41 -05:00
import { api , ApiError } from "../../lib/api" ;
2026-03-15 09:04:18 -05:00
import { emptyPurchaseReceiptInput , purchaseStatusOptions } from "./config" ;
2026-03-15 00:29:41 -05:00
import { PurchaseStatusBadge } from "./PurchaseStatusBadge" ;
2026-03-15 21:07:28 -05:00
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 } ` : "" ) ,
} ) ) ,
} ;
}
2026-03-15 00:29:41 -05:00
export function PurchaseDetailPage() {
const { token , user } = useAuth ( ) ;
const { orderId } = useParams ( ) ;
const [ document , setDocument ] = useState < PurchaseOrderDetailDto | null > ( null ) ;
2026-03-15 09:04:18 -05:00
const [ locationOptions , setLocationOptions ] = useState < WarehouseLocationOptionDto [ ] > ( [ ] ) ;
const [ receiptForm , setReceiptForm ] = useState < PurchaseReceiptInput > ( emptyPurchaseReceiptInput ) ;
const [ receiptQuantities , setReceiptQuantities ] = useState < Record < string , number > > ( { } ) ;
const [ receiptStatus , setReceiptStatus ] = useState ( "Receive ordered material into inventory against this purchase order." ) ;
const [ isSavingReceipt , setIsSavingReceipt ] = useState ( false ) ;
2026-03-15 00:29:41 -05:00
const [ status , setStatus ] = useState ( "Loading purchase order..." ) ;
const [ isUpdatingStatus , setIsUpdatingStatus ] = useState ( false ) ;
2026-03-15 09:22:39 -05:00
const [ isOpeningPdf , setIsOpeningPdf ] = useState ( false ) ;
2026-03-15 16:40:25 -05:00
const [ planningRollup , setPlanningRollup ] = useState < DemandPlanningRollupDto | null > ( null ) ;
2026-03-15 19:22:20 -05:00
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 ) ;
2026-03-15 00:29:41 -05:00
const canManage = user ? . permissions . includes ( "purchasing.write" ) ? ? false ;
2026-03-15 09:04:18 -05:00
const canReceive = canManage && ( user ? . permissions . includes ( permissions . inventoryWrite ) ? ? false ) ;
2026-03-15 00:29:41 -05:00
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 ) ;
} ) ;
2026-03-15 16:40:25 -05:00
api . getDemandPlanningRollup ( token ) . then ( setPlanningRollup ) . catch ( ( ) = > setPlanningRollup ( null ) ) ;
2026-03-15 09:04:18 -05:00
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 < string , number > = { } ;
for ( const line of document . lines ) {
if ( line . remainingQuantity > 0 ) {
next [ line . id ] = current [ line . id ] ? ? 0 ;
}
}
return next ;
} ) ;
} , [ document ] ) ;
2026-03-15 00:29:41 -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-15 00:29:41 -05:00
}
const activeDocument = document ;
2026-03-15 09:04:18 -05:00
const openLines = activeDocument . lines . filter ( ( line ) = > line . remainingQuantity > 0 ) ;
2026-03-15 16:40:25 -05:00
const demandContextItems =
planningRollup ? . items . filter ( ( item ) = > activeDocument . lines . some ( ( line ) = > line . itemId === item . itemId ) && ( item . recommendedPurchaseQuantity > 0 || item . uncoveredQuantity > 0 ) ) ? ? [ ] ;
2026-03-15 09:04:18 -05:00
function updateReceiptField < Key extends keyof PurchaseReceiptInput > ( key : Key , value : PurchaseReceiptInput [ Key ] ) {
setReceiptForm ( ( current : PurchaseReceiptInput ) = > ( { . . . current , [ key ] : value } ) ) ;
}
function updateReceiptQuantity ( lineId : string , quantity : number ) {
setReceiptQuantities ( ( current : Record < string , number > ) = > ( {
. . . current ,
[ lineId ] : quantity ,
} ) ) ;
}
2026-03-15 00:29:41 -05:00
2026-03-15 19:22:20 -05:00
async function applyStatusChange ( nextStatus : PurchaseOrderStatus ) {
2026-03-15 00:29:41 -05:00
if ( ! token ) {
return ;
}
setIsUpdatingStatus ( true ) ;
setStatus ( "Updating purchase order status..." ) ;
try {
const nextDocument = await api . updatePurchaseOrderStatus ( token , activeDocument . id , nextStatus ) ;
setDocument ( nextDocument ) ;
2026-03-15 19:22:20 -05:00
setStatus ( "Purchase order status updated. Confirm vendor communication and receiving expectations if this moved the order into a terminal state." ) ;
2026-03-15 00:29:41 -05:00
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : "Unable to update purchase order status." ;
setStatus ( message ) ;
} finally {
setIsUpdatingStatus ( false ) ;
}
}
2026-03-15 19:22:20 -05:00
async function applyReceipt() {
2026-03-15 09:04:18 -05:00
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 : "" ,
} ) ) ;
2026-03-15 19:22:20 -05:00
setReceiptStatus ( "Purchase receipt recorded. Inventory has been increased; verify stock balances and post a correcting movement if quantities were overstated." ) ;
2026-03-15 09:04:18 -05:00
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 ) ;
}
}
2026-03-15 19:22:20 -05:00
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 < HTMLFormElement > ) {
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 ,
} ) ;
}
2026-03-15 09:22:39 -05:00
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 ) ;
}
}
2026-03-15 00:29:41 -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-15 00:29:41 -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" > Purchase Order < / p >
< h3 className = "mt-2 text-xl font-bold text-text" > { activeDocument . documentNumber } < / h3 >
< p className = "mt-1 text-sm text-text" > { activeDocument . vendorName } < / p >
< div className = "mt-3 flex flex-wrap gap-2" >
< PurchaseStatusBadge status = { activeDocument . status } / >
2026-03-15 21:07:28 -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 . revisions [ 0 ] ? . revisionNumber ? ? 0 }
< / span >
2026-03-15 00:29:41 -05:00
< / div >
< / div >
< div className = "flex flex-wrap gap-3" >
< Link to = "/purchasing/orders" 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 purchase orders
< / 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-15 00:29:41 -05:00
{ canManage ? (
< Link to = { ` /purchasing/orders/ ${ 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 purchase order
< / Link >
) : null }
< / div >
< / div >
< / div >
{ 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-15 00:29:41 -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 purchase - order status without opening the full editor . < / p >
< / div >
< div className = "flex flex-wrap gap-2" >
{ purchaseStatusOptions . 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 }
< 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" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Issue Date < / p > < div className = "mt-2 text-base font-bold text-text" > { new Date ( activeDocument . issueDate ) . toLocaleDateString ( ) } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Lines < / p > < div className = "mt-2 text-base font-bold text-text" > { activeDocument . lineCount } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Receipts < / p > < div className = "mt-2 text-base font-bold text-text" > { activeDocument . receipts . length } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Qty Remaining < / p > < div className = "mt-2 text-base font-bold text-text" > { activeDocument . lines . reduce ( ( sum , line ) = > sum + line . remainingQuantity , 0 ) } < / div > < / article >
2026-03-15 00:29:41 -05:00
< / section >
< 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" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Subtotal < / p > < div className = "mt-2 text-base font-bold text-text" > $ { activeDocument . subtotal . toFixed ( 2 ) } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < 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 >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < 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 >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < 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 >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Payment Terms < / p > < div className = "mt-2 text-base font-bold text-text" > { activeDocument . paymentTerms || "N/A" } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Currency < / p > < div className = "mt-2 text-base font-bold text-text" > { activeDocument . currencyCode || "USD" } < / div > < / article >
2026-03-15 00:29:41 -05:00
< / section >
2026-03-15 21:07:28 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< 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 purchase order changes or receipts are posted . < / p >
< / div >
< / div >
{ activeDocument . revisions . length === 0 ? (
< 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" >
No revisions have been recorded yet .
< / div >
) : (
< div className = "mt-6 space-y-3" >
{ activeDocument . revisions . map ( ( revision ) = > (
< article key = { revision . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
< 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 >
{ activeDocument . revisions . length > 0 ? (
< DocumentRevisionComparison
title = "Revision Comparison"
description = "Compare earlier purchase-order revisions against the current document or another revision to review commercial, receiving, and line-level changes."
currentLabel = "Current document"
currentDocument = { mapPurchaseDocumentForComparison ( 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 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 }
2026-03-15 00:29:41 -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-15 00:29:41 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Vendor < / p >
< dl className = "mt-5 grid gap-3" >
2026-03-15 11:30:10 -05:00
< div > < dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Account < / dt > < dd className = "mt-1 text-sm text-text" > < Link to = { ` /crm/vendors/ ${ activeDocument . vendorId } ` } className = "hover:text-brand" > { activeDocument . vendorName } < / Link > < / dd > < / div >
2026-03-15 00:29:41 -05:00
< div > < dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Email < / dt > < dd className = "mt-1 text-sm text-text" > { activeDocument . vendorEmail } < / dd > < / 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 . projectId ? (
< div className = "mt-3 space-y-2" >
< Link to = { ` /projects/ ${ activeDocument . projectId } ` } 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 . projectNumber } / { activeDocument . projectName }
< / Link >
< p className = "text-sm text-muted" > This purchase order is linked to the project context used by project cockpit and downstream rollups . < / p >
< / div >
) : (
< p className = "mt-3 text-sm text-muted" > No linked project is currently attached to this purchase order . < / p >
) }
< p className = "mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Notes < / p >
2026-03-15 00:29:41 -05:00
< p className = "mt-3 whitespace-pre-line text-sm leading-6 text-text" > { activeDocument . notes || "No notes recorded for this document." } < / p >
< / 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-15 16:40:25 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Demand Context < / p >
{ demandContextItems . 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 16:40:25 -05:00
No active shared shortage or buy - signal records currently point at items on this purchase order .
< / div >
) : (
< div className = "mt-5 space-y-3" >
{ demandContextItems . map ( ( item ) = > (
2026-03-15 20:07:48 -05:00
< div key = { item . itemId } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 16:40:25 -05:00
< div className = "flex flex-wrap items-center justify-between gap-3" >
< div >
< div className = "font-semibold text-text" > { item . itemSku } < / div >
< div className = "mt-1 text-xs text-muted" > { item . itemName } < / div >
< / div >
< div className = "text-sm text-muted" >
Buy { item . recommendedPurchaseQuantity } · Uncovered { item . uncoveredQuantity }
< / div >
< / div >
< / div >
) ) }
< / div >
) }
< / 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 00:29:41 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Line Items < / p >
{ 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" > No line items have been added yet . < / div >
2026-03-15 00:29:41 -05:00
) : (
< 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" >
2026-03-15 16:40:25 -05:00
< tr > < th className = "px-2 py-2" > Item < / th > < th className = "px-2 py-2" > Description < / th > < th className = "px-2 py-2" > Demand Source < / th > < th className = "px-2 py-2" > Ordered < / th > < th className = "px-2 py-2" > Received < / th > < th className = "px-2 py-2" > Remaining < / th > < th className = "px-2 py-2" > UOM < / th > < th className = "px-2 py-2" > Unit Cost < / th > < th className = "px-2 py-2" > Total < / th > < / tr >
2026-03-15 00:29:41 -05:00
< / thead >
< tbody className = "divide-y divide-line/70 bg-surface" >
{ activeDocument . lines . map ( ( line : PurchaseOrderDetailDto [ "lines" ] [ number ] ) = > (
< 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 >
2026-03-15 16:40:25 -05:00
< td className = "px-2 py-2 text-muted" >
{ line . salesOrderId && line . salesOrderNumber ? < Link to = { ` /sales/orders/ ${ line . salesOrderId } ` } className = "hover:text-brand" > { line . salesOrderNumber } < / Link > : "Unlinked" }
< / td >
2026-03-15 00:29:41 -05:00
< td className = "px-2 py-2 text-muted" > { line . quantity } < / td >
2026-03-15 09:04:18 -05:00
< td className = "px-2 py-2 text-muted" > { line . receivedQuantity } < / td >
< td className = "px-2 py-2 text-muted" > { line . remainingQuantity } < / td >
2026-03-15 00:29:41 -05:00
< td className = "px-2 py-2 text-muted" > { line . unitOfMeasure } < / td >
< td className = "px-2 py-2 text-muted" > $ { line . unitCost . toFixed ( 2 ) } < / td >
< td className = "px-2 py-2 text-muted" > $ { line . lineTotal . toFixed ( 2 ) } < / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
) }
< / section >
2026-03-15 09:04:18 -05:00
< section className = "grid gap-3 2xl:grid-cols-[minmax(360px,0.82fr)_minmax(0,1.18fr)]" >
{ canReceive ? (
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-15 09:04:18 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Purchase Receiving < / p >
< h4 className = "mt-2 text-lg font-bold text-text" > Receive material < / h4 >
< p className = "mt-2 text-sm text-muted" > Post received quantities to inventory and retain a receipt record against this order . < / p >
{ openLines . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-15 09:04:18 -05:00
All ordered quantities have been received for this purchase order .
< / div >
) : (
< form className = "mt-5 space-y-4" onSubmit = { handleReceiptSubmit } >
< div className = "grid gap-3 xl:grid-cols-2" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Receipt date < / span >
< input
type = "date"
value = { receiptForm . receivedAt . slice ( 0 , 10 ) }
onChange = { ( event ) = > updateReceiptField ( "receivedAt" , new Date ( event . target . value ) . toISOString ( ) ) }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Stock location < / span >
< select
value = { receiptForm . locationId }
onChange = { ( event ) = > {
const nextLocation = locationOptions . find ( ( option ) = > option . locationId === event . target . value ) ;
updateReceiptField ( "locationId" , event . target . value ) ;
if ( nextLocation ) {
updateReceiptField ( "warehouseId" , nextLocation . warehouseId ) ;
}
} }
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
>
{ locationOptions . map ( ( option ) = > (
< option key = { option . locationId } value = { option . locationId } >
{ option . warehouseCode } / { option . locationCode }
< / option >
) ) }
< / select >
< / label >
< / div >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes < / span >
< textarea
value = { receiptForm . notes }
onChange = { ( event ) = > updateReceiptField ( "notes" , event . target . value ) }
rows = { 3 }
2026-03-15 20:07:48 -05:00
className = "w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
2026-03-15 09:04:18 -05:00
/ >
< / label >
< div className = "space-y-3" >
{ openLines . map ( ( line ) = > (
2026-03-15 20:07:48 -05:00
< div key = { line . id } className = "grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[minmax(0,1.3fr)_0.6fr_0.7fr_0.7fr]" >
2026-03-15 09:04:18 -05:00
< div >
< div className = "font-semibold text-text" > { line . itemSku } < / div >
< div className = "mt-1 text-xs text-muted" > { line . itemName } < / div >
< div className = "mt-2 text-xs text-muted" > { line . description } < / div >
< / div >
< div className = "text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Remaining < / div >
< div className = "mt-2 font-semibold text-text" > { line . remainingQuantity } < / div >
< / div >
< div className = "text-sm" >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Received < / div >
< div className = "mt-2 font-semibold text-text" > { line . receivedQuantity } < / div >
< / div >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Receive Now < / span >
< input
type = "number"
min = { 0 }
max = { line . remainingQuantity }
step = { 1 }
value = { receiptQuantities [ line . id ] ? ? 0 }
onChange = { ( event ) = > updateReceiptQuantity ( line . id , Number . parseInt ( event . target . value , 10 ) || 0 ) }
className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/ >
< / label >
< / div >
) ) }
< / div >
< div className = "flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between" >
< span className = "min-w-0 text-sm text-muted" > { receiptStatus } < / span >
< button
type = "submit"
disabled = { isSavingReceipt || locationOptions . length === 0 }
className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
>
{ isSavingReceipt ? "Posting..." : "Post receipt" }
< / button >
< / div >
< / form >
) }
< / article >
) : null }
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-15 09:04:18 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Receipt History < / p >
< h4 className = "mt-2 text-lg font-bold text-text" > Received material log < / h4 >
{ activeDocument . receipts . 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 09:04:18 -05:00
No purchase receipts have been recorded for this order yet .
< / div >
) : (
< div className = "mt-6 space-y-3" >
{ activeDocument . receipts . map ( ( receipt ) = > (
2026-03-15 20:07:48 -05:00
< article key = { receipt . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 09:04:18 -05:00
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
< div >
< div className = "text-sm font-semibold text-text" > { receipt . receiptNumber } < / div >
< div className = "mt-1 text-xs text-muted" >
{ receipt . warehouseCode } / { receipt . locationCode } · { receipt . totalQuantity } units across { receipt . lineCount } line { receipt . lineCount === 1 ? "" : "s" }
< / div >
< div className = "mt-2 text-xs text-muted" >
{ receipt . warehouseName } · { receipt . locationName }
< / div >
< div className = "mt-3 space-y-1" >
{ receipt . lines . map ( ( line ) = > (
< div key = { line . id } className = "text-sm text-text" >
< span className = "font-semibold" > { line . itemSku } < / span > · { line . quantity }
< / div >
) ) }
< / div >
{ receipt . notes ? < p className = "mt-3 whitespace-pre-line text-sm leading-6 text-text" > { receipt . notes } < / p > : null }
< / div >
< div className = "text-sm text-muted lg:text-right" >
< div > { new Date ( receipt . receivedAt ) . toLocaleDateString ( ) } < / div >
< div className = "mt-1" > { receipt . createdByName } < / div >
< / div >
< / div >
< / article >
) ) }
< / div >
) }
< / article >
< / section >
2026-03-15 11:30:10 -05:00
< FileAttachmentsPanel
ownerType = "PURCHASE_ORDER"
ownerId = { activeDocument . id }
eyebrow = "Supporting Documents"
title = "Vendor invoices and backup"
description = "Store vendor invoices, acknowledgements, certifications, and supporting procurement documents directly on the purchase order."
emptyMessage = "No vendor supporting documents have been uploaded for this purchase order yet."
/ >
2026-03-15 00:29:41 -05:00
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted" > { status } < / div >
2026-03-15 19:22:20 -05:00
< ConfirmActionDialog
open = { pendingConfirmation != null }
title = { pendingConfirmation ? . title ? ? "Confirm purchasing 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 === "receipt" && isSavingReceipt )
}
onClose = { ( ) = > {
if ( ! isUpdatingStatus && ! isSavingReceipt ) {
setPendingConfirmation ( null ) ;
}
} }
onConfirm = { async ( ) = > {
if ( ! pendingConfirmation ) {
return ;
}
if ( pendingConfirmation . kind === "status" && pendingConfirmation . nextStatus ) {
await applyStatusChange ( pendingConfirmation . nextStatus ) ;
setPendingConfirmation ( null ) ;
return ;
}
if ( pendingConfirmation . kind === "receipt" ) {
await applyReceipt ( ) ;
setPendingConfirmation ( null ) ;
}
} }
/ >
2026-03-15 00:29:41 -05:00
< / section >
) ;
}
2026-03-15 20:07:48 -05:00