feat(warehouses): gate loading queues on the train's station loading window

Warehouse loading queues and the train schedule page now share one truth:
the schedule's per-yard stationWorkLogs. A booking's items aren't loadable
until "Start loading" has been clicked for their boarding yard, mirroring
the same assertStationWorkStarted check the train schedule page's own Load
button already enforces.

- LoadableTrainRow/LoadableTrain carry originStationId + stationWorkLogs.
- TrainLoadableItem carries originYardId/originYardLabel/loadingWindowStarted,
  computed from station_work_logs in the same query.
- New YardLoadingWindows component surfaces the per-yard windows on the
  loading panel; ReceiveInventoryModal and LoadToTrainPanel wire it in.
- Invalidate loadable-trains/train-loadable-items/warehouse-inventory
  queries alongside train-scheduling ones, since they render off the same
  data.

Fixes the StationWorkLogJson typo that broke the freight-api build.
This commit is contained in:
Hagernesh
2026-09-01 03:01:48 +00:00
parent c7133ba1da
commit 3721ac1ef0
7 changed files with 225 additions and 4 deletions

View File

@@ -90,6 +90,7 @@ import { MoveInventoryModal } from './MoveInventoryModal';
import { ReleaseOrderModal } from './ReleaseOrderModal';
import { StoreInventoryModal } from './StoreInventoryModal';
import { WarehouseInquiryTable } from './WarehouseInquiryTable';
import { YardLoadingWindows } from './YardLoadingWindows';
import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options';
import { openPdfBlob } from './pdf';
import ListControls from '@/components/common/ListControls';
@@ -1598,6 +1599,11 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
queryFn: () => warehouseService.getLoadableTrains(),
enabled: enabled && trainPickerOpen,
});
const { data: pickerItems = [] } = useQuery({
queryKey: ['train-loadable-items', targetScheduleId],
queryFn: () => warehouseService.getTrainLoadableItems(targetScheduleId!),
enabled: Boolean(targetScheduleId) && trainPickerOpen,
});
const loadOntoTrain = useMutation({
mutationFn: async ({ scheduleId, onlyIds }: { scheduleId: string; onlyIds: string[] }) => {
const items = await warehouseService.getTrainLoadableItems(scheduleId);
@@ -1608,6 +1614,17 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
loadableIds = loadableIds.filter((id) => picked.has(id));
}
if (!loadableIds.length) {
const scope = onlyIds.length
? items.filter((i) => onlyIds.includes(i.id))
: items.filter((i) => i.status === 'READY_FOR_LOADING');
// A closed loading window is the blocker staff hit most, and the old
// wagon-only message sent them to fix the wrong thing.
const shut = scope.find((i) => !i.loadingWindowStarted);
if (shut) {
throw new Error(
`Start loading at ${shut.originYardLabel ?? 'the boarding yard'} first — the loading time window has not been started`,
);
}
throw new Error(
onlyIds.length
? 'None of the selected items have an allocated wagon on this train'
@@ -1701,6 +1718,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
searchable
/>
)}
{targetScheduleId ? (
<YardLoadingWindows
scheduleId={targetScheduleId}
items={pickerItems}
logs={trains.find((t) => t.scheduleId === targetScheduleId)?.stationWorkLogs}
/>
) : null}
<Group justify="flex-end">
<Button variant="default" onClick={() => setTrainPickerOpen(false)} disabled={loadOntoTrain.isPending}>
Cancel