feat(train-scheduling): auto-place intercity cargo on wagons freed mid-corridor

Intercity bookings ride the wagons freed by earlier unloads (e.g. import
containers uncoupled at Dire Dawa). loadBooking now auto-allocates a
DOMESTIC booking onto on-train slots whose cargo has all departed —
greedy in consist order by capacity, container numbers copied for the
marshalling tally. Falls back to unallocated load when nothing is free.
This commit is contained in:
Hagernesh
2026-08-02 23:07:32 +00:00
parent 035369af43
commit 6a62ece814
9 changed files with 593 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import {
ArrowLeft,
CalendarClock,
CheckCircle2,
FileText,
Flag,
MapPin,
Navigation,
@@ -40,6 +41,8 @@ import { freightBrand } from "@/theme/freight-brand";
import { useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
import { openPdfBlob } from "@/components/warehouses/pdf";
import { trainSchedulingService } from "@/services/trainScheduling.service";
const parseError = (error: unknown, fallback: string) => {
if (isAxiosError(error)) {
@@ -160,6 +163,31 @@ export default function TrainScheduleTrackPage() {
enabled: Boolean(scheduleId) && trackQuery.data?.status === "DISPATCHED",
}),
);
// Marshalling 2: the current on-board list, reprinted after station work.
const intercityMarshalling = useMutation({
mutationFn: () =>
trainSchedulingService.downloadIntercityMarshallingDocument(scheduleId ?? ""),
});
const openIntercityMarshalling = async () => {
const pdfWindow = window.open("", "_blank");
try {
const blob = await intercityMarshalling.mutateAsync();
const opened = openPdfBlob(blob, `intercity-marshalling-${scheduleId}.pdf`, pdfWindow);
toast({
title: "Intercity marshalling ready",
description: opened
? "The PDF opened in a browser tab for printing or saving."
: "The browser blocked the preview tab, so the PDF was downloaded.",
});
} catch (error) {
pdfWindow?.close();
toast({
title: "Could not open intercity marshalling document",
description: parseError(error, "Please try again"),
variant: "destructive",
});
}
};
const [yardModal, setYardModal] = useState<{
station: TrackStation;
isFinal: boolean;
@@ -249,17 +277,32 @@ export default function TrainScheduleTrackPage() {
return (
<PageContainer>
<Button
component={Link}
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
variant="subtle"
color="gray"
size="compact-sm"
leftSection={<ArrowLeft size={16} />}
w="fit-content"
>
Back to schedule
</Button>
<Group justify="space-between" w="100%">
<Button
component={Link}
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
variant="subtle"
color="gray"
size="compact-sm"
leftSection={<ArrowLeft size={16} />}
w="fit-content"
>
Back to schedule
</Button>
{inTransit || arrived ? (
<Button
variant="light"
color="edr-green"
radius="lg"
size="compact-sm"
leftSection={<FileText size={16} />}
loading={intercityMarshalling.isPending}
onClick={() => void openIntercityMarshalling()}
>
Intercity Marshalling
</Button>
) : null}
</Group>
{/* ── Hero: gradient wash, route + a bold progress ring woven together ── */}
<Paper