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

@@ -416,6 +416,8 @@ export const URL_CONSTANTS = {
`/train-scheduling/schedules/${id}/import-djibouti/load-list/document`,
EXPORT_LOAD_LIST_DOCUMENT: (id: string) =>
`/train-scheduling/schedules/${id}/export/load-list/document`,
INTERCITY_MARSHALLING_DOCUMENT: (id: string) =>
`/train-scheduling/schedules/${id}/intercity/marshalling/document`,
CHECKPOINTS: (id: string) =>
`/train-scheduling/schedules/${id}/checkpoints`,
ARRIVE: (id: string) => `/train-scheduling/schedules/${id}/arrive`,

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

View File

@@ -208,10 +208,12 @@ export default function TrainScheduleV2DetailPage() {
const switchGov = useMutation(api.trainScheduling.switchGovernmentBooking.mutationOptions());
const dispatch = useMutation(api.trainScheduling.dispatchSchedule.mutationOptions());
const downloadMarshalling = useMutation({
mutationFn: ({ id, direction }: { id: string; direction?: string | null }) =>
direction === "EXPORT"
? trainSchedulingService.downloadExportLoadListDocument(id)
: trainSchedulingService.downloadImportDjiboutiLoadListDocument(id),
mutationFn: ({ id, direction, variant }: { id: string; direction?: string | null; variant?: "INTERCITY" }) =>
variant === "INTERCITY"
? trainSchedulingService.downloadIntercityMarshallingDocument(id)
: direction === "EXPORT"
? trainSchedulingService.downloadExportLoadListDocument(id)
: trainSchedulingService.downloadImportDjiboutiLoadListDocument(id),
});
useEffect(() => {
@@ -426,14 +428,21 @@ export default function TrainScheduleV2DetailPage() {
title?: string;
successDescription?: string;
errorTitle?: string;
variant?: "INTERCITY";
}) => {
const pdfWindow = window.open("", "_blank");
try {
const blob = await downloadMarshalling.mutateAsync({
id: scheduleId,
direction: schedule.direction,
variant: options?.variant,
});
const prefix = schedule.direction === "EXPORT" ? "export-marshalling" : "import-marshalling";
const prefix =
options?.variant === "INTERCITY"
? "intercity-marshalling"
: schedule.direction === "EXPORT"
? "export-marshalling"
: "import-marshalling";
const filename = `${prefix}-${schedule.trainNumber ?? scheduleId}.pdf`;
const opened = openPdfBlob(blob, filename, pdfWindow);
toast({
@@ -973,6 +982,24 @@ export default function TrainScheduleV2DetailPage() {
Marshalling PDF
</Button>
) : null}
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
<Button
variant="light"
color="edr-green"
radius="lg"
size="sm"
leftSection={<FileText size={16} />}
loading={downloadMarshalling.isPending}
onClick={() =>
void openMarshallingDocument({
title: "Intercity marshalling ready",
variant: "INTERCITY",
})
}
>
Intercity Marshalling
</Button>
) : null}
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
<Button
component={Link}

View File

@@ -624,6 +624,16 @@ export const trainSchedulingService = {
return response.data;
},
downloadIntercityMarshallingDocument: async (
scheduleId: string,
): Promise<Blob> => {
const response = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_MARSHALLING_DOCUMENT(scheduleId),
{ responseType: "blob" },
);
return response.data;
},
getTrack: async (scheduleId: string): Promise<TrainTrackResponse> => {
const response = await client.get<TrainTrackResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId),