// Purchase-order detail screen — symmetric to SalesOrderDetailPage. // Confirm a DRAFT, receive a CONFIRMED into a warehouse, or cancel // either. Each action surfaces the corresponding stock movement // (PURCHASE_RECEIPT) and pbc-finance journal entry (AP, POSTED → // SETTLED on receive) inline. import { useCallback, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { finance, inventory, purchaseOrders } from '@/api/client' import type { JournalEntry, Location, PurchaseOrder, StockMovement } from '@/types/api' import { PageHeader } from '@/components/PageHeader' import { Loading } from '@/components/Loading' import { ErrorBox } from '@/components/ErrorBox' import { StatusBadge } from '@/components/StatusBadge' export function PurchaseOrderDetailPage() { const { id = '' } = useParams<{ id: string }>() const navigate = useNavigate() const [order, setOrder] = useState(null) const [locations, setLocations] = useState([]) const [receivingLocation, setReceivingLocation] = useState('') const [movements, setMovements] = useState([]) const [journalEntries, setJournalEntries] = useState([]) const [loading, setLoading] = useState(true) const [acting, setActing] = useState(false) const [error, setError] = useState(null) const [actionMessage, setActionMessage] = useState(null) const reloadSideEffects = useCallback(async (orderCode: string) => { const [ms, js] = await Promise.all([ inventory.listMovements(), finance.listJournalEntries(), ]) setMovements(ms.filter((m) => m.reference?.includes(orderCode) ?? false)) setJournalEntries(js.filter((j) => j.orderCode === orderCode)) }, []) useEffect(() => { let active = true setLoading(true) Promise.all([purchaseOrders.get(id), inventory.listLocations()]) .then(async ([o, locs]: [PurchaseOrder, Location[]]) => { if (!active) return setOrder(o) setLocations(locs.filter((l) => l.active)) const firstWarehouse = locs.find((l) => l.active && l.type === 'WAREHOUSE') setReceivingLocation(firstWarehouse?.code ?? locs[0]?.code ?? '') await reloadSideEffects(o.code) }) .catch((e: unknown) => { if (active) setError(e instanceof Error ? e : new Error(String(e))) }) .finally(() => active && setLoading(false)) return () => { active = false } }, [id, reloadSideEffects]) if (loading) return if (error) return if (!order) return const onConfirm = async () => { setActing(true) setError(null) setActionMessage(null) try { const updated = await purchaseOrders.confirm(order.id) setOrder(updated) await reloadSideEffects(updated.code) setActionMessage('Confirmed. pbc-finance has posted an AP journal entry.') } catch (e: unknown) { setError(e instanceof Error ? e : new Error(String(e))) } finally { setActing(false) } } const onReceive = async () => { if (!receivingLocation) { setError(new Error('Pick a receiving location first.')) return } setActing(true) setError(null) setActionMessage(null) try { const updated = await purchaseOrders.receive(order.id, receivingLocation) setOrder(updated) await reloadSideEffects(updated.code) setActionMessage( `Received into ${receivingLocation}. Stock credited, journal entry settled.`, ) } catch (e: unknown) { setError(e instanceof Error ? e : new Error(String(e))) } finally { setActing(false) } } const onCancel = async () => { setActing(true) setError(null) setActionMessage(null) try { const updated = await purchaseOrders.cancel(order.id) setOrder(updated) await reloadSideEffects(updated.code) setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.') } catch (e: unknown) { setError(e instanceof Error ? e : new Error(String(e))) } finally { setActing(false) } } const canConfirm = order.status === 'DRAFT' const canReceive = order.status === 'CONFIRMED' const canCancel = order.status === 'DRAFT' || order.status === 'CONFIRMED' return (
navigate('/purchase-orders')}> ← Back } />
Status:
Total
{Number(order.totalAmount).toLocaleString(undefined, { minimumFractionDigits: 2, })}{' '} {order.currencyCode}
{actionMessage && (
{actionMessage}
)}

Actions

Lines

{order.lines.map((l) => ( ))}
# Item Qty Unit price Line total
{l.lineNo} {l.itemCode} {String(l.quantity)} {Number(l.unitPrice).toLocaleString(undefined, { minimumFractionDigits: 2, })}{' '} {l.currencyCode} {Number(l.lineTotal).toLocaleString(undefined, { minimumFractionDigits: 2, })}

Inventory movements

{movements.length === 0 ? (
No movements yet.
) : ( {movements.map((m) => ( ))}
Item Δ Reason Reference
{m.itemCode} {String(m.delta)} {m.reason} {m.reference ?? '—'}
)}

Journal entries

{journalEntries.length === 0 ? (
No entries yet.
) : ( {journalEntries.map((j) => ( ))}
Code Type Status Amount
{j.code} {j.type} {Number(j.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '} {j.currencyCode}
)}
) }