import { Fragment, useEffect, useMemo, useState, type MouseEvent } from 'react'; import { ActionIcon, Alert, Badge, Button, Checkbox, Group, Loader, Modal, NumberInput, ScrollArea, Select, Stack, Table, Tabs, Text, Textarea, TextInput, Tooltip, } from '@mantine/core'; import { ChevronDown, ChevronRight, ClipboardCheck, Eye, FileText, History, Info, 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 { useInventoryInquiry } from '@/hooks/useWarehouses'; import { firstMileService } from '@/services/first-mile.service'; import { warehouseService } from '@/services/warehouse.service'; import type { EligibleBooking, InventoryInquiryFilter, InventoryInquiryResult, ImportTrain, ImportTrainItem, ImportUnloadedItem, ReadyToLoadRow, ReceiveInventoryPayload, TruckEntrancePayload, WarehouseInventoryItem, } from '@/types/warehouse'; import { BookingSelect } from './BookingSelect'; import { DeliverInventoryModal } from './DeliverInventoryModal'; 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 { ReleaseOrderModal } from './ReleaseOrderModal'; import { WarehouseInquiryTable } from './WarehouseInquiryTable'; import { extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions } from './options'; import { openPdfBlob } from './pdf'; import '@/components/overview/overview.css'; interface ReceiveInventoryModalProps { opened: boolean; onClose: () => void; /** When supplied the modal locks to a single booking (legacy single-receive). */ bookingId?: string; bookingLabel?: string; 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: extractErrorMessage(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; incoterms: string; hsCodes: string; itemCode: string; itemDescription: string; packagingType: string; unitCount: number | ''; grossWeightKg: number | ''; netWeightKg: number | ''; volumeDimensions: string; conditionAtReceipt: string; damagedRejectedQuantity: number | ''; warehouseCodeLocation: string; 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; assignedEquipmentNumber?: boolean; itemDescription?: boolean; packagingType?: boolean; unitCount?: boolean; grossWeightKg?: boolean; } type PackagingFreightType = 'CONTAINER' | 'BULK' | 'MIXED'; const emptyTruckEntrance = (): TruckEntranceFormState => ({ ownerName: '', consigneeDetails: '', edrDigitalBookingId: '', tin: '', customerPhone: '', truckPlateNumber: '', trailerPlateNumber: '', assignedEquipmentNumber: '', customsSealNumber: '', declarationNumber: '', incoterms: '', hsCodes: '', itemCode: '', itemDescription: '', packagingType: '', unitCount: '', grossWeightKg: '', netWeightKg: '', volumeDimensions: '', conditionAtReceipt: '', damagedRejectedQuantity: '', warehouseCodeLocation: '', driverName: '', driverPhone: '', driverLicenseNumber: '', truckType: '', entranceTareWeightKg: '', exitTareWeightKg: '', driverSignatoryName: '', warehouseManagerName: '', }); const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayload => ({ truckPlateNumber: form.truckPlateNumber.trim(), trailerPlateNumber: form.trailerPlateNumber.trim() || undefined, customsSealNumber: form.customsSealNumber.trim() || undefined, declarationNumber: form.declarationNumber.trim() || undefined, incoterms: form.incoterms.trim() || undefined, hsCodes: form.hsCodes.trim() || undefined, itemCode: form.itemCode.trim() || 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), warehouseCodeLocation: form.warehouseCodeLocation.trim() || undefined, driverName: form.driverName.trim(), driverPhone: form.driverPhone.trim(), driverLicenseNumber: form.driverLicenseNumber.trim() || undefined, truckType: form.truckType.trim() || undefined, entranceTareWeightKg: Number(form.entranceTareWeightKg), exitTareWeightKg: form.exitTareWeightKg === '' ? undefined : Number(form.exitTareWeightKg), driverSignatoryName: form.driverSignatoryName.trim() || undefined, warehouseManagerName: form.warehouseManagerName.trim() || undefined, }); 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.containerNumber)); const itemDescription = commonNonEmptyValue(bookings.map((booking) => booking.cargoDescription ?? booking.cargo)); const packagingType = commonNonEmptyValue(bookings.map((booking) => booking.containerPackagingType)); const edrDigitalBookingId = bookings.length === 1 ? bookings[0]?.reference ?? bookings[0]?.id ?? '' : commonNonEmptyValue(bookings.map((booking) => booking.reference)); const firstMileBooking = bookings.length === 1 ? bookings[0] : null; const unitCount = bookings.length === 1 && bookings[0]?.containerQuantity != null ? Number(bookings[0].containerQuantity) : ''; const grossWeightKg = bookings.length === 1 && bookings[0]?.weight != null ? Number(bookings[0].weight) : ''; 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, itemDescription, packagingType, unitCount, grossWeightKg, truckPlateNumber: firstMileBooking?.firstMileTruckPlateNumber ?? '', trailerPlateNumber: firstMileBooking?.firstMileTrailerPlateNumber ?? '', driverName: firstMileBooking?.firstMileDriverName ?? '', driverPhone: firstMileBooking?.firstMileDriverPhone ?? '', driverLicenseNumber: firstMileBooking?.firstMileDriverLicenseNumber ?? '', truckType: firstMileBooking?.firstMileTruckType ?? '', }, 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: grossWeightKg !== '', }, 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', }: { value: TruckEntranceFormState; onChange: (next: TruckEntranceFormState) => void; lockedFields?: LockedTruckEntranceFields; packagingFreightType?: PackagingFreightType; }) { 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 })} /> onChange({ ...value, entranceTareWeightKg: v === '' ? '' : Number(v) })} /> onChange({ ...value, exitTareWeightKg: v === '' ? '' : Number(v) })} /> Customs and compliance onChange({ ...value, declarationNumber: e.currentTarget.value })} /> onChange({ ...value, incoterms: e.currentTarget.value })} /> onChange({ ...value, hsCodes: e.currentTarget.value })} /> Physical cargo specifications onChange({ ...value, itemCode: e.currentTarget.value })} /> onChange({ ...value, itemDescription: e.currentTarget.value })} /> onChange({ warehouseId: v ?? '', yardId: '', zoneId: '' })} /> onChange({ ...value, zoneId: v ?? '' })} /> ); } /** One tab: eligible PAID bookings for a direction, with bulk receive (+ export load). */ function EligibleTab({ direction, location, enabled, onChanged, }: { direction: 'IMPORT' | 'EXPORT'; location: Location; enabled: boolean; onChanged?: () => void; }) { const { toast } = useToast(); const qc = useQueryClient(); const { data: allRows = [], isLoading } = useQuery( api.warehouses.eligibleBookings.queryOptions({ input: { direction }, enabled, }), ); const rows = useMemo(() => allRows.filter((r) => r.direction === direction), [allRows, direction]); const bulkReceive = useMutation(api.warehouses.bulkReceive.mutationOptions()); const requestFirstMile = useMutation({ mutationFn: (reference: string) => firstMileService.accept(reference), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT }); toast({ title: 'First mile requested', description: 'Booking was added to the existing First Mile workflow.' }); }, onError: (error) => { toast({ variant: 'destructive', title: 'First mile request failed', description: extractErrorMessage(error) }); }, }); const [selected, setSelected] = useState>(new Set()); const [statusTab, setStatusTab] = useState('ALL'); const [truckOpen, setTruckOpen] = useState(false); const [pendingReceiveIds, setPendingReceiveIds] = useState([]); const [receivedAt, setReceivedAt] = useState(null); const [truckForm, setTruckForm] = useState(emptyTruckEntrance()); const [lockedTruckFields, setLockedTruckFields] = useState({}); const [packagingFreightType, setPackagingFreightType] = useState('MIXED'); const locationReady = Boolean(location.warehouseId && location.yardId && location.zoneId); const canReceiveBooking = (row: EligibleBooking) => !(direction === 'EXPORT' && row.hasFirstMile && row.firstMileStatus !== 'RECEIVED_TO_PORT'); const statusOptions = useMemo(() => { const base = [{ value: 'ALL', label: 'All bookings' }]; if (direction === 'EXPORT') { return [ ...base, { value: 'DIRECT', label: 'Direct truck' }, { value: 'FIRST_MILE', label: 'First mile' }, { value: 'FIRST_MILE_READY', label: 'First mile arrived' }, { value: 'AWAITING_FIRST_MILE', label: 'Awaiting first mile' }, ]; } return [ ...base, { value: 'READY_TO_RECEIVE', label: 'Ready to receive' }, { value: 'PAID', label: 'Paid' }, ]; }, [direction]); const statusFilteredRows = useMemo( () => rows.filter((row) => { switch (statusTab) { case 'DIRECT': return !row.hasFirstMile; case 'FIRST_MILE': return row.hasFirstMile; case 'FIRST_MILE_READY': return row.hasFirstMile && row.firstMileStatus === 'RECEIVED_TO_PORT'; case 'AWAITING_FIRST_MILE': return row.hasFirstMile && row.firstMileStatus !== 'RECEIVED_TO_PORT'; case 'READY_TO_RECEIVE': return canReceiveBooking(row); case 'PAID': return row.paymentStatus === 'PAID'; default: return true; } }), [rows, statusTab], ); const statusCounts = useMemo( () => Object.fromEntries( statusOptions.map((option) => [ option.value, rows.filter((row) => { switch (option.value) { case 'DIRECT': return !row.hasFirstMile; case 'FIRST_MILE': return row.hasFirstMile; case 'FIRST_MILE_READY': return row.hasFirstMile && row.firstMileStatus === 'RECEIVED_TO_PORT'; case 'AWAITING_FIRST_MILE': return row.hasFirstMile && row.firstMileStatus !== 'RECEIVED_TO_PORT'; case 'READY_TO_RECEIVE': return canReceiveBooking(row); case 'PAID': return row.paymentStatus === 'PAID'; default: return true; } }).length, ]), ), [rows, statusOptions], ); const selectableRows = statusFilteredRows.filter(canReceiveBooking); const allSelected = selectableRows.length > 0 && selected.size === selectableRows.length; const someSelected = selected.size > 0 && !allSelected; const pendingReceiveRows = useMemo( () => pendingReceiveIds .map((id) => rows.find((item) => item.id === id)) .filter(Boolean) as EligibleBooking[], [pendingReceiveIds, rows], ); const pendingHasFirstMile = pendingReceiveRows.some((row) => row.hasFirstMile); const toggleAll = () => setSelected(allSelected ? new Set() : new Set(selectableRows.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 receiveBookings = async (bookingIds: string[], truckEntrance?: TruckEntrancePayload) => { try { const r = await bulkReceive.mutateAsync({ direction, ...location, bookingIds, ...(truckEntrance ? { truckEntrance } : {}), }); toast({ title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`, description: r.results.find((item) => item.grnNumber)?.grnNumber ?? (r.skippedCount ? `${r.skippedCount} skipped` : undefined), }); setSelected(new Set()); setTruckOpen(false); setPendingReceiveIds([]); setReceivedAt(null); setLockedTruckFields({}); setPackagingFreightType('MIXED'); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Receive failed', description: extractErrorMessage(error) }); } }; const openTruckReceive = (bookingIds: string[]) => { if (!locationReady) { toast({ variant: 'destructive', title: 'Select warehouse, yard and zone first' }); return; } if (bookingIds.length === 0) { toast({ variant: 'destructive', title: 'Select at least one booking' }); return; } const allowedIds = new Set(selectableRows.map((row) => row.id)); const filteredIds = bookingIds.filter((id) => allowedIds.has(id)); if (filteredIds.length === 0) { toast({ variant: 'destructive', title: 'No selected booking is ready to receive' }); return; } const selectedRows = filteredIds .map((id) => rows.find((item) => item.id === id)) .filter(Boolean) as EligibleBooking[]; if (direction === 'IMPORT') { void receiveBookings(filteredIds); return; } const { form, lockedFields, packagingFreightType: nextPackagingFreightType } = truckEntranceFromBookings(selectedRows); const totalContainerQuantity = selectedRows.reduce( (sum, row) => sum + Number(row.containerQuantity ?? 0), 0, ); const normalizedForm = nextPackagingFreightType === 'CONTAINER' && totalContainerQuantity > 0 ? { ...form, unitCount: totalContainerQuantity, } : form; setPendingReceiveIds(filteredIds); setReceivedAt(new Date().toISOString()); setTruckForm(normalizedForm); setLockedTruckFields({ ...lockedFields, unitCount: nextPackagingFreightType === 'CONTAINER' && totalContainerQuantity > 0, }); setPackagingFreightType(nextPackagingFreightType); setTruckOpen(true); }; const receive = async () => { if (!truckForm.truckPlateNumber.trim() || !truckForm.driverName.trim() || !truckForm.driverPhone.trim() || truckForm.entranceTareWeightKg === '') { toast({ variant: 'destructive', title: 'Truck, driver and entrance tare weight are required' }); return; } await receiveBookings(pendingReceiveIds, toTruckEntrancePayload(truckForm)); }; return ( setStatusTab(v ?? 'ALL')}> {statusOptions.map((option) => ( {option.label} ({statusCounts[option.value] ?? 0}) ))} Selected: {selected.size} / {statusFilteredRows.length} eligible {!locationReady && ( } color="orange" variant="light"> Select a warehouse, yard and zone above before receiving. )} {isLoading ? ( ) : statusFilteredRows.length === 0 ? ( No eligible PAID {direction.toLowerCase()} bookings to receive. ) : ( Booking Ref Booking ID Customer ID Customer Name Origin Destination Route Container / Cargo Items Cargo Type Weight Payment Current Status Inspection {direction === 'EXPORT' && First Mile} Actions {statusFilteredRows.map((r) => { const canReceive = canReceiveBooking(r); return ( toggleOne(r.id)} /> {r.reference} {r.id.slice(0, 8)}… {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} {r.customer ?? '—'} {r.origin ?? '—'} {r.destination ?? '—'} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.containerNumber ?? r.cargoDescription ?? r.cargo ?? '—'} {[ r.containerQuantity != null ? `${r.containerQuantity} unit(s)` : null, r.containerPackagingType, ].filter(Boolean).join(' / ') || 'Item details from booking'} {r.cargo ?? '—'} {formatNumber(Number(r.weight))} {r.paymentStatus} {r.status ?? '—'} {direction === 'EXPORT' && ( {r.hasFirstMile ? ( {r.firstMileStatus ?? 'Request needed'} {[r.firstMileTruckPlateNumber, r.firstMileTrailerPlateNumber].filter(Boolean).join(' / ') || 'Truck not assigned'} ) : ( Direct arrival )} )} {direction === 'EXPORT' && r.hasFirstMile && !r.firstMileRequestId ? ( ) : ( )} ); })}
)} setTruckOpen(false)} title={direction === 'EXPORT' ? 'Receive for Loading' : 'Receive to Warehouse'} centered size="lg" > } color={pendingHasFirstMile ? 'green' : 'blue'} variant="light"> {pendingHasFirstMile ? 'Assigned first-mile truck and driver details are prefilled. Confirm or update the actual arrival details before GRN.' : 'Register the customer or third-party truck and driver before export receiving and GRN.'} 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 [inspectId, setInspectId] = useState(null); const pendingRows = rows.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: r.skippedCount ? `${r.skippedCount} skipped` : undefined, }); 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 Booking ID Customer ID Customer Name Container / Cargo Items Cargo Type Weight Route Inspection Status Actions {rows.map((r: ReadyToLoadRow) => { const selectable = r.inspectionStatus !== 'PASSED'; return ( toggleOne(r.id)} /> {r.bookingReference ?? '—'} {r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'} {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.inspectionStatus ?? 'PENDING'} {r.status} ); })}
)} setInspectId(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 { data: rows = [], isLoading } = useQuery( api.warehouses.readyToLoadExport.queryOptions({ enabled }), ); const loadPassed = useMutation(api.warehouses.loadPassedExport.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 autoLoad = async () => { try { const r = await loadPassed.mutateAsync(undefined); toast({ title: `${r.loadedCount} items loaded`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, }); setSelected(new Set()); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) }); } }; return ( {rows.length} item{rows.length !== 1 ? 's' : ''} ready to load {isLoading ? ( ) : rows.length === 0 ? ( No EXPORT items with inspection PASSED waiting to be loaded. ) : ( Booking Ref GRN Booking ID Customer ID Customer Name Container # Cargo Type Weight Route Inspection Status {rows.map((r: ReadyToLoadRow) => ( toggleOne(r.id)} /> {r.bookingReference ?? '—'} {r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'} {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.inspectionStatus ?? '—'} {r.status} ))}
)}
); } /** * 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 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: r.skippedCount ? `${r.skippedCount} skipped` : undefined, }); 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 Booking ID Customer ID Customer Name Container # Cargo Type Weight Route Status {dispatchable && Actions} {rows.map((r: ReadyToLoadRow) => ( {dispatchable && ( toggleOne(r.id)} /> )} {r.bookingReference ?? '—'} {r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'} {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} {r.customerName ?? '—'} {r.containerNumber ?? '—'} {r.cargoType ?? '—'} {formatNumber(Number(r.weight))} {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} {r.status} {dispatchable && ( )} ))}
)}
); } /** Assigned bookings/items for an arrived import train (read-only detail view). */ function ImportTrainDetailTable({ train }: { train: ImportTrain }) { const { data: items = [], isLoading } = useQuery( api.warehouses.importTrainItems.queryOptions({ input: { scheduleId: train.scheduleId }, enabled: Boolean(train.scheduleId), }), ); if (isLoading) { return ( ); } if (items.length === 0) { return ( No assigned bookings on this train. ); } return ( Wagon Booking ID Booking Ref Customer ID Customer Name Container # Cargo Type Weight Arrival Inspection Current Status Last Mile Pickup Option {items.map((it: ImportTrainItem) => ( {it.sequenceNo ? `#${it.sequenceNo}` : '-'} {it.wagonNumber ?? ''} {it.bookingId.slice(0, 8)}… {it.bookingReference ?? '—'} {it.customerId ? `${it.customerId.slice(0, 8)}…` : '—'} {it.customerName ?? '—'} {it.containerNumber ?? '—'} {it.cargoType ?? '—'} {formatNumber(Number(it.weight))} {formatDate(it.arrivalTime)} {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); function ImportArriveQueueTab({ enabled, onChanged, }: { enabled: boolean; onChanged?: () => void; }) { const { toast } = useToast(); const { data: trains = [], isLoading } = useQuery( api.warehouses.importArriveQueue.queryOptions({ enabled }), ); const autoUnloadMutation = useMutation( api.warehouses.autoUnloadArrivedBookings.mutationOptions(), ); const [openId, setOpenId] = useState(null); const [busyId, setBusyId] = useState(null); const autoUnload = async (train: ImportTrain) => { 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(train.scheduleId); const alreadyUnloaded = r.unloadedCount === 0 && r.skippedCount > 0 && r.failedCount === 0; const firstReason = r.results.find((item) => item.reason)?.reason; const extra = [ r.skippedCount ? `${r.skippedCount} skipped` : '', 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 && ( )} ); })}
)}
); } /** * 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 storeMutation = useMutation(api.warehouses.store.mutationOptions()); const readyMutation = useMutation(api.warehouses.markReadyForPickup.mutationOptions()); const dispatchMutation = useMutation(api.warehouses.dispatch.mutationOptions()); const [selected, setSelected] = useState>(new Set()); 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 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: r.skippedCount ? `${r.skippedCount} skipped` : undefined, }); 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, booking: row.bookingId ? { id: row.bookingId, reference: row.bookingReference ?? row.bookingId, tradeDirection: 'IMPORT', lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null, } : 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: extractErrorMessage(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: extractErrorMessage(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 ID Booking Ref GRN Customer ID Customer Name Arrival Time Container # Cargo Type Weight Train Schedule Inspection Pickup Option Last Mile Current Status Actions {rows.map((r: ImportUnloadedItem) => ( toggleOne(r.id)} /> {r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'} {r.bookingReference ?? '—'} {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} {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'} {r.currentStatus} setViewItem(toInventoryItem(r))}> {r.currentStatus === 'UNLOADED' && ( )} {['UNLOADED', 'STORED'].includes(r.currentStatus) && r.inspectionStatus === 'PASSED' && ( )} {r.currentStatus === 'READY_FOR_PICKUP' && !r.releaseDate && ( <> )} {r.currentStatus === 'READY_FOR_PICKUP' && ( )} {r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && ( )} {r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && ( )} {r.inspectionStatus === 'PASSED' && ( )} setFeeItem(toInventoryItem(r))}> setHistoryItem(toInventoryItem(r))}> ))}
)} setInspectId(null)} inventoryId={inspectId} /> setViewItem(null)} item={viewItem} /> setHistoryItem(null)} item={historyItem} /> setFeeItem(null)} inventoryId={feeItem?.id ?? null} /> setReleaseItem(null)} item={releaseItem} /> setDeliverItem(null)} item={deliverItem} />
); } /** * 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; } 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} />