import { Fragment, useEffect, useMemo, useState } from 'react'; import { ActionIcon, Alert, Badge, Button, Card, Checkbox, CopyButton, Group, Loader, Menu, Modal, MultiSelect, NumberInput, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Table, Tabs, Text, Textarea, TextInput, ThemeIcon, Tooltip, } from '@mantine/core'; import { ArrowRightLeft, Calendar, Check, CheckCheck, ChevronDown, ChevronRight, ClipboardCheck, Copy, Eye, FileText, History, Info, Layers, MapPin, MoreHorizontal, PackageCheck, PackageOpen, PackageSearch, Send, Search, Train, Truck, } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useAuth } from '@/auth/useAuth'; import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from '@/lib/permissions'; import { api } from '@/services/api'; import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; import { useToast } from '@/hooks/use-toast'; import { useAllWarehouseYards, useAllWarehouseZones, useInventoryInquiry } from '@/hooks/useWarehouses'; import { firstMileService } from '@/services/first-mile.service'; import { bookingsService } from '@/services/bookings.service'; import { warehouseService } from '@/services/warehouse.service'; import type { EligibleBooking, InventoryInquiryFilter, InventoryStatus, InventoryInquiryResult, ImportTrain, ImportTrainItem, ImportUnloadedItem, ReadyToLoadRow, ReceiveInventoryPayload, TruckEntrancePayload, Warehouse, WarehouseInventoryItem, WarehouseYard, WarehouseZone, } from '@/types/warehouse'; import { InventoryStatusBadge } from './badges'; import { BookingSelect } from './BookingSelect'; import { DeliverInventoryModal } from './DeliverInventoryModal'; import { ContainerItemsModal } from './ContainerItemsModal'; import { FeePreviewModal } from './FeePreviewModal'; import { GrnDocumentButton } from './GrnDocumentButton'; import { InspectionReportModal } from './InspectionReportModal'; import { InventoryDetailModal } from './InventoryDetailModal'; import { InventoryHistoryModal } from './InventoryHistoryModal'; import { InventoryWorkbench } from './InventoryWorkbench'; import { InventoryInquiryDetailModal } from './InventoryInquiryDetailModal'; import { MoveInventoryModal } from './MoveInventoryModal'; import { ReleaseOrderModal } from './ReleaseOrderModal'; import { StoreInventoryModal } from './StoreInventoryModal'; import { WarehouseInquiryTable } from './WarehouseInquiryTable'; import { TrainLoadingWorkspace } from './TrainLoadingWorkspace'; import { YardLoadingWindows } from './YardLoadingWindows'; import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options'; import { openPdfBlob } from './pdf'; import ListControls from '@/components/common/ListControls'; import RuleEngineListFooter from '@/components/ruleEngine/RuleEngineListFooter'; import { useListControls } from '@/hooks/useListControls'; import '@/components/overview/overview.css'; type ImportUnloadAssignment = { bookingId: string; warehouseId: string; yardId: string; zoneId: string }; type ImportUnloadAssignmentDraft = Partial>; interface ReceiveInventoryModalProps { opened: boolean; onClose: () => void; /** When supplied the modal locks to a single booking (legacy single-receive). */ bookingId?: string; bookingLabel?: string; mode?: 'single' | 'bulk'; direction?: WarehouseFlowDirection; onReceived?: () => void; } interface Location { warehouseId: string; yardId: string; zoneId: string; } interface TruckEntranceFormState { ownerName: string; consigneeDetails: string; edrDigitalBookingId: string; tin: string; customerPhone: string; truckPlateNumber: string; trailerPlateNumber: string; assignedEquipmentNumber: string; customsSealNumber: string; declarationNumber: string; itemDescription: string; packagingType: string; unitCount: number | ''; grossWeightKg: number | ''; weighingRequired: boolean | null; netWeightKg: number | ''; volumeDimensions: string; conditionAtReceipt: string; damagedRejectedQuantity: number | ''; driverName: string; driverPhone: string; driverLicenseNumber: string; truckType: string; entranceTareWeightKg: number | ''; exitTareWeightKg: number | ''; driverSignatoryName: string; warehouseManagerName: string; } interface LockedTruckEntranceFields { ownerName?: boolean; consigneeDetails?: boolean; tin?: boolean; edrDigitalBookingId?: boolean; customerPhone?: boolean; truckPlateNumber?: boolean; trailerPlateNumber?: boolean; assignedEquipmentNumber?: boolean; itemDescription?: boolean; packagingType?: boolean; unitCount?: boolean; grossWeightKg?: boolean; driverName?: boolean; driverPhone?: boolean; driverLicenseNumber?: boolean; truckType?: boolean; } type PackagingFreightType = 'CONTAINER' | 'BULK' | 'MIXED'; const emptyTruckEntrance = (): TruckEntranceFormState => ({ ownerName: '', consigneeDetails: '', edrDigitalBookingId: '', tin: '', customerPhone: '', truckPlateNumber: '', trailerPlateNumber: '', assignedEquipmentNumber: '', customsSealNumber: '', declarationNumber: '', itemDescription: '', packagingType: '', unitCount: '', grossWeightKg: '', weighingRequired: null, netWeightKg: '', volumeDimensions: '', conditionAtReceipt: '', damagedRejectedQuantity: '', driverName: '', driverPhone: '', driverLicenseNumber: '', truckType: '', entranceTareWeightKg: '', exitTareWeightKg: '', driverSignatoryName: '', warehouseManagerName: '', }); const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayload => ({ ownerName: form.ownerName.trim() || undefined, consigneeDetails: form.consigneeDetails.trim() || undefined, edrDigitalBookingId: form.edrDigitalBookingId.trim() || undefined, tin: form.tin.trim() || undefined, customerPhone: form.customerPhone.trim() || undefined, truckPlateNumber: form.truckPlateNumber.trim(), trailerPlateNumber: form.trailerPlateNumber.trim() || undefined, assignedEquipmentNumber: form.assignedEquipmentNumber.trim() || undefined, customsSealNumber: form.customsSealNumber.trim() || undefined, declarationNumber: form.declarationNumber.trim() || undefined, itemDescription: form.itemDescription.trim() || undefined, packagingType: form.packagingType.trim() || undefined, unitCount: form.unitCount === '' ? undefined : Number(form.unitCount), weighingRequired: form.weighingRequired ?? undefined, grossWeightKg: form.weighingRequired && form.grossWeightKg !== '' ? Number(form.grossWeightKg) : undefined, netWeightKg: form.netWeightKg === '' ? undefined : Number(form.netWeightKg), volumeDimensions: form.volumeDimensions.trim() || undefined, conditionAtReceipt: form.conditionAtReceipt.trim() || undefined, damagedRejectedQuantity: form.damagedRejectedQuantity === '' ? undefined : Number(form.damagedRejectedQuantity), driverName: form.driverName.trim(), driverPhone: form.driverPhone.trim(), driverLicenseNumber: form.driverLicenseNumber.trim() || undefined, truckType: form.truckType.trim() || undefined, entranceTareWeightKg: form.entranceTareWeightKg === '' ? undefined : Number(form.entranceTareWeightKg), exitTareWeightKg: form.weighingRequired && form.exitTareWeightKg !== '' ? Number(form.exitTareWeightKg) : undefined, driverSignatoryName: form.driverSignatoryName.trim() || undefined, warehouseManagerName: form.warehouseManagerName.trim() || undefined, }); const SUB_STAGE_COLOR: Record = { PENDING: 'gray', RECEIVED: 'blue', GRN: 'teal', ASSIGNED: 'indigo', LOADED: 'grape', LEFT: 'orange', DELIVERED: 'green', }; /** * Expanded booking row: the booking's containers / bulk items with their * lifecycle stage. Shares the ['container-items', bookingId] cache with * ContainerItemsModal, so expanding after using the modal is instant. */ function BookingItemsExpansion({ bookingId, colSpan, bulkFallback, }: { bookingId: string | null; colSpan: number; bulkFallback?: string; }) { const { data: items = [], isLoading } = useQuery({ queryKey: ['container-items', bookingId], queryFn: () => warehouseService.getContainerItems(bookingId as string), enabled: Boolean(bookingId), }); return ( {isLoading ? ( ) : items.length === 0 ? ( {bulkFallback ?? 'No container units recorded on this booking.'} ) : ( Container # Goods Stage Truck GRN {items.map((i) => ( {i.containerNumber} {i.goods ?? '—'} {i.stage} {i.truckPlate ?? '—'} {i.grnNumber ?? '—'} ))}
)}
); } type ConfirmAction = { title: string; message: string; confirmLabel: string; run: () => void }; /** One-click bulk actions are irreversible — make the click deliberate. */ function ConfirmActionModal({ action, onClose, }: { action: ConfirmAction | null; onClose: () => void; }) { return ( {action?.message} ); } /** "3 skipped — Booking not PAID" instead of a bare count. */ const skippedSummary = ( skippedCount: number, results: Array<{ reason?: string; message?: string }>, ): string | undefined => { if (!skippedCount) return undefined; const reason = results.find((x) => x.reason || x.message); return `${skippedCount} skipped${reason ? ` — ${reason.reason ?? reason.message}` : ''}`; }; const commonNonEmptyValue = (values: Array) => { const unique = [...new Set(values.map((value) => value?.trim()).filter(Boolean))] as string[]; return unique.length === 1 ? unique[0] : ''; }; const truckEntranceFromBookings = (bookings: EligibleBooking[]): { form: TruckEntranceFormState; lockedFields: LockedTruckEntranceFields; packagingFreightType: PackagingFreightType; } => { const ownerName = commonNonEmptyValue(bookings.map((booking) => booking.customer)); const tin = commonNonEmptyValue(bookings.map((booking) => booking.customerTin)); const customerPhone = commonNonEmptyValue(bookings.map((booking) => booking.customerPhone)); const consigneeDetails = commonNonEmptyValue(bookings.map((booking) => booking.customer)); const assignedEquipmentNumber = commonNonEmptyValue( bookings.map((booking) => booking.customerTruckContainerNumber || booking.containerNumber), ); const itemDescription = commonNonEmptyValue(bookings.map((booking) => booking.cargoDescription ?? booking.cargo)); const packagingType = commonNonEmptyValue(bookings.map((booking) => booking.containerPackagingType)); const truckPlateNumber = commonNonEmptyValue( bookings.map((booking) => booking.firstMileTruckPlateNumber || booking.customerTruckPlateNumber), ); const trailerPlateNumber = commonNonEmptyValue(bookings.map((booking) => booking.firstMileTrailerPlateNumber)); const driverName = commonNonEmptyValue( bookings.map((booking) => booking.firstMileDriverName || booking.customerTruckDriverName), ); const driverPhone = commonNonEmptyValue(bookings.map((booking) => booking.firstMileDriverPhone)); const driverLicenseNumber = commonNonEmptyValue(bookings.map((booking) => booking.firstMileDriverLicenseNumber)); const truckType = commonNonEmptyValue( bookings.map((booking) => booking.firstMileTruckType || booking.customerTruckType), ); const customsSealNumber = commonNonEmptyValue(bookings.map((booking) => booking.sealNumbers)); // Booking's declared cargo weight (tonnes) — the receive-time net until re-weighed. const bookingWeight = bookings.length === 1 ? Number(bookings[0]?.weight ?? '') : NaN; const netWeightKg: number | '' = Number.isFinite(bookingWeight) && bookingWeight > 0 ? bookingWeight : ''; const edrDigitalBookingId = bookings.length === 1 ? bookings[0]?.reference ?? bookings[0]?.id ?? '' : commonNonEmptyValue(bookings.map((booking) => booking.reference)); const unitCount = bookings.length === 1 && bookings[0]?.containerQuantity != null ? Number(bookings[0].containerQuantity) : ''; const freightTypes = [...new Set(bookings.map((booking) => booking.freightType).filter(Boolean))]; const packagingFreightType = freightTypes.length === 1 && freightTypes[0] === 'CONTAINER' ? 'CONTAINER' : freightTypes.length === 1 && freightTypes[0] === 'BULK' ? 'BULK' : 'MIXED'; return { form: { ...emptyTruckEntrance(), ownerName, consigneeDetails, tin, customerPhone, edrDigitalBookingId, assignedEquipmentNumber, customsSealNumber, itemDescription, packagingType, unitCount, netWeightKg, grossWeightKg: '', truckPlateNumber, trailerPlateNumber, driverName, driverPhone, driverLicenseNumber, truckType, driverSignatoryName: driverName, }, lockedFields: { ownerName: Boolean(ownerName), consigneeDetails: Boolean(consigneeDetails), tin: Boolean(tin), edrDigitalBookingId: Boolean(edrDigitalBookingId), customerPhone: Boolean(customerPhone), assignedEquipmentNumber: Boolean(assignedEquipmentNumber), itemDescription: Boolean(itemDescription), packagingType: Boolean(packagingType), unitCount: unitCount !== '', grossWeightKg: false, truckPlateNumber: Boolean(truckPlateNumber), trailerPlateNumber: Boolean(trailerPlateNumber), driverName: Boolean(driverName), driverPhone: Boolean(driverPhone), driverLicenseNumber: Boolean(driverLicenseNumber), truckType: Boolean(truckType), }, packagingFreightType, }; }; const BULK_PACKAGING_TYPE_OPTIONS = [ { value: 'BAG', label: 'Bag' }, { value: 'SACK', label: 'Sack' }, { value: 'BALE', label: 'Bale' }, { value: 'CARTON', label: 'Carton' }, { value: 'CRATE', label: 'Crate' }, { value: 'DRUM', label: 'Drum' }, { value: 'BARREL', label: 'Barrel' }, { value: 'PALLET', label: 'Pallet' }, { value: 'LOOSE_BULK', label: 'Loose bulk' }, { value: 'OTHER', label: 'Other' }, ]; const CONTAINER_PACKAGING_TYPE_OPTIONS = [ { value: 'CONTAINER_20FT', label: '20 ft container' }, { value: 'CONTAINER_40FT', label: '40 ft container' }, { value: 'CONTAINER_45FT', label: '45 ft container' }, { value: 'REEFER_CONTAINER', label: 'Reefer container' }, { value: 'TANK_CONTAINER', label: 'Tank container' }, { value: 'FLAT_RACK_CONTAINER', label: 'Flat rack container' }, { value: 'OPEN_TOP_CONTAINER', label: 'Open top container' }, { value: 'OTHER_CONTAINER', label: 'Other container' }, ]; const packagingOptionsFor = (freightType: PackagingFreightType) => freightType === 'CONTAINER' ? CONTAINER_PACKAGING_TYPE_OPTIONS : freightType === 'BULK' ? BULK_PACKAGING_TYPE_OPTIONS : [...CONTAINER_PACKAGING_TYPE_OPTIONS, ...BULK_PACKAGING_TYPE_OPTIONS]; function TruckEntranceFields({ value, onChange, lockedFields, packagingFreightType = 'MIXED', allowTruckWeighing = true, }: { value: TruckEntranceFormState; onChange: (next: TruckEntranceFormState) => void; lockedFields?: LockedTruckEntranceFields; packagingFreightType?: PackagingFreightType; allowTruckWeighing?: boolean; }) { const packagingOptions = packagingOptionsFor(packagingFreightType); const quantityLabel = packagingFreightType === 'CONTAINER' ? 'Container quantity' : packagingFreightType === 'BULK' ? 'Unit count' : 'Quantity'; return ( Customer and cargo ownership onChange({ ...value, ownerName: e.currentTarget.value })} /> onChange({ ...value, consigneeDetails: e.currentTarget.value })} /> onChange({ ...value, edrDigitalBookingId: e.currentTarget.value })} /> onChange({ ...value, tin: e.currentTarget.value })} /> onChange({ ...value, customerPhone: e.currentTarget.value })} /> Transport and equipment tracking onChange({ ...value, truckPlateNumber: e.currentTarget.value })} /> onChange({ ...value, trailerPlateNumber: e.currentTarget.value })} /> onChange({ ...value, assignedEquipmentNumber: e.currentTarget.value })} /> onChange({ ...value, customsSealNumber: e.currentTarget.value })} /> onChange({ ...value, driverName: e.currentTarget.value })} /> onChange({ ...value, driverPhone: e.currentTarget.value })} /> onChange({ ...value, driverLicenseNumber: e.currentTarget.value })} /> onChange({ ...value, truckType: e.currentTarget.value })} /> {allowTruckWeighing ? ( <> onChange({ ...value, packagingType: v ?? '' })} /> onChange({ ...value, unitCount: v === '' ? '' : Number(v) })} /> onChange({ ...value, netWeightKg: v === '' ? '' : Number(v) })} /> onChange({ ...value, volumeDimensions: e.currentTarget.value })} /> Quality and inspection onChange({ ...value, conditionAtReceipt: e.currentTarget.value })} /> onChange({ ...value, damagedRejectedQuantity: v === '' ? '' : Number(v) })} /> onChange({ ...value, driverSignatoryName: e.currentTarget.value })} /> onChange({ ...value, warehouseManagerName: e.currentTarget.value })} /> ); } /** Cascading Warehouse → Yard → Zone selectors (ACTIVE only). */ function LocationSelects({ value, onChange, allowedYardTypes, allowedZoneTypes, }: { value: Location; onChange: (next: Location) => void; /** When non-empty, only yards of these types are offered (matched to freight). */ allowedYardTypes?: string[]; /** When non-empty, only zones of these types are offered. */ allowedZoneTypes?: string[]; }) { const warehousesQuery = useQuery( api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }), ); const yardsQuery = useQuery( api.warehouses.listYards.queryOptions({ input: { warehouseId: value.warehouseId ?? '' }, enabled: Boolean(value.warehouseId), }), ); const zonesQuery = useQuery( api.warehouses.listZones.queryOptions({ input: { yardId: value.yardId ?? '' }, enabled: Boolean(value.yardId), }), ); const warehouseOptions = useMemo( () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), [warehousesQuery.data], ); const yardOptions = useMemo( () => (yardsQuery.data ?? []) .filter((y) => y.status === 'ACTIVE') .filter((y) => !allowedYardTypes?.length || allowedYardTypes.includes((y as { type?: string }).type ?? '')) .map((y) => ({ value: y.id, label: `${y.name} (${y.code})` })), [yardsQuery.data, allowedYardTypes], ); const zoneOptions = useMemo( () => (zonesQuery.data ?? []) .filter((z) => z.status === 'ACTIVE') .filter((z) => !allowedZoneTypes?.length || allowedZoneTypes.includes((z as { type?: string }).type ?? '')) .map((z) => ({ value: z.id, label: `${z.name} (${z.code})` })), [zonesQuery.data, allowedZoneTypes], ); return ( onChange({ ...value, yardId: v ?? '', zoneId: '' })} /> ({ value: truck.id, label: `${truck.plateNumber} · ${truck.driverName} · ${(truck.containers ?? []) .map((container) => container.containerNumber) .join(', ') || 'no containers'}`, }))} value={selectedCustomerTruckId} onChange={chooseCustomerTruck} searchable required /> )} { const selected = selectedContainerNumbers.includes(unit.containerNumber); const selectedHasNon20 = selectedContainerUnits.some( (selectedUnit) => !String(selectedUnit.containerSize ?? '').includes('20'), ); const candidateIs20 = String(unit.containerSize ?? '').includes('20'); return { value: unit.containerNumber, label: `${unit.containerNumber} · ${unit.containerSize ?? 'size unknown'} · ${Number( unit.weightTons || 0, ).toLocaleString()} t`, disabled: !selected && (selectedContainerNumbers.length >= 2 || (selectedContainerNumbers.length === 1 && (selectedHasNon20 || !candidateIs20))), }; })} value={selectedContainerNumbers} onChange={setSelectedContainerNumbers} maxValues={2} searchable required /> {containerCapacityError && ( {containerCapacityError} )} )} Booking Customer TIN / Phone Container / Cargo Qty / Package Weight Received at {pendingReceiveRows.map((booking) => ( {booking.reference} {booking.id.slice(0, 8)}... {booking.customer ?? '-'} {booking.customerTin ?? '-'} {booking.customerPhone ?? '-'} {booking.containerNumber ?? booking.cargoDescription ?? booking.cargo ?? '-'} {booking.freightType ?? '-'} {[ booking.containerQuantity != null ? `${booking.containerQuantity} unit(s)` : null, booking.containerPackagingType, ].filter(Boolean).join(' / ') || '-'} {formatNumber(Number(booking.weight))} {formatDate(receivedAt)} ))}
); } /** Export received items awaiting inspection before loading. */ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) { const { toast } = useToast(); const { data: rows = [], isLoading } = useQuery( api.warehouses.receivedExport.queryOptions({ enabled }), ); const inspectMutation = useMutation( api.warehouses.bulkMarkInspected.mutationOptions(), ); const [selected, setSelected] = useState>(new Set()); const [confirmAction, setConfirmAction] = useState(null); const [expandedRow, setExpandedRow] = useState(null); const [inspectId, setInspectId] = useState(null); const controls = useListControls(rows, { searchKeys: ['bookingReference', 'customerName', 'containerNumber', 'cargoType', 'grnNumber', 'origin', 'destination'], }); const pendingRows = controls.filteredRows.filter((r) => r.inspectionStatus !== 'PASSED'); const allSelected = pendingRows.length > 0 && selected.size === pendingRows.length; const someSelected = selected.size > 0 && !allSelected; const toggleAll = () => setSelected(allSelected ? new Set() : new Set(pendingRows.map((r) => r.id))); const toggleOne = (id: string) => setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); const markInspected = async () => { if (selected.size === 0) { toast({ variant: 'destructive', title: 'Select at least one item' }); return; } try { const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] }); toast({ title: `${r.inspectedCount} marked inspected`, description: skippedSummary(r.skippedCount, r.results), }); setSelected(new Set()); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Mark inspected failed', description: extractErrorMessage(error) }); } }; return ( Selected: {selected.size} / {pendingRows.length} received {isLoading ? ( ) : rows.length === 0 ? ( No received export items awaiting inspection. ) : ( Booking Ref GRN Customer Name Container / Cargo Items Cargo Type Weight Route Inspection Status Actions {controls.pagedRows.map((r: ReadyToLoadRow) => { const selectable = r.inspectionStatus !== 'PASSED'; return ( setExpandedRow(expandedRow === r.id ? null : r.id)} > {expandedRow === r.id ? : } toggleOne(r.id)} /> {r.bookingReference ?? '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.inspectionStatus ?? 'PENDING'} {expandedRow === r.id && ( )} ); })}
)} setInspectId(null)} /> setConfirmAction(null)} />
); } /** Export items that passed inspection and are queued to be loaded onto a train. */ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) { const { toast } = useToast(); const { user } = useAuth(); const canLoad = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.load); const { data: rows = [], isLoading } = useQuery( api.warehouses.readyToLoadExport.queryOptions({ enabled }), ); const qc = useQueryClient(); // "Items" is the inventory list with the auto-load picker; "By train" mirrors // the train schedule's per-booking Load / Wagons / Unload workspace here. const [view, setView] = useState<'items' | 'train'>('items'); const [trainPickerOpen, setTrainPickerOpen] = useState(false); const [expandedRow, setExpandedRow] = useState(null); const [targetScheduleId, setTargetScheduleId] = useState(null); const [selected, setSelected] = useState>(new Set()); const controls = useListControls(rows, { searchKeys: ['bookingReference', 'customerName', 'containerNumber', 'cargoType', 'grnNumber', 'origin', 'destination'], }); const allSelected = controls.filteredRows.length > 0 && selected.size === controls.filteredRows.length; const someSelected = selected.size > 0 && !allSelected; const toggleAll = () => setSelected(allSelected ? new Set() : new Set(controls.filteredRows.map((r) => r.id))); const toggleOne = (id: string) => setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); // Loading is always onto a SPECIFIC pre-dispatch train. No train -> no auto-load. const { data: trains = [], isLoading: trainsLoading } = useQuery({ queryKey: ['warehouse-inventory', 'loadable-trains'], queryFn: () => warehouseService.getLoadableTrains(), enabled: enabled && trainPickerOpen, }); const { data: pickerItems = [] } = useQuery({ queryKey: ['train-loadable-items', targetScheduleId], queryFn: () => warehouseService.getTrainLoadableItems(targetScheduleId!), enabled: Boolean(targetScheduleId) && trainPickerOpen, }); const loadOntoTrain = useMutation({ mutationFn: async ({ scheduleId, onlyIds }: { scheduleId: string; onlyIds: string[] }) => { const items = await warehouseService.getTrainLoadableItems(scheduleId); let loadableIds = items.filter((i) => i.loadable).map((i) => i.id); // When rows are checked, load only those; otherwise load every loadable item. if (onlyIds.length) { const picked = new Set(onlyIds); loadableIds = loadableIds.filter((id) => picked.has(id)); } if (!loadableIds.length) { const scope = onlyIds.length ? items.filter((i) => onlyIds.includes(i.id)) : items.filter((i) => i.status === 'READY_FOR_LOADING'); // A closed loading window is the blocker staff hit most, and the old // wagon-only message sent them to fix the wrong thing. const shut = scope.find((i) => !i.loadingWindowStarted); if (shut) { throw new Error( `Start loading at ${shut.originYardLabel ?? 'the boarding yard'} first — the loading time window has not been started`, ); } throw new Error( onlyIds.length ? 'None of the selected items have an allocated wagon on this train' : 'No ready items with an allocated wagon on this train', ); } return warehouseService.loadItemsOntoTrain(scheduleId, loadableIds); }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); void qc.invalidateQueries({ queryKey: ['warehouse-loadings'] }); }, }); const confirmLoad = async () => { if (!targetScheduleId) { toast({ variant: 'destructive', title: 'Select a train to load onto' }); return; } try { const r = await loadOntoTrain.mutateAsync({ scheduleId: targetScheduleId, onlyIds: [...selected], }); const train = trains.find((t) => t.scheduleId === targetScheduleId); toast({ title: `${r.loadedCount} item(s) loaded onto train ${train?.trainNumber ?? ''}`.trim(), description: r.skippedCount ? `${r.skippedCount} skipped — ${r.results.find((x) => x.reason)?.reason ?? 'see results'}` : undefined, }); setTrainPickerOpen(false); setTargetScheduleId(null); setSelected(new Set()); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) }); } }; if (view === 'train') { return ( setView(v as 'items' | 'train')} data={[ { value: 'items', label: 'Items' }, { value: 'train', label: 'By train' }, ]} /> Per-booking Load, wagon-by-wagon loading and unloading — the train schedule's own actions, run from the warehouse. ); } return ( setView(v as 'items' | 'train')} data={[ { value: 'items', label: 'Items' }, { value: 'train', label: 'By train' }, ]} /> {selected.size > 0 ? ( <>{selected.size} of {controls.filteredRows.length} selected ) : ( <>{controls.filteredRows.length} item{controls.filteredRows.length !== 1 ? 's' : ''} ready to load )} setTrainPickerOpen(false)} title="Load ready items onto a train" centered size="lg" > {trainsLoading ? ( ) : trains.length === 0 ? ( }> No train available. Auto-loading needs a scheduled (not yet dispatched) train with these bookings assigned — schedule the train and allocate wagons first. ) : ( onAssignmentChange(it.bookingId, { warehouseId: value ?? undefined })} searchable disabled={!pending} w={210} /> onAssignmentChange(it.bookingId, { ...draft, zoneId: value ?? undefined })} searchable disabled={!pending || !draft.yardId} w={190} /> {it.inspectionStatus ?? 'Not inspected'} {it.currentStatus ?? '—'} {it.lastMileRequested ? 'Yes' : 'No'} {it.pickupOption} ); })} ); } /** Import Arrive Queue: arrived IMPORT trains, with Open (detail) + Auto Unload per train. */ const getPendingUnloadBookings = (train: ImportTrain) => train.pendingUnloadBookings ?? train.totalBookings; const isFullyUnloaded = (train: ImportTrain) => Boolean(train.fullyUnloaded) || (train.totalBookings > 0 && getPendingUnloadBookings(train) === 0); export function ImportArriveQueueTab({ enabled, onChanged, }: { enabled: boolean; onChanged?: () => void; }) { const { toast } = useToast(); const { user } = useAuth(); const canUnload = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.unload); const { data: trains = [], isLoading } = useQuery( api.warehouses.importArriveQueue.queryOptions({ enabled }), ); const { data: warehouses = [], isLoading: warehousesLoading } = useQuery( api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } }, enabled }), ); const { data: yards = [] } = useAllWarehouseYards(); const { data: zones = [] } = useAllWarehouseZones(); const autoUnloadMutation = useMutation( api.warehouses.autoUnloadArrivedBookings.mutationOptions(), ); const [openId, setOpenId] = useState(null); const [confirmAction, setConfirmAction] = useState(null); const [busyId, setBusyId] = useState(null); const [assignmentsBySchedule, setAssignmentsBySchedule] = useState< Record> >({}); const [readyBySchedule, setReadyBySchedule] = useState>({}); const controls = useListControls(trains, { searchKeys: ['trainNumber', 'route', 'origin', 'destination', 'status'], dateKey: 'arrivalTime', }); const autoUnload = async (train: ImportTrain) => { const assignments = Object.entries(assignmentsBySchedule[train.scheduleId] ?? {}) .filter((entry): entry is [string, Required] => Boolean(entry[1].warehouseId && entry[1].yardId && entry[1].zoneId), ) .map(([bookingId, draft]) => ({ bookingId, warehouseId: draft.warehouseId, yardId: draft.yardId, zoneId: draft.zoneId, })); if (!readyBySchedule[train.scheduleId] || assignments.length === 0) { toast({ variant: 'destructive', title: 'Assign locations', description: 'Select warehouse, yard and zone for each pending booking before unloading.', }); return; } if (isFullyUnloaded(train)) { toast({ title: 'Already unloaded', description: `${train.trainNumber ?? 'This train'} has no remaining bookings to auto unload.`, }); return; } setBusyId(train.scheduleId); try { const r = await autoUnloadMutation.mutateAsync({ scheduleId: train.scheduleId, assignments }); const alreadyUnloaded = r.unloadedCount === 0 && r.skippedCount > 0 && r.failedCount === 0; const firstReason = r.results.find((item) => item.reason)?.reason; const extra = [ skippedSummary(r.skippedCount, r.results) ?? '', r.failedCount ? `${r.failedCount} failed` : '', ] .filter(Boolean) .join(', '); toast({ title: alreadyUnloaded ? 'Already unloaded' : `${r.unloadedCount} unloaded`, description: alreadyUnloaded ? firstReason ?? 'This train is already in warehouse inventory.' : extra || undefined, }); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Auto unload failed', description: extractErrorMessage(error) }); } finally { setBusyId(null); } }; return ( {isLoading ? ( ) : trains.length === 0 ? ( No arrived import trains. Trains appear here once their schedule status is ARRIVED. ) : ( {controls.totalCount} arrived import train{controls.totalCount !== 1 ? 's' : ''} Schedule ID Train # Route Origin Destination Arrival Time Bookings Containers Cargoes Status Actions {controls.pagedRows.length === 0 ? ( No trains match the current filters. ) : ( controls.pagedRows.map((t: ImportTrain) => { const isOpen = openId === t.scheduleId; const fullyUnloaded = isFullyUnloaded(t); const unloadedBookings = t.unloadedBookings ?? t.totalBookings - getPendingUnloadBookings(t); return ( {t.scheduleId.slice(0, 8)}… {({ copied, copy }) => ( {copied ? : } )} {t.trainNumber ?? '—'} {t.route ?? '—'} {t.origin ?? '—'} {t.destination ?? '—'} {formatDate(t.arrivalTime)} {t.totalBookings} {t.totalContainers} {t.totalCargoes} {t.status} {Math.max(unloadedBookings, 0)}/{t.totalBookings} unloaded {isOpen && ( setAssignmentsBySchedule((current) => ({ ...current, [t.scheduleId]: { ...(current[t.scheduleId] ?? {}), [bookingId]: draft.warehouseId ? draft : {}, }, })) } onReadyChange={(ready) => setReadyBySchedule((current) => ({ ...current, [t.scheduleId]: ready })) } /> )} ); }))}
)} setConfirmAction(null)} />
); } /** * Import Unloaded Queue (Batch 9): all unloaded import items with the destination-inspection columns. * Multi-select + Mark Selected as Inspected (import passed → READY_FOR_PICKUP), and the per-item * Inspect / Report action stays for damage / images / weight-loss detail. */ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) { const { toast } = useToast(); const qc = useQueryClient(); const { data: rows = [], isLoading } = useQuery( api.warehouses.importUnloadedQueue.queryOptions({ enabled }), ); const inspectMutation = useMutation( api.warehouses.bulkMarkInspected.mutationOptions(), ); const readyMutation = useMutation(api.warehouses.markReadyForPickup.mutationOptions()); const [selected, setSelected] = useState>(new Set()); const [confirmAction, setConfirmAction] = useState(null); const [expandedRow, setExpandedRow] = useState(null); const [inspectId, setInspectId] = useState(null); const [busyId, setBusyId] = useState(null); const [viewItem, setViewItem] = useState(null); const [historyItem, setHistoryItem] = useState(null); const [feeItem, setFeeItem] = useState(null); const [releaseItem, setReleaseItem] = useState(null); const [deliverItem, setDeliverItem] = useState(null); const [containerItemsItem, setContainerItemsItem] = useState(null); const [storeItem, setStoreItem] = useState(null); const [moveItem, setMoveItem] = useState(null); const allSelected = rows.length > 0 && selected.size === rows.length; const someSelected = selected.size > 0 && !allSelected; const selectAll = () => setSelected(new Set(rows.map((r) => r.id))); const unselectAll = () => setSelected(new Set()); const toggleOne = (id: string) => setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); const markInspected = async () => { if (selected.size === 0) { toast({ variant: 'destructive', title: 'Select at least one item' }); return; } try { const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] }); toast({ title: `${r.inspectedCount} marked inspected`, description: skippedSummary(r.skippedCount, r.results), }); setSelected(new Set()); void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); } catch (error) { toast({ variant: 'destructive', title: 'Mark inspected failed', description: extractErrorMessage(error) }); } }; const toInventoryItem = (row: ImportUnloadedItem): WarehouseInventoryItem => ({ id: row.id, bookingId: row.bookingId, quantity: 1, weight: Number(row.weight) || 0, grnNumber: row.grnNumber, status: row.currentStatus, arrivedAt: row.arrivalTime, unloadedAt: row.arrivalTime, inspectionStatus: row.inspectionStatus, releaseDate: row.releaseDate, releaseOrderReference: row.releaseOrderReference, handoverDocumentReference: row.handoverDocumentReference, handoverDocumentDate: row.handoverDocumentDate, deliveredAt: row.deliveredAt, // Carries the saved [Exit Inspection] block so Truck Leaving opens with the // arrival details (plate, driver, tare, gate-in) read-only instead of blank. notes: row.notes, booking: row.bookingId ? { id: row.bookingId, reference: row.bookingReference ?? row.bookingId, tradeDirection: 'IMPORT', lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null, customerTruckPlateNumber: row.customerTruckPlateNumber, customerTruckDriverName: row.customerTruckDriverName, customerTruckType: row.customerTruckType, customerTruckContainerNumber: row.customerTruckContainerNumber, customerTruckAssignedAt: row.customerTruckAssignedAt, } : null, }) as unknown as WarehouseInventoryItem; const runRowAction = async (row: ImportUnloadedItem, label: string, fn: () => Promise) => { setBusyId(row.id); try { await fn(); toast({ title: label }); void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); } catch (error) { toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) }); } finally { setBusyId(null); } }; const openHandoverDocument = async (row: ImportUnloadedItem) => { setBusyId(row.id); const pdfWindow = window.open('', '_blank'); try { const response = await warehouseService.downloadHandoverDocument(row.id); openPdfBlob(response.data, `handover-${row.bookingReference ?? row.id}.pdf`, pdfWindow); void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); } catch (error) { pdfWindow?.close(); toast({ variant: 'destructive', title: 'Handover document failed', description: await extractDownloadErrorMessage(error) }); } finally { setBusyId(null); } }; const openReleaseDocument = async (row: ImportUnloadedItem) => { setBusyId(row.id); const pdfWindow = window.open('', '_blank'); try { const response = await warehouseService.downloadReleaseDocument(row.id); openPdfBlob(response.data, `release-${row.bookingReference ?? row.id}.pdf`, pdfWindow); } catch (error) { pdfWindow?.close(); toast({ variant: 'destructive', title: 'Exit paper failed', description: await extractDownloadErrorMessage(error) }); } finally { setBusyId(null); } }; return ( Selected: {selected.size} / {rows.length} unloaded {isLoading ? ( ) : rows.length === 0 ? ( No unloaded import items. Items appear here after Auto Unload on an arrived train. ) : ( (allSelected ? unselectAll() : selectAll())} /> Booking Ref GRN Customer Name Arrival Time Container # Cargo Type Weight Train Schedule Inspection Pickup Option Last Mile Current Status Actions {rows.map((r: ImportUnloadedItem) => ( setExpandedRow(expandedRow === r.id ? null : r.id)} > {expandedRow === r.id ? : } toggleOne(r.id)} /> {r.bookingReference ?? '—'} {r.customerName ?? '—'} {formatDate(r.arrivalTime)} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.trainSchedule ?? '—'} {r.inspectionStatus ?? 'Not inspected'} {r.pickupOption} {r.lastMileRequested ? 'Yes' : 'No'} setContainerItemsItem(toInventoryItem(r))}> {/* Primary stage action stays visible; the rest live under the kebab. */} {/* Stays visible after the first exit — multi-truck bookings weigh each truck in and out until all have left. */} {r.currentStatus === 'READY_FOR_PICKUP' && r.hasAssignedTruck && ( )} {r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && ( )} {r.currentStatus === 'UNLOADED' && ( } onClick={() => setStoreItem(toInventoryItem(r))}> Store… )} {r.currentStatus !== 'UNLOADED' && ( } onClick={() => setMoveItem(toInventoryItem(r))}> Move… )} {['UNLOADED', 'STORED'].includes(r.currentStatus) && r.inspectionStatus === 'PASSED' && ( runRowAction(r, 'Ready for pickup', () => readyMutation.mutateAsync(r.id))}> Ready for pickup )} {r.currentStatus === 'READY_FOR_PICKUP' && ( } disabled={!r.hasAssignedTruck} onClick={() => setReleaseItem(toInventoryItem(r))} > {r.hasAssignedTruck ? r.releaseOrderReference ? 'Truck arrival / leaving' : 'Truck arrival' : 'Truck arrival — assign a truck first'} )} {r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && ( } onClick={() => openReleaseDocument(r)}> Exit paper )} {r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && ( setDeliverItem(toInventoryItem(r))}>Deliver )} {r.inspectionStatus === 'PASSED' && ( } onClick={() => openHandoverDocument(r)}> {r.handoverDocumentReference ? 'View handover' : 'Handover'} )} setInspectId(r.id)}>Inspection / Report {/* Double handling is decided once the goods are off the wagon (every row here is unloaded) — Yes is what makes the fee rule bill this booking. */} Double handling —{' '} {r.doubleHandling == null ? 'not set' : r.doubleHandling ? 'Yes' : 'No'} : } disabled={!r.bookingId || r.doubleHandling === true} onClick={() => runRowAction(r, 'Double handling: Yes — fee rule applies', () => warehouseService.setDoubleHandling(r.bookingId as string, true), ) } > Yes — apply fee : } disabled={!r.bookingId || r.doubleHandling === false} onClick={() => runRowAction(r, 'Double handling: No', () => warehouseService.setDoubleHandling(r.bookingId as string, false), ) } > No } onClick={() => setFeeItem(toInventoryItem(r))}> Storage / fee preview } onClick={() => setHistoryItem(toInventoryItem(r))}> History {expandedRow === r.id && ( )} ))}
)} setInspectId(null)} inventoryId={inspectId} /> setViewItem(null)} item={viewItem} /> setHistoryItem(null)} item={historyItem} /> setFeeItem(null)} inventoryId={feeItem?.id ?? null} /> setReleaseItem(null)} item={releaseItem} /> setStoreItem(null)} item={storeItem} /> setMoveItem(null)} item={moveItem} /> setDeliverItem(null)} item={deliverItem} /> setContainerItemsItem(null)} bookingId={containerItemsItem?.booking?.id ?? null} bookingReference={containerItemsItem?.booking?.reference ?? null} /> setConfirmAction(null)} />
); } /** * Import Dispatch Queue (Batch 10): inspected import items that are PICKUP_READY (READY_FOR_PICKUP), * awaiting Customer Pickup (release → deliver), Store, or Dispatch. Reuses InventoryWorkbench so all * existing actions + modals stay intact; Last Mile shows only when the booking requested door delivery. * Nothing is stored automatically — Store is an explicit operator action. */ function ImportDispatchQueueTab({ enabled }: { enabled: boolean }) { const { toast } = useToast(); const { data: items = [], isLoading } = useQuery( api.warehouses.listInventory.queryOptions({ input: { filter: enabled ? { status: 'READY_FOR_PICKUP' } : undefined }, }), ); return ( {items.length} pickup-ready item{items.length !== 1 ? 's' : ''} toast({ title: 'Last mile delivery', description: `Door delivery for ${it.booking?.reference ?? it.id} — handled in the next batch.`, }) } /> ); } type WarehouseFlowDirection = 'IMPORT' | 'EXPORT' | 'BOTH'; type ImportWarehouseTab = 'arrive-queue' | 'unloaded-queue' | 'dispatch-queue' | 'locate-booking'; type ExportWarehouseTab = 'receive-queue' | 'received' | 'ready-to-load' | 'loaded' | 'dispatch-queue' | 'locate-booking'; interface WarehouseQueueTab { value: TValue; label: string; icon: React.ReactNode; count?: number; } interface WarehouseFlowWorkbenchProps { direction?: WarehouseFlowDirection; enabled?: boolean; onChanged?: () => void; focusedBookingId?: string; focusedBookingLabel?: string; } function WarehouseStatCard({ icon, label, value, sub, color, }: { icon: React.ReactNode; label: string; value: React.ReactNode; sub: string; color: string; }) { return ( {icon} {value} {label} {sub} ); } function WarehouseQueueTabs({ value, onChange, tabs, }: { value: TValue; onChange: (value: TValue) => void; tabs: WarehouseQueueTab[]; }) { return ( onChange((next as TValue) ?? value)} variant="pills" color="edr-green" keepMounted={false} classNames={{ list: 'ov-tablist', tab: 'ov-tab' }} > {tabs.map((tab) => { const active = value === tab.value; return ( {tab.count} ) : undefined } > {tab.label} ); })} ); } function LocateBookingTab({ enabled }: { enabled: boolean }) { const [draft, setDraft] = useState({}); const [applied, setApplied] = useState({}); const [viewResult, setViewResult] = useState(null); const hasSearch = Boolean( applied.bookingReference || applied.containerNumber || applied.goodsName || applied.cargoType || applied.status, ); const { data: results = [], isFetching } = useInventoryInquiry(applied, enabled && hasSearch); const controls = useListControls(results); const normalizeDraft = (): InventoryInquiryFilter => ({ bookingReference: draft.bookingReference?.trim() || undefined, containerNumber: draft.containerNumber?.trim() || undefined, goodsName: draft.goodsName?.trim() || undefined, cargoType: draft.cargoType?.trim() || undefined, status: draft.status, }); const runSearch = () => setApplied(normalizeDraft()); const reset = () => { setDraft({}); setApplied({}); }; return ( setDraft((filter) => ({ ...filter, bookingReference: e.currentTarget.value || undefined }))} onKeyDown={(e) => { if (e.key === 'Enter') runSearch(); }} w={230} /> setDraft((filter) => ({ ...filter, containerNumber: e.currentTarget.value || undefined }))} onKeyDown={(e) => { if (e.key === 'Enter') runSearch(); }} w={220} /> { const value = e.currentTarget.value || undefined; setDraft((filter) => ({ ...filter, goodsName: value, cargoType: value })); }} onKeyDown={(e) => { if (e.key === 'Enter') runSearch(); }} w={200} />