mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat(warehouses): auto-load targets a selected train, never loads trainless
"Auto Load Ready Items" now opens a train picker (pre-dispatch trains with these bookings assigned, via the existing loadable-trains flow). No train available -> no auto-loading, with a clear notice. Loading goes through the existing per-wagon load path, so items without an allocated wagon are skipped with a reason. The train association is stored on the existing warehouse_loadings table (no new table needed): new train_schedule_id column + a note recording train number, origin -> destination, and departure time; wagon_id becomes nullable. The trainless load-passed-export endpoint, its frontend wiring, and the unused useLoadPassedExport hook are removed. Migration 2100000000000 (idempotent) also applied to the dev database. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1414,8 +1414,30 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
const { data: rows = [], isLoading } = useQuery(
|
||||
api.warehouses.readyToLoadExport.queryOptions({ enabled }),
|
||||
);
|
||||
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
|
||||
const qc = useQueryClient();
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [trainPickerOpen, setTrainPickerOpen] = useState(false);
|
||||
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
|
||||
// Loading is always onto a SPECIFIC pre-dispatch train. No train -> no auto-load.
|
||||
const { data: trains = [], isLoading: trainsLoading } = useQuery({
|
||||
queryKey: ['warehouse-inventory', 'loadable-trains'],
|
||||
queryFn: () => warehouseService.getLoadableTrains(),
|
||||
enabled: enabled && trainPickerOpen,
|
||||
});
|
||||
const loadOntoTrain = useMutation({
|
||||
mutationFn: async (scheduleId: string) => {
|
||||
const items = await warehouseService.getTrainLoadableItems(scheduleId);
|
||||
const loadableIds = items.filter((i) => i.loadable).map((i) => i.id);
|
||||
if (!loadableIds.length) {
|
||||
throw new Error('No ready items with an allocated wagon on this train');
|
||||
}
|
||||
return warehouseService.loadItemsOntoTrain(scheduleId, loadableIds);
|
||||
},
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
void qc.invalidateQueries({ queryKey: ['warehouse-loadings'] });
|
||||
},
|
||||
});
|
||||
|
||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||
const someSelected = selected.size > 0 && !allSelected;
|
||||
@@ -1427,13 +1449,22 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
return next;
|
||||
});
|
||||
|
||||
const autoLoad = async () => {
|
||||
const confirmLoad = async () => {
|
||||
if (!targetScheduleId) {
|
||||
toast({ variant: 'destructive', title: 'Select a train to load onto' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await loadPassed.mutateAsync(undefined);
|
||||
const r = await loadOntoTrain.mutateAsync(targetScheduleId);
|
||||
const train = trains.find((t) => t.scheduleId === targetScheduleId);
|
||||
toast({
|
||||
title: `${r.loadedCount} items loaded`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
title: `${r.loadedCount} item(s) loaded onto train ${train?.trainNumber ?? ''}`.trim(),
|
||||
description: r.skippedCount
|
||||
? `${r.skippedCount} skipped — ${r.results.find((x) => x.reason)?.reason ?? 'see results'}`
|
||||
: undefined,
|
||||
});
|
||||
setTrainPickerOpen(false);
|
||||
setTargetScheduleId(null);
|
||||
setSelected(new Set());
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
@@ -1452,14 +1483,58 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
variant="filled"
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={loadPassed.isPending}
|
||||
disabled={rows.length === 0}
|
||||
onClick={autoLoad}
|
||||
onClick={() => setTrainPickerOpen(true)}
|
||||
>
|
||||
Auto Load Ready Items
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Modal
|
||||
opened={trainPickerOpen}
|
||||
onClose={() => setTrainPickerOpen(false)}
|
||||
title="Load ready items onto a train"
|
||||
centered
|
||||
size="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{trainsLoading ? (
|
||||
<Group justify="center" py="md"><Loader size="sm" /></Group>
|
||||
) : trains.length === 0 ? (
|
||||
<Alert color="yellow" variant="light" icon={<Info size={16} />}>
|
||||
No train available. Auto-loading needs a scheduled (not yet dispatched) train with
|
||||
these bookings assigned — schedule the train and allocate wagons first.
|
||||
</Alert>
|
||||
) : (
|
||||
<Select
|
||||
label="Available trains"
|
||||
placeholder="Select the train to load onto"
|
||||
data={trains.map((t) => ({
|
||||
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
|
||||
/>
|
||||
)}
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setTrainPickerOpen(false)} disabled={loadOntoTrain.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={loadOntoTrain.isPending}
|
||||
disabled={!targetScheduleId}
|
||||
onClick={confirmLoad}
|
||||
>
|
||||
Load onto this train
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
|
||||
Reference in New Issue
Block a user