import { useEffect } from 'react'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Text } from '@astryxdesign/core/Text'; import { VStack } from '@astryxdesign/core/VStack'; import { Bike, Check, MapPin, Phone, X } from 'lucide-react'; import type { DeliveryRow, OrderRow } from '@/api/types'; import { branchLabel, moneyExact } from './format'; import { DELIVERY_STATUS, ORDER_STATUS, orderQuantity, orderValue, statusColor } from './orderStatus'; type Row = OrderRow | DeliveryRow; export type RowKind = 'order' | 'delivery'; /** * The five stages an order walks, taken from the old console verbatim. * * The middle one matters and is easy to drop: **Packed & Ready** sits between * confirmation and pickup, and it is the stage where an order stalls when a * shop is busy. A four-step timeline that jumps from Confirmed to Out for * Delivery hides exactly the delay an operator is trying to find. */ const STEPS: { label: string; fields: string[] }[] = [ // A delivery job has no `orderdate` of its own — its clock starts at // `deliverydate`. Falling back keeps the first step from reading as a missing // stamp on every delivery ever opened. { label: 'Order placed', fields: ['orderdate', 'deliverydate'] }, { label: 'Confirmed', fields: ['starttime', 'assigntime'] }, { label: 'Packed & ready', fields: ['packtime', 'arrivaltime'] }, { label: 'Out for delivery', fields: ['pickuptime'] }, { label: 'Delivered', fields: ['deliverytime'] }, ]; /** * One order or delivery, in full. * * A right-hand sheet rather than a modal: the operator is comparing this row * against the list behind it — "is this the one that stalled?" — and a modal * that greys the list out removes the thing being compared against. */ export function OrderDetailDrawer({ row, kind, onClose, }: { row: Row; kind: RowKind; onClose: () => void; }) { const delivery = kind === 'delivery'; // Narrowed once, here. `kind` comes from the table the row was clicked in, so // it is authoritative in a way that sniffing fields is not — a delivery row // carries an `orderheaderid` too, which is what made the first attempt at // this discriminator silently mis-classify every delivery. const job = delivery ? (row as DeliveryRow) : undefined; const order = delivery ? undefined : (row as OrderRow); // Escape closes. A dialog that can only be dismissed by finding the scrim // with a mouse is not dismissible for anyone on a keyboard, and this one sits // over the list an operator is working through. useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') onClose(); } window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [onClose]); const title = row.orderid || (job ? `DLV-${job.deliveryid}` : `#${order?.orderheaderid}`); const status = row.orderstatus; const colour = statusColor(delivery ? DELIVERY_STATUS : ORDER_STATUS, status); const value = job ? (job.deliveryamt ?? 0) : orderValue(row); const cancelled = (status ?? '').toLowerCase().includes('cancel'); return (
{/* ── Money ─────────────────────────────────────────────────── */}
0 ? moneyExact(value) : '—'} isStrong /> {job ? (
) : (
)}
{/* ── Journey ───────────────────────────────────────────────── */} {/* ── Pickup and drop ───────────────────────────────────────── */} } heading="Pickup" name={row.pickupcustomer || branchLabel(row.locationname)} detail={ job ? job.pickupsuburb || job.pickuplocation || job.Pickupaddress || job.pickupaddress : order?.pickupaddress || order?.pickupsuburb } phone={row.pickupcontactno} /> } heading="Drop" name={row.deliverycustomer} detail={row.deliveryaddress || row.deliverysuburb} phone={row.deliverycontactno} /> {/* ── Rider ─────────────────────────────────────────────────── */} {job ? ( {job.ridername ? ( {job.ridername} {job.ridercontact ? ( {job.ridercontact} ) : null} {/* Planned against actual. The gap is the number worth reading — a job quoted 3km and ridden 9 is either a bad address or a rider taking a detour, and one figure hides both. */}
{job.transitminutes ? (
) : null} ) : ( No rider assigned yet. )} ) : order?.rider ? ( {order.rider} {order.ridercontactno ? ` · ${order.ridercontactno}` : ''} ) : null} {/* ── Notes ─────────────────────────────────────────────────── */} {row.ordernotes || job?.notes ? ( {row.ordernotes || job?.notes} ) : null} Line items are not in the list read — Fiesta returns them from the order-detail endpoint, which this sheet does not call yet.
); } /* ────────────────────────────────────────────────────────────────────────── */ /** * The journey, as stamps rather than as a guess. * * A step is complete only when its timestamp is present. Nothing is inferred * from the status: an order marked Delivered with no `pickuptime` really does * have a missing stamp, and drawing that step as done would paper over the * data-quality problem the operator opened this sheet to find. */ function Timeline({ row, isCancelled }: { row: Row; isCancelled: boolean }) { const steps = STEPS.map((step) => ({ label: step.label, // Read by name: the two row shapes do not carry the same stamps — a // delivery has no `packtime`, an order has no `arrivaltime` — so a step // whose field is absent simply reads undefined and renders as not-yet. at: step.fields .map((field) => (row as unknown as Record)[field]) .find(Boolean), })); return ( {steps.map((step, index) => { const done = Boolean(step.at); const isLast = index === steps.length - 1; return ( {done ? : null} {!isLast ? ( ) : null} {step.label} {step.at ? ( {new Date(step.at).toLocaleString('en-IN', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false, })} ) : null} ); })} {isCancelled ? ( Cancelled {row.canceltime ? ` · ${new Date(row.canceltime).toLocaleString('en-IN', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false, })}` : ''} ) : null} ); } function Party({ icon, heading, name, detail, phone, }: { icon: React.ReactNode; heading: string; name: string | undefined; detail: string | undefined; phone: string | undefined; }) { return ( {icon} {heading} {name || '—'} {detail ? ( {detail} ) : null} {phone ? ( {phone} ) : null} ); } function Figure({ label, value, isStrong, }: { label: string; value: string; isStrong?: boolean; }) { return ( {label} {value} ); } const Label = ({ children }: { children: React.ReactNode }) => ( {children} ); const km = (value: string | undefined): string => { const n = Number(value); return Number.isFinite(n) && n > 0 ? `${n.toFixed(1)} km` : '—'; };