|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import { Alert, Button, Group, Modal, MultiSelect, NumberInput, Select, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
|
|
|
|
|
import { Alert, Button, Group, Modal, MultiSelect, NumberInput, SegmentedControl, Select, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
|
|
|
|
|
import { Info, Scale } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
|
|
|
@@ -105,6 +105,7 @@ const parseInspectionNote = (notes: string | null | undefined) => {
|
|
|
|
|
grossWeight: lineNumber(note, 'Gross Weight'),
|
|
|
|
|
netWeight: lineNumber(note, 'Net Weight'),
|
|
|
|
|
gateOutTime: toLocalDateTimeInput(lineValue(note, 'Gate Out Time')),
|
|
|
|
|
weighingSkipped: /^Weighing:\s*SKIPPED/im.test(note ?? ''),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -135,6 +136,8 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
const [containerNumbers, setContainerNumbers] = useState<string[]>(['']);
|
|
|
|
|
const [gateInTime, setGateInTime] = useState('');
|
|
|
|
|
const [tareWeight, setTareWeight] = useState<number | ''>('');
|
|
|
|
|
// Containers may skip the weighbridge (decided at arrival, sticks for exit). Bulk always weighs.
|
|
|
|
|
const [weighTruck, setWeighTruck] = useState<'yes' | 'no'>('yes');
|
|
|
|
|
const [grossWeight, setGrossWeight] = useState<number | ''>('');
|
|
|
|
|
const [netWeight, setNetWeight] = useState<number | ''>('');
|
|
|
|
|
const [gateOutTime, setGateOutTime] = useState('');
|
|
|
|
|
@@ -158,6 +161,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
setContainerNumbers(initialContainerNumbers(item, inspection.containerNumber || prefillContainerNumber || assignedContainerNumber));
|
|
|
|
|
setGateInTime(inspection.gateInTime);
|
|
|
|
|
setTareWeight(inspection.tareWeight);
|
|
|
|
|
setWeighTruck(inspection.weighingSkipped ? 'no' : 'yes');
|
|
|
|
|
setGrossWeight(inspection.grossWeight);
|
|
|
|
|
setNetWeight(item?.weight == null ? inspection.netWeight : Number(item.weight));
|
|
|
|
|
setGateOutTime(inspection.gateOutTime);
|
|
|
|
|
@@ -165,7 +169,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
}, [opened, item, truckPrefill]);
|
|
|
|
|
|
|
|
|
|
const savedInspection = parseInspectionNote(item?.notes);
|
|
|
|
|
const isExitStep = savedInspection.tareWeight !== '';
|
|
|
|
|
const isExitStep = savedInspection.tareWeight !== '' || savedInspection.weighingSkipped;
|
|
|
|
|
const isEntranceLocked = isExitStep;
|
|
|
|
|
const isCustomerAssignedTruck = Boolean(item?.booking?.customerTruckAssignedAt);
|
|
|
|
|
const hasLastMileTruckPrefill = Boolean(truckPrefill?.truckPlateNumber || truckPrefill?.trailerPlateNumber);
|
|
|
|
|
@@ -220,7 +224,9 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
.reduce((sum, n) => sum + (containerWeightByNumber.get(n.toUpperCase()) ?? 0), 0)
|
|
|
|
|
.toFixed(3),
|
|
|
|
|
);
|
|
|
|
|
const useContainerNet = hasContainerWeights && selectedContainerNumbers.length > 0;
|
|
|
|
|
// Skip is only offered for container bookings; bulk always weighs.
|
|
|
|
|
const skipWeighing = hasContainerWeights && weighTruck === 'no';
|
|
|
|
|
const useContainerNet = hasContainerWeights && selectedContainerNumbers.length > 0 && !skipWeighing;
|
|
|
|
|
|
|
|
|
|
const systemNetWeight = useContainerNet
|
|
|
|
|
? selectedCargoWeight
|
|
|
|
|
@@ -230,6 +236,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
const computedNetWeight =
|
|
|
|
|
tareWeight !== '' && grossWeight !== '' ? Number((Number(grossWeight) - Number(tareWeight)).toFixed(3)) : null;
|
|
|
|
|
const weightMismatch =
|
|
|
|
|
!skipWeighing &&
|
|
|
|
|
computedNetWeight != null && systemNetWeight !== '' && Math.abs(Number(systemNetWeight) - computedNetWeight) > 0.001;
|
|
|
|
|
const title = isExitStep ? 'Customer truck leaving and exit weighing' : 'Customer truck arrival weighing';
|
|
|
|
|
|
|
|
|
|
@@ -239,19 +246,25 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
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' });
|
|
|
|
|
if (!gateInTime || (!skipWeighing && tareWeight === '')) {
|
|
|
|
|
toast({
|
|
|
|
|
variant: 'destructive',
|
|
|
|
|
title: skipWeighing ? 'Gate in time is required' : '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' });
|
|
|
|
|
if (isExitStep && (!gateOutTime || (!skipWeighing && grossWeight === ''))) {
|
|
|
|
|
toast({
|
|
|
|
|
variant: 'destructive',
|
|
|
|
|
title: skipWeighing ? 'Gate out time is required' : 'Gate out time and gross weight are required',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isExitStep && hasContainerWeights && selectedContainerNumbers.length === 0) {
|
|
|
|
|
toast({ variant: 'destructive', title: 'Select the containers loaded on this truck' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isExitStep && systemNetWeight === '') {
|
|
|
|
|
if (isExitStep && !skipWeighing && systemNetWeight === '') {
|
|
|
|
|
toast({ variant: 'destructive', title: 'System recorded net weight is missing' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -279,9 +292,10 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
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,
|
|
|
|
|
weighingSkipped: skipWeighing || undefined,
|
|
|
|
|
tareWeight: skipWeighing ? undefined : Number(tareWeight),
|
|
|
|
|
grossWeight: skipWeighing || grossWeight === '' ? undefined : Number(grossWeight),
|
|
|
|
|
netWeight: !skipWeighing && isExitStep && systemNetWeight !== '' ? Number(systemNetWeight) : undefined,
|
|
|
|
|
gateOutTime: isExitStep ? toIsoDateTime(gateOutTime) : undefined,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
@@ -421,9 +435,24 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|
|
|
|
)}
|
|
|
|
|
<TextInput label="Gate in time" type="datetime-local" value={gateInTime} onChange={(e) => setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
|
|
|
|
</Group>
|
|
|
|
|
{hasContainerWeights && (
|
|
|
|
|
<Group gap="md" align="center">
|
|
|
|
|
<Text size="sm" fw={600}>Weigh truck?</Text>
|
|
|
|
|
<SegmentedControl
|
|
|
|
|
size="xs"
|
|
|
|
|
data={[{ value: 'yes', label: 'Yes — weigh' }, { value: 'no', label: 'No — pass' }]}
|
|
|
|
|
value={weighTruck}
|
|
|
|
|
onChange={(v) => setWeighTruck((v as 'yes' | 'no') ?? 'yes')}
|
|
|
|
|
disabled={isEntranceLocked}
|
|
|
|
|
/>
|
|
|
|
|
{skipWeighing && (
|
|
|
|
|
<Text size="xs" c="dimmed">Weighbridge skipped — container passes without tare/gross.</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Group>
|
|
|
|
|
)}
|
|
|
|
|
<Group grow>
|
|
|
|
|
<NumberInput label="Tare weight (t)" required min={0} value={tareWeight} onChange={(v) => setTareWeight(v === '' ? '' : Number(v))} readOnly={isEntranceLocked} />
|
|
|
|
|
<NumberInput label="Gross weight (t)" required={isExitStep} min={0} value={grossWeight} onChange={(v) => setGrossWeight(v === '' ? '' : Number(v))} disabled={!isExitStep} />
|
|
|
|
|
<NumberInput label="Tare weight (t)" required={!skipWeighing} min={0} value={tareWeight} onChange={(v) => setTareWeight(v === '' ? '' : Number(v))} readOnly={isEntranceLocked} disabled={skipWeighing} />
|
|
|
|
|
<NumberInput label="Gross weight (t)" required={isExitStep && !skipWeighing} min={0} value={grossWeight} onChange={(v) => setGrossWeight(v === '' ? '' : Number(v))} disabled={!isExitStep || skipWeighing} />
|
|
|
|
|
<NumberInput
|
|
|
|
|
label={useContainerNet ? 'Selected cargo net (t)' : 'Recorded net weight (system t)'}
|
|
|
|
|
min={0}
|
|
|
|
|
|