import { Fragment, useEffect, useMemo, useState, type MouseEvent } from 'react'; import { ActionIcon, Alert, Badge, Button, Checkbox, Group, Loader, Menu, Modal, NumberInput, ScrollArea, Select, Stack, Table, Tabs, Text, Textarea, TextInput, Tooltip, } from '@mantine/core'; import { ArrowRightLeft, ChevronDown, ChevronRight, ClipboardCheck, Eye, FileText, History, Info, MapPin, MoreHorizontal, PackageCheck, PackageOpen, PackageSearch, Send, Search, Train, Truck, } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 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 { 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 { 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 { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions } from './options'; import { openPdfBlob } from './pdf'; 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; } function GrnDocumentButton({ inventoryId, grnNumber }: { inventoryId: string; grnNumber?: string | null }) { const { toast } = useToast(); const [loading, setLoading] = useState(false); const openDocument = async (event: MouseEvent) => { event.stopPropagation(); if (!grnNumber) { toast({ variant: 'destructive', title: 'GRN document unavailable', description: 'This item has no GRN number yet.' }); return; } setLoading(true); const pdfWindow = window.open('', '_blank'); try { const response = await warehouseService.downloadGrnDocument(inventoryId); const opened = openPdfBlob(response.data, `grn-${grnNumber}.pdf`, pdfWindow); toast({ title: opened ? 'GRN document opened' : 'GRN document downloaded' }); } catch (error) { pdfWindow?.close(); toast({ variant: 'destructive', title: 'GRN document failed', description: await extractDownloadErrorMessage(error) }); } finally { setLoading(false); } }; return ( ); } 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: t.scheduleId, label: `${t.trainNumber ?? t.scheduleId.slice(0, 8)} · ${t.origin ?? '?'} → ${t.destination ?? '?'} · dep ${t.departureTime ? formatDate(t.departureTime) : '—'} · ${t.readyCount} ready`, }))} value={targetScheduleId} onChange={setTargetScheduleId} searchable /> )} {isLoading ? ( ) : rows.length === 0 ? ( No EXPORT items with inspection PASSED waiting to be loaded. ) : ( Booking Ref GRN Customer Name Container # Cargo Type Weight Route Inspection Status {rows.map((r: ReadyToLoadRow) => ( toggleOne(r.id)} /> setExpandedRow(expandedRow === r.id ? null : r.id)} > {expandedRow === r.id ? : } {r.bookingReference ?? '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.inspectionStatus ?? '—'} {expandedRow === r.id && ( )} ))}
)} ); } /** * Export items that are LOADED onto a wagon. Serves both the "Loaded" tab (read-only) * and the "Dispatch Queue" tab (dispatchable=true → selection + Dispatch actions). */ function LoadedExportTab({ enabled, dispatchable, onChanged, }: { enabled: boolean; dispatchable: boolean; onChanged?: () => void; }) { const { toast } = useToast(); const { data: rows = [], isLoading } = useQuery( api.warehouses.loadedExport.queryOptions({ enabled }), ); const [confirmAction, setConfirmAction] = useState(null); const [expandedRow, setExpandedRow] = useState(null); const bulkDispatch = useMutation( api.warehouses.bulkDispatchExport.mutationOptions(), ); const [selected, setSelected] = useState>(new Set()); const allSelected = rows.length > 0 && selected.size === rows.length; const someSelected = selected.size > 0 && !allSelected; const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.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 dispatch = async (inventoryIds: string[]) => { if (inventoryIds.length === 0) { toast({ variant: 'destructive', title: 'Select at least one item' }); return; } try { const r = await bulkDispatch.mutateAsync(inventoryIds); toast({ title: `${r.dispatchedCount} dispatched`, description: skippedSummary(r.skippedCount, r.results), }); setSelected(new Set()); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Dispatch failed', description: extractErrorMessage(error) }); } }; return ( {dispatchable ? ( <> Selected: {selected.size} / {rows.length} loaded ) : ( <> {rows.length} item{rows.length !== 1 ? 's' : ''} loaded )} {dispatchable && ( )} {isLoading ? ( ) : rows.length === 0 ? ( No LOADED export items {dispatchable ? 'waiting to dispatch' : 'yet'}. ) : ( {dispatchable && ( )} Booking Ref GRN Customer Name Container # Cargo Type Weight Route Status {rows.map((r: ReadyToLoadRow) => ( {dispatchable && ( toggleOne(r.id)} /> )} setExpandedRow(expandedRow === r.id ? null : r.id)} > {expandedRow === r.id ? : } {r.bookingReference ?? '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {expandedRow === r.id && ( )} ))}
)} setConfirmAction(null)} />
); } const importLocationTypesForFreight = (freightType: string | null | undefined) => { const normalized = (freightType ?? '').toUpperCase(); if (normalized === 'CONTAINER') { return { yardTypes: ['CONTAINER_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['CONTAINER_ZONE', 'GENERAL_CARGO_ZONE'] }; } return { yardTypes: ['BULK_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['BULK_ZONE', 'GENERAL_CARGO_ZONE'] }; }; const isImportContainerFreight = (freightType: string | null | undefined) => (freightType ?? '').toUpperCase() === 'CONTAINER'; /** * Yard/zone types valid for the freight being received — used to filter the receive * location pickers so the yard list matches the cargo. Container freight → container * yards only; bulk / break-bulk → bulk, general-cargo, hazardous, or cold-storage. * Union across the given freight types; empty input → no restriction (show all). */ const yardZoneTypesForFreights = (freightTypes: Array) => { const yardTypes = new Set(); const zoneTypes = new Set(); for (const freightType of freightTypes) { const normalized = (freightType ?? '').toUpperCase(); if (!normalized) continue; if (normalized === 'CONTAINER') { yardTypes.add('CONTAINER_YARD'); zoneTypes.add('CONTAINER_ZONE'); } else { ['BULK_YARD', 'GENERAL_CARGO_YARD', 'HAZARDOUS_YARD', 'COLD_STORAGE_YARD'].forEach((t) => yardTypes.add(t)); ['BULK_ZONE', 'GENERAL_CARGO_ZONE', 'HAZARDOUS_ZONE', 'COLD_STORAGE_ZONE'].forEach((t) => zoneTypes.add(t)); } } return { yardTypes: [...yardTypes], zoneTypes: [...zoneTypes] }; }; const isImportUnloadPending = (item: ImportTrainItem) => !item.currentStatus || item.currentStatus === 'RECEIVED'; /** Assigned bookings/items for an arrived import train with per-booking unload locations. */ function ImportTrainDetailTable({ train, warehouses, yards, zones, assignments, onAssignmentChange, onReadyChange, }: { train: ImportTrain; warehouses: Warehouse[]; yards: WarehouseYard[]; zones: WarehouseZone[]; assignments: Record; onAssignmentChange: (bookingId: string, draft: ImportUnloadAssignmentDraft) => void; onReadyChange: (ready: boolean) => void; }) { const { data: items = [], isLoading } = useQuery( api.warehouses.importTrainItems.queryOptions({ input: { scheduleId: train.scheduleId }, enabled: Boolean(train.scheduleId), }), ); const warehouseOptions = useMemo( () => warehouses.map((warehouse) => ({ value: warehouse.id, label: `${warehouse.name} (${warehouse.code})` })), [warehouses], ); useEffect(() => { const pending = items.filter(isImportUnloadPending); onReadyChange( pending.length > 0 && pending.every((item) => { const draft = assignments[item.bookingId]; return Boolean(draft?.warehouseId && draft.yardId && draft.zoneId); }), ); }, [assignments, items]); if (isLoading) { return ( ); } if (items.length === 0) { return ( No assigned bookings on this train. ); } return ( Wagon Booking Ref Customer Name Container # Cargo Type Weight Arrival Warehouse Yard Zone Inspection Current Status Last Mile Pickup Option {items.map((it: ImportTrainItem) => { const draft = assignments[it.bookingId] ?? {}; const { yardTypes, zoneTypes } = importLocationTypesForFreight(it.freightType); const yardOptions = yards .filter((yard) => yard.warehouseId === draft.warehouseId && yardTypes.includes(yard.type)) .map((yard) => ({ value: yard.id, label: `${yard.name} (${yard.code})` })); const zoneOptions = zones .filter((zone) => zone.yardId === draft.yardId && zoneTypes.includes(zone.type)) .map((zone) => ({ value: zone.id, label: `${zone.name} (${zone.code})` })); const pending = isImportUnloadPending(it); return ( {it.sequenceNo ? `#${it.sequenceNo}` : '-'} {it.wagonNumber ?? ''} {it.bookingReference ?? '—'} {it.customerName ?? '—'} {it.containerNumber ?? '—'} {it.cargoType ?? '—'} {formatNumber(Number(it.weight))} {formatDate(it.arrivalTime)} onAssignmentChange(it.bookingId, { ...draft, yardId: value ?? undefined, zoneId: undefined }) } searchable disabled={!pending || !draft.warehouseId} w={190} />
); } /** 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); function ImportArriveQueueTab({ enabled, onChanged, }: { enabled: boolean; onChanged?: () => void; }) { const { toast } = useToast(); 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 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 ( {trains.length} arrived import train{trains.length !== 1 ? 's' : ''} {isLoading ? ( ) : trains.length === 0 ? ( No arrived import trains. Trains appear here once their schedule status is ARRIVED. ) : ( Schedule ID Train # Route Origin Destination Arrival Time Bookings Containers Cargoes Status Actions {trains.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)}… {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. */} {r.currentStatus === 'READY_FOR_PICKUP' && !r.releaseDate && 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' && !r.releaseDate && ( } disabled={!r.hasAssignedTruck} onClick={() => setReleaseItem(toInventoryItem(r))} > {r.hasAssignedTruck ? r.releaseOrderReference ? 'Truck 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)}>Inspect / report } 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 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 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} />