import { Alert, Badge, Button, Checkbox, Group, Loader, Modal, Select, Stack, Table, Tabs, Text, Tooltip, } from '@mantine/core'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FileText } from 'lucide-react'; import { useMemo, useState } from 'react'; import { useToast } from '@/hooks/use-toast'; import { warehouseService, type ContainerItem, type ContainerItemStage, } from '@/services/warehouse.service'; import { extractDownloadErrorMessage, extractErrorMessage } from './options'; import { openPdfBlob } from './pdf'; interface ContainerItemsModalProps { opened: boolean; onClose: () => void; bookingId: string | null; bookingReference?: string | null; } const STAGE_TABS: Array<{ value: string; label: string }> = [ { value: 'ALL', label: 'All' }, { value: 'RECEIVED', label: 'Received' }, { value: 'GRN', label: "GRN'd" }, { value: 'ASSIGNED', label: 'Assigned' }, { value: 'LOADED', label: 'Loaded' }, { value: 'LEFT', label: 'Left' }, { value: 'DELIVERED', label: 'Delivered' }, ]; const STAGE_COLOR: Record = { PENDING: 'gray', RECEIVED: 'blue', GRN: 'teal', ASSIGNED: 'indigo', LOADED: 'grape', LEFT: 'orange', DELIVERED: 'green', }; /** Loadable = not yet loaded (PENDING/RECEIVED/GRN, or customer-ASSIGNED awaiting load). */ const isLoadable = (i: ContainerItem) => i.stage === 'PENDING' || i.stage === 'RECEIVED' || i.stage === 'GRN' || i.stage === 'ASSIGNED'; export function ContainerItemsModal({ opened, onClose, bookingId, bookingReference }: ContainerItemsModalProps) { const { toast } = useToast(); const queryClient = useQueryClient(); const [tab, setTab] = useState('ALL'); const [selected, setSelected] = useState([]); const [truckId, setTruckId] = useState(null); const itemsKey = ['container-items', bookingId]; const { data: items = [], isLoading } = useQuery({ queryKey: itemsKey, queryFn: () => warehouseService.getContainerItems(bookingId as string), enabled: opened && Boolean(bookingId), }); const { data: trucks = [] } = useQuery({ queryKey: ['ci-trucks', bookingId], queryFn: () => warehouseService.getCustomerTrucks(bookingId as string), enabled: opened && Boolean(bookingId), }); const visible = useMemo( () => (tab === 'ALL' ? items : items.filter((i) => i.stage === tab)), [items, tab], ); // Any assigned, not-yet-departed truck can be loaded here — loading a truck at // the warehouse auto-marks it arrived on the backend, so assigned-but-not-yet- // arrived trucks are selectable too (labelled "assigned" until they arrive). const truckOptions = trucks .filter((t) => !(t as { departedAt?: string }).departedAt) .map((t) => ({ value: t.id, label: `${t.plateNumber} · ${t.driverName}${ (t as { arrivedAt?: string }).arrivedAt ? '' : ' (assigned)' }`, })); const loadMutation = useMutation({ mutationFn: () => warehouseService.loadTruck(bookingId as string, truckId as string, selected), onSuccess: () => { queryClient.invalidateQueries({ queryKey: itemsKey }); setSelected([]); toast({ title: 'Containers loaded onto truck' }); }, onError: (e) => toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(e) }), }); const requestSign = async () => { try { const res = await warehouseService.requestHandoverSignature(bookingId as string); queryClient.invalidateQueries({ queryKey: itemsKey }); if (res.alreadySigned) { toast({ title: 'Handover already signed', description: 'You can generate the exit paper now.' }); } else { toast({ title: 'Handover not signed', description: `Signature request sent to the customer${res.reference ? ` (${res.reference})` : ''}.`, }); } } catch (e) { toast({ variant: 'destructive', title: 'Could not request signature', description: extractErrorMessage(e) }); } }; const openExitPaper = async (assignmentId: string, plate: string) => { try { const res = await warehouseService.downloadTruckExitPaper(assignmentId); openPdfBlob(res.data, `exit-${plate}.pdf`); } catch (e) { toast({ variant: 'destructive', title: 'Exit paper not ready', description: await extractDownloadErrorMessage(e) }); } }; const is40 = (n: string) => (items.find((i) => i.containerNumber === n)?.containerSize ?? '').includes('40'); // A truck carries at most 2 containers, and a 40ft fills the truck (max 1). const toggle = (n: string) => setSelected((s) => { if (s.includes(n)) return s.filter((x) => x !== n); const next = [...s, n]; if (next.length > 2) { toast({ variant: 'destructive', title: 'A truck carries at most 2 containers' }); return s; } if (next.length > 1 && next.some(is40)) { toast({ variant: 'destructive', title: 'A 40ft container fills the truck', description: 'Load only one 40ft container per truck.', }); return s; } return next; }); return ( Container / bulk items {bookingReference ? `· ${bookingReference}` : ''}} > setTab(v ?? 'ALL')} mb="sm"> {STAGE_TABS.map((t) => { const count = t.value === 'ALL' ? items.length : items.filter((i) => i.stage === t.value).length; return ( {count}}> {t.label} ); })} {isLoading ? ( ) : items.length === 0 ? ( No container or bulk items on this booking. ) : ( Container Size Goods Stage Truck Booking Contract Last mile Actions {visible.map((i) => ( toggle(i.containerNumber)} disabled={!isLoadable(i)} /> {i.containerNumber} {i.containerSize ? ( {i.containerSize} ) : ( bulk )} {i.goods ?? '—'} {i.stage} {i.truckPlate ?? '—'} {i.bookingReference ?? '—'} {i.contractId ? Contract : '—'} {i.hasLastMile ? EDR : Self-haul} {i.loaded && i.truckAssignmentId && ( )} ))}
{/* Multiselect → load onto a truck */} {selected.length} selected {(() => { const pending = items.filter((i) => !i.truckAssignmentId).length; return pending > 0 ? ` · ${pending} container${pending === 1 ? '' : 's'} pending assignment` : ''; })()}