2026-03-15 16:40:25 -05:00
import type { InventoryItemOptionDto , PurchaseLineInput , PurchaseOrderInput , PurchaseVendorOptionDto , SalesOrderPlanningDto , SalesOrderPlanningNodeDto } from "@mrp/shared" ;
2026-03-15 00:29:41 -05:00
import { useEffect , useState } from "react" ;
2026-03-18 22:44:01 -05:00
import { useNavigate , useParams , useSearchParams } from "react-router-dom" ;
2026-03-15 00:29:41 -05:00
2026-03-15 19:40:35 -05:00
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog" ;
2026-03-15 00:29:41 -05:00
import { useAuth } from "../../auth/AuthProvider" ;
import { api , ApiError } from "../../lib/api" ;
import { inventoryUnitOptions } from "../inventory/config" ;
import { emptyPurchaseOrderInput , purchaseStatusOptions } from "./config" ;
export function PurchaseFormPage ({ mode } : { mode : "create" | "edit" }) {
const { token } = useAuth ();
const navigate = useNavigate ();
const { orderId } = useParams ();
2026-03-15 11:30:10 -05:00
const [ searchParams ] = useSearchParams ();
const seededVendorId = searchParams . get ( "vendorId" );
2026-03-18 11:54:22 -05:00
const seededProjectId = searchParams . get ( "projectId" );
const seededProjectNumber = searchParams . get ( "projectNumber" );
const seededProjectName = searchParams . get ( "projectName" );
2026-03-15 16:40:25 -05:00
const planningOrderId = searchParams . get ( "planningOrderId" );
const selectedPlanningItemId = searchParams . get ( "itemId" );
2026-03-15 00:29:41 -05:00
const [ form , setForm ] = useState < PurchaseOrderInput >( emptyPurchaseOrderInput );
const [ status , setStatus ] = useState ( mode === "create" ? "Create a new purchase order." : "Loading purchase order..." );
const [ vendors , setVendors ] = useState < PurchaseVendorOptionDto [] >([]);
const [ vendorSearchTerm , setVendorSearchTerm ] = useState ( "" );
const [ vendorPickerOpen , setVendorPickerOpen ] = useState ( false );
const [ itemOptions , setItemOptions ] = useState < InventoryItemOptionDto [] >([]);
const [ lineSearchTerms , setLineSearchTerms ] = useState < string [] >([]);
const [ activeLinePicker , setActiveLinePicker ] = useState < number | null >( null );
const [ isSaving , setIsSaving ] = useState ( false );
2026-03-15 19:40:35 -05:00
const [ pendingLineRemovalIndex , setPendingLineRemovalIndex ] = useState < number | null >( null );
2026-03-15 00:29:41 -05:00
2026-03-15 16:40:25 -05:00
function collectRecommendedPurchaseNodes ( node : SalesOrderPlanningNodeDto ) : SalesOrderPlanningNodeDto [] {
const nodes = node . recommendedPurchaseQuantity > 0 ? [ node ] : [];
for ( const child of node . children ) {
nodes . push (... collectRecommendedPurchaseNodes ( child ));
}
return nodes ;
}
2026-03-15 00:29:41 -05:00
const subtotal = form . lines . reduce (( sum : number , line : PurchaseLineInput ) => sum + line . quantity * line . unitCost , 0 );
const taxAmount = subtotal * ( form . taxPercent / 100 );
const total = subtotal + taxAmount + form . freightAmount ;
useEffect (() => {
if ( ! token ) {
return ;
}
2026-03-15 11:30:10 -05:00
api . getPurchaseVendors ( token ). then (( nextVendors ) => {
setVendors ( nextVendors );
if ( mode === "create" && seededVendorId ) {
const seededVendor = nextVendors . find (( vendor ) => vendor . id === seededVendorId );
if ( seededVendor ) {
setForm (( current : PurchaseOrderInput ) => ({ ... current , vendorId : seededVendor.id }));
setVendorSearchTerm ( seededVendor . name );
}
}
}). catch (() => setVendors ([]));
2026-03-15 00:47:16 -05:00
api . getInventoryItemOptions ( token ). then (( options ) => setItemOptions ( options . filter (( option : InventoryItemOptionDto ) => option . isPurchasable ))). catch (() => setItemOptions ([]));
2026-03-15 11:30:10 -05:00
}, [ mode , seededVendorId , token ]);
2026-03-15 00:29:41 -05:00
2026-03-18 11:54:22 -05:00
useEffect (() => {
if ( mode === "create" && seededProjectId ) {
setForm (( current ) => ({ ... current , projectId : current.projectId || seededProjectId }));
}
}, [ mode , seededProjectId ]);
2026-03-15 16:40:25 -05:00
useEffect (() => {
if ( ! token || mode !== "create" || ! planningOrderId || itemOptions . length === 0 ) {
return ;
}
api . getSalesOrderPlanning ( token , planningOrderId )
. then (( planning : SalesOrderPlanningDto ) => {
const recommendedNodes = planning . lines . flatMap (( line ) =>
collectRecommendedPurchaseNodes ( line . rootNode ). map (( node ) => ({
2026-03-15 23:24:04 -05:00
salesOrderLineId : node.itemId === line . itemId ? line.lineId : null ,
2026-03-15 16:40:25 -05:00
... node ,
}))
);
const filteredNodes = selectedPlanningItemId
? recommendedNodes . filter (( node ) => node . itemId === selectedPlanningItemId )
: recommendedNodes ;
const recommendedLines = filteredNodes . map (( node , index ) => {
const inventoryItem = itemOptions . find (( option ) => option . id === node . itemId );
return {
itemId : node.itemId ,
description : node.itemName ,
quantity : node.recommendedPurchaseQuantity ,
unitOfMeasure : node.unitOfMeasure ,
unitCost : inventoryItem?.defaultCost ?? 0 ,
salesOrderId : planning.orderId ,
salesOrderLineId : node.salesOrderLineId ,
position : ( index + 1 ) * 10 ,
} satisfies PurchaseLineInput ;
});
if ( recommendedLines . length === 0 ) {
return ;
}
const preferredVendorIds = [
... new Set (
recommendedLines
. map (( line ) => itemOptions . find (( option ) => option . id === line . itemId ) ? . preferredVendorId )
. filter (( vendorId ) : vendorId is string => Boolean ( vendorId ))
),
];
const autoVendorId = seededVendorId || ( preferredVendorIds . length === 1 ? preferredVendorIds [ 0 ] : null );
setForm (( current ) => ({
... current ,
vendorId : current.vendorId || autoVendorId || "" ,
2026-03-18 11:54:22 -05:00
projectId : current.projectId || seededProjectId || null ,
2026-03-15 16:40:25 -05:00
notes : current.notes || `Demand-planning recommendation from sales order ${ planning . documentNumber } .` ,
lines : current.lines.length > 0 ? current.lines : recommendedLines ,
}));
if ( autoVendorId ) {
const autoVendor = vendors . find (( vendor ) => vendor . id === autoVendorId );
if ( autoVendor ) {
setVendorSearchTerm ( autoVendor . name );
}
}
setLineSearchTerms (( current ) =>
current . length > 0 ? current : recommendedLines.map (( line ) => itemOptions . find (( option ) => option . id === line . itemId ) ? . sku ?? "" )
);
setStatus (
preferredVendorIds . length > 1 && ! seededVendorId
? `Loaded ${ recommendedLines . length } recommended buy lines from ${ planning . documentNumber } . Multiple preferred vendors exist, so confirm the vendor before saving.`
: `Loaded ${ recommendedLines . length } recommended buy lines from ${ planning . documentNumber } .`
);
})
. catch (() => {
setStatus ( "Unable to load demand-planning recommendations." );
});
2026-03-18 11:54:22 -05:00
}, [ itemOptions , mode , planningOrderId , seededProjectId , seededVendorId , selectedPlanningItemId , token , vendors ]);
2026-03-15 16:40:25 -05:00
2026-03-15 00:29:41 -05:00
useEffect (() => {
if ( ! token || mode !== "edit" || ! orderId ) {
return ;
}
api . getPurchaseOrder ( token , orderId )
. then (( document ) => {
setForm ({
vendorId : document.vendorId ,
2026-03-18 11:54:22 -05:00
projectId : document.projectId ,
2026-03-15 00:29:41 -05:00
status : document.status ,
issueDate : document.issueDate ,
taxPercent : document.taxPercent ,
freightAmount : document.freightAmount ,
notes : document.notes ,
2026-03-15 21:07:28 -05:00
revisionReason : "" ,
2026-03-15 16:40:25 -05:00
lines : document.lines.map (( line : { itemId : string ; description : string ; quantity : number ; unitOfMeasure : PurchaseLineInput [ "unitOfMeasure" ]; unitCost : number ; position : number ; salesOrderId : string | null ; salesOrderLineId : string | null }) => ({
2026-03-15 00:29:41 -05:00
itemId : line.itemId ,
description : line.description ,
quantity : line.quantity ,
unitOfMeasure : line.unitOfMeasure ,
unitCost : line.unitCost ,
2026-03-15 16:40:25 -05:00
salesOrderId : line.salesOrderId ,
salesOrderLineId : line.salesOrderLineId ,
2026-03-15 00:29:41 -05:00
position : line.position ,
})),
});
setVendorSearchTerm ( document . vendorName );
setLineSearchTerms ( document . lines . map (( line : { itemSku : string }) => line . itemSku ));
setStatus ( "Purchase order loaded." );
})
. catch (( error : unknown ) => {
const message = error instanceof ApiError ? error . message : "Unable to load purchase order." ;
setStatus ( message );
});
}, [ mode , orderId , token ]);
function updateField < Key extends keyof PurchaseOrderInput >( key : Key , value : PurchaseOrderInput [ Key ]) {
setForm (( current : PurchaseOrderInput ) => ({ ... current , [ key ] : value }));
}
function getSelectedVendorName ( vendorId : string ) {
return vendors . find (( vendor ) => vendor . id === vendorId ) ? . name ?? "" ;
}
function getSelectedVendor ( vendorId : string ) {
return vendors . find (( vendor ) => vendor . id === vendorId ) ?? null ;
}
function updateLine ( index : number , nextLine : PurchaseLineInput ) {
setForm (( current : PurchaseOrderInput ) => ({
... current ,
lines : current.lines.map (( line : PurchaseLineInput , lineIndex : number ) => ( lineIndex === index ? nextLine : line )),
}));
}
function updateLineSearchTerm ( index : number , value : string ) {
setLineSearchTerms (( current ) => {
const next = [... current ];
next [ index ] = value ;
return next ;
});
}
function addLine() {
setForm (( current : PurchaseOrderInput ) => ({
... current ,
lines : [
... current . lines ,
{
itemId : "" ,
description : "" ,
quantity : 1 ,
unitOfMeasure : "EA" ,
unitCost : 0 ,
position : current.lines.length === 0 ? 10 : Math.max (... current . lines . map (( line : PurchaseLineInput ) => line . position )) + 10 ,
},
],
}));
setLineSearchTerms (( current ) => [... current , "" ]);
}
function removeLine ( index : number ) {
setForm (( current : PurchaseOrderInput ) => ({
... current ,
lines : current.lines.filter (( _line : PurchaseLineInput , lineIndex : number ) => lineIndex !== index ),
}));
setLineSearchTerms (( current ) => current . filter (( _term , termIndex ) => termIndex !== index ));
}
2026-03-15 19:40:35 -05:00
const pendingLineRemoval =
pendingLineRemovalIndex != null
? {
index : pendingLineRemovalIndex ,
line : form.lines [ pendingLineRemovalIndex ],
sku : lineSearchTerms [ pendingLineRemovalIndex ] ?? "" ,
}
: null ;
2026-03-15 00:29:41 -05:00
async function handleSubmit ( event : React.FormEvent < HTMLFormElement >) {
event . preventDefault ();
if ( ! token ) {
return ;
}
setIsSaving ( true );
setStatus ( "Saving purchase order..." );
try {
const saved = mode === "create" ? await api . createPurchaseOrder ( token , form ) : await api . updatePurchaseOrder ( token , orderId ?? "" , form );
navigate ( `/purchasing/orders/ ${ saved . id } ` );
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : "Unable to save purchase order." ;
setStatus ( message );
setIsSaving ( false );
}
}
const filteredVendorCount = vendors . filter (( vendor ) => {
const query = vendorSearchTerm . trim (). toLowerCase ();
if ( ! query ) {
return true ;
}
return vendor . name . toLowerCase (). includes ( query ) || vendor . email . toLowerCase (). includes ( query );
}). length ;
2026-03-18 22:44:01 -05:00
function closeEditor() {
navigate ( mode === "create" ? "/purchasing/orders" : `/purchasing/orders/ ${ orderId } ` );
}
2026-03-15 00:29:41 -05:00
return (
2026-03-18 20:36:30 -05:00
< form className = "page-stack" onSubmit = { handleSubmit }>
< section className = "surface-panel" >
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 >
2026-03-18 20:36:30 -05:00
< p className = "section-kicker" > PURCHASING EDITOR </ p >
< h3 className = "module-title" >{ mode === "create" ? "New Purchase Order" : "Edit Purchase Order" }</ h3 >
2026-03-15 00:29:41 -05:00
</ div >
2026-03-18 22:44:01 -05:00
< button type = "button" onClick = { closeEditor } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
2026-03-15 00:29:41 -05:00
Cancel
2026-03-18 22:44:01 -05:00
</ button >
2026-03-15 00:29:41 -05:00
</ div >
</ section >
2026-03-18 20:36:30 -05:00
< section className = "space-y-3 surface-panel" >
2026-03-15 00:29:41 -05:00
< div className = "grid gap-3 xl:grid-cols-4" >
< label className = "block xl:col-span-2" >
< span className = "mb-2 block text-sm font-semibold text-text" > Vendor </ span >
< div className = "relative" >
< input
value = { vendorSearchTerm }
onChange = {( event ) => {
setVendorSearchTerm ( event . target . value );
updateField ( "vendorId" , "" );
setVendorPickerOpen ( true );
}}
onFocus = {() => setVendorPickerOpen ( true )}
onBlur = {() => {
window . setTimeout (() => {
setVendorPickerOpen ( false );
if ( form . vendorId ) {
setVendorSearchTerm ( getSelectedVendorName ( form . vendorId ));
}
}, 120 );
}}
placeholder = "Search vendor"
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/>
{ vendorPickerOpen ? (
< div className = "absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel" >
{ vendors
. filter (( vendor ) => {
const query = vendorSearchTerm . trim (). toLowerCase ();
if ( ! query ) {
return true ;
}
return vendor . name . toLowerCase (). includes ( query ) || vendor . email . toLowerCase (). includes ( query );
})
. slice ( 0 , 12 )
. map (( vendor ) => (
< button
key = { vendor . id }
type = "button"
onMouseDown = {( event ) => {
event . preventDefault ();
updateField ( "vendorId" , vendor . id );
setVendorSearchTerm ( vendor . name );
setVendorPickerOpen ( false );
}}
className = "block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70"
>
< div className = "font-semibold text-text" >{ vendor . name }</ div >
< div className = "mt-1 text-xs text-muted" >{ vendor . email }</ div >
</ button >
))}
{ filteredVendorCount === 0 ? < div className = "px-2 py-2 text-sm text-muted" > No matching vendors found .</ div > : null }
</ div >
) : null }
</ div >
< div className = "mt-2 min-h-5 text-xs text-muted" >{ form . vendorId ? getSelectedVendorName ( form . vendorId ) : "No vendor selected" }</ div >
{ form . vendorId ? (
< div className = "mt-1 text-xs text-muted" >
Terms : { getSelectedVendor ( form . vendorId ) ? . paymentTerms || "N/A" } | Currency : { getSelectedVendor ( form . vendorId ) ? . currencyCode || "USD" }
</ div >
) : null }
</ label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Status </ span >
< select value = { form . status } onChange = {( event ) => updateField ( "status" , event . target . value as PurchaseOrderInput [ "status" ])} className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
{ purchaseStatusOptions . map (( option ) => (
< option key = { option . value } value = { option . value }>{ option . label }</ option >
))}
</ select >
</ label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Issue date </ span >
< input type = "date" value = { form . issueDate . slice ( 0 , 10 )} onChange = {( event ) => updateField ( "issueDate" , 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 >
</ div >
2026-03-18 22:44:01 -05:00
< div className = "rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm" >
2026-03-18 11:54:22 -05:00
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Linked Project </ div >
< div className = "mt-2 font-semibold text-text" >
{ mode === "edit"
? ( form . projectId ? "Project context saved on this purchase order." : "No project linked." )
: ( seededProjectId ? ` ${ seededProjectNumber || "Project" }${ seededProjectName ? ` - ${ seededProjectName } ` : "" } ` : "Will auto-link from sales-order demand when possible." )}
</ div >
</ div >
2026-03-15 00:29:41 -05:00
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes </ span >
2026-03-15 20:07:48 -05:00
< textarea value = { form . notes } onChange = {( event ) => updateField ( "notes" , event . target . value )} rows = { 3 } 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 00:29:41 -05:00
</ label >
2026-03-15 21:07:28 -05:00
{ mode === "edit" ? (
< label className = "block xl:max-w-xl" >
< span className = "mb-2 block text-sm font-semibold text-text" > Revision Reason </ span >
< input
value = { form . revisionReason ?? "" }
onChange = {( event ) => updateField ( "revisionReason" , event . target . value )}
placeholder = "What changed in this revision?"
className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/>
</ label >
) : null }
2026-03-15 00:29:41 -05:00
< div className = "grid gap-3 xl:grid-cols-2" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Tax % </ span >
< input type = "number" min = { 0 } max = { 100 } step = { 0.01 } value = { form . taxPercent } onChange = {( event ) => updateField ( "taxPercent" , Number ( event . target . value ) || 0 )} 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" > Freight </ span >
< input type = "number" min = { 0 } step = { 0.01 } value = { form . freightAmount } onChange = {( event ) => updateField ( "freightAmount" , Number ( event . target . value ) || 0 )} className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</ label >
</ div >
</ section >
2026-03-18 22:44:01 -05:00
< section className = "surface-panel" >
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 >
2026-03-18 22:44:01 -05:00
< p className = "section-kicker" > LINE ITEMS </ p >
< h4 className = "text-lg font-bold text-text" > PROCUREMENT LINES </ h4 >
2026-03-15 00:29:41 -05:00
</ div >
< button type = "button" onClick = { addLine } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Add line </ button >
</ div >
{ form . lines . length === 0 ? (
2026-03-18 22:44:01 -05:00
< div className = "mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted" > No line items added yet .</ div >
2026-03-15 00:29:41 -05:00
) : (
2026-03-18 22:44:01 -05:00
< div className = "mt-3 space-y-3" >
2026-03-15 00:29:41 -05:00
{ form . lines . map (( line : PurchaseLineInput , index : number ) => (
2026-03-15 20:07:48 -05:00
< div key = { index } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 00:29:41 -05:00
< div className = "grid gap-3 xl:grid-cols-[1.15fr_1.25fr_0.5fr_0.55fr_0.7fr_0.75fr_auto]" >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > SKU </ span >
< div className = "relative" >
< input
value = { lineSearchTerms [ index ] ?? "" }
onChange = {( event ) => {
updateLineSearchTerm ( index , event . target . value );
updateLine ( index , { ... line , itemId : "" });
setActiveLinePicker ( index );
}}
onFocus = {() => setActiveLinePicker ( index )}
onBlur = {() => window . setTimeout (() => setActiveLinePicker (( current ) => ( current === index ? null : current )), 120 )}
placeholder = "Search by SKU"
className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/>
{ activeLinePicker === index ? (
< div className = "absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel" >
{ itemOptions
. filter (( option ) => option . sku . toLowerCase (). includes (( lineSearchTerms [ index ] ?? "" ). trim (). toLowerCase ()))
. slice ( 0 , 12 )
. map (( option ) => (
< button
key = { option . id }
type = "button"
onMouseDown = {( event ) => {
event . preventDefault ();
updateLine ( index , {
... line ,
itemId : option.id ,
description : line.description || option . name ,
2026-03-15 16:40:25 -05:00
salesOrderId : line.salesOrderId ?? null ,
salesOrderLineId : line.salesOrderLineId ?? null ,
2026-03-15 00:29:41 -05:00
});
updateLineSearchTerm ( index , option . sku );
setActiveLinePicker ( null );
}}
className = "block w-full border-b border-line/50 px-2 py-2 text-left text-sm font-semibold text-text transition last:border-b-0 hover:bg-page/70"
>
{ option . sku }
</ button >
))}
</ div >
) : null }
</ div >
</ label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Description </ span >
< input value = { line . description } onChange = {( event ) => updateLine ( index , { ... line , description : event.target.value })} className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</ label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Qty </ span >
< input type = "number" min = { 1 } step = { 1 } value = { line . quantity } onChange = {( event ) => updateLine ( index , { ... line , quantity : 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 >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > UOM </ span >
< select value = { line . unitOfMeasure } onChange = {( event ) => updateLine ( index , { ... line , unitOfMeasure : event.target.value as PurchaseLineInput [ "unitOfMeasure" ] })} className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" >
{ inventoryUnitOptions . map (( option ) => (
< option key = { option . value } value = { option . value }>{ option . label }</ option >
))}
</ select >
</ label >
< label className = "block" >
< span className = "mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Unit Cost </ span >
< input type = "number" min = { 0 } step = { 0.01 } value = { line . unitCost } onChange = {( event ) => updateLine ( index , { ... line , unitCost : Number ( event . target . value ) })} 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 className = "flex items-end" >< div className = "w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-sm text-text" > $ {( line . quantity * line . unitCost ). toFixed ( 2 )}</ div ></ div >
2026-03-15 19:40:35 -05:00
< div className = "flex items-end" >< button type = "button" onClick = {() => setPendingLineRemovalIndex ( index )} className = "rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300" > Remove </ button ></ div >
2026-03-15 00:29:41 -05:00
</ div >
</ div >
))}
</ div >
)}
2026-03-18 22:44:01 -05:00
< div className = "mt-4 grid gap-2 md:grid-cols-3 xl:grid-cols-4" >
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" >< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Subtotal </ div >< div className = "mt-1 font-semibold text-text" > $ { subtotal . toFixed ( 2 )}</ div ></ div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Tax </ div >< div className = "mt-1 font-semibold text-text" > $ { taxAmount . toFixed ( 2 )}</ div ></ div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Freight </ div >< div className = "mt-1 font-semibold text-text" > $ { form . freightAmount . toFixed ( 2 )}</ div ></ div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Total </ div >< div className = "mt-1 font-semibold text-text" > $ { total . toFixed ( 2 )}</ div ></ div >
</ div >
2026-03-18 22:44:01 -05:00
< div className = "mt-4 flex flex-col gap-2 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between" >
2026-03-15 00:29:41 -05:00
< span className = "min-w-0 text-sm text-muted" >{ status }</ span >
< button type = "submit" disabled = { isSaving } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" >
{ isSaving ? "Saving..." : mode === "create" ? "Create purchase order" : "Save changes" }
</ button >
</ div >
</ section >
2026-03-15 19:40:35 -05:00
< ConfirmActionDialog
open = { pendingLineRemoval != null }
title = "Remove purchase line"
description = {
pendingLineRemoval
? `Remove ${ pendingLineRemoval . sku || pendingLineRemoval . line ? . description || "this line" } from the purchase order draft.`
: "Remove this purchase line."
}
impact = "The line will be removed from the draft immediately and purchasing totals will recalculate."
recovery = "Re-add the line before saving if the removal was accidental."
confirmLabel = "Remove line"
onClose = {() => setPendingLineRemovalIndex ( null )}
onConfirm = {() => {
if ( pendingLineRemoval ) {
removeLine ( pendingLineRemoval . index );
}
setPendingLineRemovalIndex ( null );
}}
/>
2026-03-15 00:29:41 -05:00
</ form >
);
}
2026-03-15 20:07:48 -05:00