import { useEffect, useState } from 'react'; import { Alert, Button, Group, Modal, NumberInput, Select, SimpleGrid, Stack, Text, TextInput } from '@mantine/core'; import { Info, Scale } from 'lucide-react'; import { useMutation } from '@tanstack/react-query'; import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; import { warehouseService } from '@/services/warehouse.service'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { extractErrorMessage } from './options'; import { openPdfBlob } from './pdf'; interface ReleaseOrderModalProps { opened: boolean; onClose: () => void; item: WarehouseInventoryItem | null; } const REGISTERED_FIRST_LAST_MILE_TRUCKS = [ ['03-ET A45843', '43495'], ['03-ET A45866', '43470'], ['03-ET A45853', '43508'], ['03-ET A45849', '43414'], ['03-ET A45845', '43492'], ['03-ET A45820', '43487'], ['03-ET A45842', '43478'], ['03-ET A45841', '43515'], ['03-ET A45832', '43504'], ['03-ET A45856', '43510'], ['03-ET A45865', '43490'], ['03-ET A45855', '43485'], ['03-ET A45840', '43499'], ['03-ET A45867', '43493'], ['03-ET A45833', '43466'], ['03-ET A45858', '43496'], ['03-ET A45819', '43474'], ['03-ET A45834', '43502'], ['03-ET A45868', '43469'], ['03-ET A45831', '43488'], ['03-ET A45828', '43479'], ['03-ET A45850', '43505'], ['03-ET A45823', '43480'], ['03-ET A45838', '43472'], ['03-ET A45854', '43500'], ['03-ET A45839', '43486'], ['03-ET A45861', '43513'], ['03-ET A45830', '43501'], ['03-ET A45826', '43498'], ['03-ET A45836', '43467'], ['03-ET A45822', '43512'], ['03-ET A45821', '43210'], ['03-ET A45837', '43475'], ['03-ET A45860', '43497'], ['03-ET A45863', '43477'], ['03-ET A45825', '43483'], ['03-ET A45829', '43473'], ['03-ET A45824', '43491'], ['03-ET A45857', '43481'], ['03-ET A45851', '43509'], ['03-ET A45827', '43468'], ['03-ET A45859', '43887'], ['03-ET A45846', '43471'], ['03-ET A45847', '43511'], ['03-ET A45852', '43484'], ['03-ET A45844', '43476'], ['03-ET A45835', '43482'], ['03-ET A45864', '43503'], ['03-ET A45848', '43494'], ['03-ET A45862', '43465'], ['03-ET A39105', '41218'], ['03-ET A39098', '41220'], ['03-ET A29900', '41865'], ['03-ET A39097', '41226'], ['03-ET A39104', '41225'], ['03-ET A39103', '41223'], ['03-ET A39106', '41221'], ['03-ET A39107', '41215'], ['03-ET A39094', '41222'], ['03-ET A39099', '41216'], ['03-ET A39092', '41224'], ['03-ET A31801', '41214'], ].map(([powerPlate, trailerPlate], index) => ({ value: powerPlate, label: `${index + 1}. ${powerPlate} / ${trailerPlate}`, trailerPlate, })); const toIsoDateTime = (value: string) => { if (!value) return undefined; const date = new Date(value); return Number.isNaN(date.getTime()) ? undefined : date.toISOString(); }; const toLocalDateTimeInput = (value?: string | null) => { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; const offsetMs = date.getTimezoneOffset() * 60_000; return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16); }; const generateReleaseReference = (item: WarehouseInventoryItem | null) => { const bookingReference = item?.booking?.reference; if (bookingReference) return `REL-${bookingReference.replace(/^BK-?/i, '')}`; if (item?.bookingId) return `REL-${item.bookingId.replace(/-/g, '').slice(0, 8).toUpperCase()}`; return ''; }; const lineValue = (notes: string | null | undefined, label: string) => { const match = notes?.match(new RegExp(`^${label}:\\s*(.+)$`, 'im')); return match?.[1]?.trim() ?? ''; }; const lineNumber = (notes: string | null | undefined, label: string): number | '' => { const value = lineValue(notes, label).replace(/\s*kg$/i, ''); if (!value) return ''; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : ''; }; const splitContainerNumbers = (value: string | null | undefined) => (value ?? '') .split(/[,;\n]+/) .map((number) => number.trim()) .filter(Boolean); const getItemContainerNumber = (item: WarehouseInventoryItem | null) => (item as (WarehouseInventoryItem & { containerNumber?: string | null }) | null)?.containerNumber ?? ''; const isContainerInventory = (item: WarehouseInventoryItem | null, containerCount: number) => { const freightType = (item as (WarehouseInventoryItem & { booking?: { freightType?: string | null } | null }) | null) ?.booking?.freightType; return Boolean(item?.containerId || containerCount > 0 || freightType === 'CONTAINER'); }; const initialContainerNumbers = (item: WarehouseInventoryItem | null, savedContainerNumber: string) => { const savedNumbers = splitContainerNumbers(savedContainerNumber); const itemNumbers = splitContainerNumbers(getItemContainerNumber(item)); const sourceNumbers = savedNumbers.length ? savedNumbers : itemNumbers; const quantityCount = isContainerInventory(item, sourceNumbers.length) ? Number(item?.quantity ?? 0) : 0; const expectedCount = Math.max(1, sourceNumbers.length, quantityCount); return Array.from({ length: expectedCount }, (_, index) => sourceNumbers[index] ?? ''); }; const parseInspectionNote = (notes: string | null | undefined) => { const marker = '[Exit Inspection]'; const index = notes?.lastIndexOf(marker) ?? -1; const note = index >= 0 ? notes?.slice(index + marker.length) : notes; return { truckPlateNumber: lineValue(note, 'Truck Plate'), trailerPlateNumber: lineValue(note, 'Trailer Plate'), driverName: lineValue(note, 'Driver'), driverLicense: lineValue(note, 'Driver License'), driverPhone: lineValue(note, 'Driver Phone'), truckType: lineValue(note, 'Truck Type'), containerNumber: lineValue(note, 'Container Number'), gateInTime: toLocalDateTimeInput(lineValue(note, 'Gate In Time')), tareWeight: lineNumber(note, 'Tare Weight'), grossWeight: lineNumber(note, 'Gross Weight'), netWeight: lineNumber(note, 'Net Weight'), gateOutTime: toLocalDateTimeInput(lineValue(note, 'Gate Out Time')), }; }; export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) { const { toast } = useToast(); const releaseMutation = useMutation(api.warehouses.release.mutationOptions()); const [reference, setReference] = useState(''); const [truckPlateNumber, setTruckPlateNumber] = useState(''); const [trailerPlateNumber, setTrailerPlateNumber] = useState(''); const [driverName, setDriverName] = useState(''); const [driverLicense, setDriverLicense] = useState(''); const [driverPhone, setDriverPhone] = useState(''); const [truckType, setTruckType] = useState(''); const [containerNumbers, setContainerNumbers] = useState(['']); const [gateInTime, setGateInTime] = useState(''); const [tareWeight, setTareWeight] = useState(''); const [grossWeight, setGrossWeight] = useState(''); const [netWeight, setNetWeight] = useState(''); const [gateOutTime, setGateOutTime] = useState(''); const [downloading, setDownloading] = useState(false); useEffect(() => { if (opened) { const inspection = parseInspectionNote(item?.notes); setReference(item?.releaseOrderReference ?? generateReleaseReference(item)); setTruckPlateNumber(inspection.truckPlateNumber); setTrailerPlateNumber(inspection.trailerPlateNumber); setDriverName(inspection.driverName); setDriverLicense(inspection.driverLicense); setDriverPhone(inspection.driverPhone); setTruckType(inspection.truckType); setContainerNumbers(initialContainerNumbers(item, inspection.containerNumber)); setGateInTime(inspection.gateInTime); setTareWeight(inspection.tareWeight); setGrossWeight(inspection.grossWeight); setNetWeight(item?.weight == null ? inspection.netWeight : Number(item.weight)); setGateOutTime(inspection.gateOutTime); } }, [opened, item]); const savedInspection = parseInspectionNote(item?.notes); const isExitStep = savedInspection.tareWeight !== ''; const isEntranceLocked = isExitStep; const systemNetWeight = item?.weight == null ? netWeight : Number(item.weight); const computedNetWeight = tareWeight !== '' && grossWeight !== '' ? Number((Number(grossWeight) - Number(tareWeight)).toFixed(3)) : null; const weightMismatch = computedNetWeight != null && systemNetWeight !== '' && Math.abs(Number(systemNetWeight) - computedNetWeight) > 0.001; const title = isExitStep ? 'Customer truck leaving and exit weighing' : 'Customer truck arrival weighing'; const handleSubmit = async () => { if (!item) return; if (!truckPlateNumber.trim() || !driverName.trim()) { toast({ variant: 'destructive', title: 'Truck plate and driver name are required' }); return; } if (!gateInTime || tareWeight === '') { toast({ variant: 'destructive', title: 'Gate in time and tare weight are required' }); return; } if (isExitStep && (!gateOutTime || grossWeight === '')) { toast({ variant: 'destructive', title: 'Gate out time and gross weight are required' }); return; } if (isExitStep && systemNetWeight === '') { toast({ variant: 'destructive', title: 'System recorded net weight is missing' }); return; } if (isExitStep && weightMismatch) { toast({ variant: 'destructive', title: 'Weight mismatch', description: 'Gate clearance is blocked. Reassign the item to warehouse if it cannot exit.', }); return; } const pdfWindow = isExitStep ? window.open('', '_blank') : null; try { const released = await releaseMutation.mutateAsync({ id: item.id, payload: { reference: reference.trim() || undefined, bookingId: item.bookingId ?? undefined, customerId: undefined, truckPlateNumber: truckPlateNumber.trim(), trailerPlateNumber: trailerPlateNumber.trim() || undefined, driverName: driverName.trim(), driverLicense: driverLicense.trim() || undefined, driverPhone: driverPhone.trim() || undefined, truckType: truckType.trim() || undefined, containerNumber: containerNumbers.map((number) => number.trim()).filter(Boolean).join(', ') || undefined, gateInTime: toIsoDateTime(gateInTime), tareWeight: Number(tareWeight), grossWeight: grossWeight === '' ? undefined : Number(grossWeight), netWeight: isExitStep && systemNetWeight !== '' ? Number(systemNetWeight) : undefined, gateOutTime: isExitStep ? toIsoDateTime(gateOutTime) : undefined, }, }); if (!isExitStep) { toast({ title: 'Truck arrival saved', description: `${released.releaseOrderReference ?? reference} is ready for exit weighing.`, }); onClose(); return; } setDownloading(true); const response = await warehouseService.downloadReleaseDocument(item.id); const blob = response.data; const filename = `release-${released.booking?.reference ?? released.bookingId ?? item.id}.pdf`; const opened = openPdfBlob(blob, filename, pdfWindow); toast({ title: 'Release exit paper issued', description: opened ? 'The PDF opened in a browser tab for printing or saving.' : 'The browser blocked the preview tab, so the PDF was downloaded.', }); onClose(); } catch (error) { pdfWindow?.close(); toast({ variant: 'destructive', title: 'Release failed', description: extractErrorMessage(error) }); } finally { setDownloading(false); } }; return ( } color="orange" variant="light"> {isExitStep ? ( Record the truck leaving time and gross weight. The system recorded net weight is locked, and the exit paper is generated only when it equals gross weight minus tare weight. ) : ( Register the customer truck and driver at arrival, then save gate in time and tare weight. Reopen this form when the truck is leaving to complete the exit weighing. )} setReference(e.currentTarget.value)} readOnly={isEntranceLocked} />