diff --git a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts index 66a55b555..0ee03a626 100644 --- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts @@ -161,6 +161,8 @@ export const QUERY_KEYS = { ["train-scheduling", "schedules", filters ?? {}] as const, scheduleById: (id: string) => ["train-scheduling", "schedule", id] as const, track: (id: string) => ["train-scheduling", "track", id] as const, + marshallingStops: (id: string) => + ["train-scheduling", "marshalling-stops", id] as const, batchBoard: (filters?: unknown) => ["train-scheduling", "batch-board", "list", filters ?? {}] as const, batchBoardDetail: (scheduleId: string) => diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 16635162a..8603444d2 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -537,6 +537,10 @@ export const URL_CONSTANTS = { `/train-scheduling/schedules/${id}/export/load-list/document`, INTERCITY_MARSHALLING_DOCUMENT: (id: string) => `/train-scheduling/schedules/${id}/intercity/marshalling/document`, + MARSHALLING_STOPS: (id: string) => + `/train-scheduling/schedules/${id}/marshalling/stops`, + MARSHALLING_DOCUMENT_AT: (id: string, stopIndex: number) => + `/train-scheduling/schedules/${id}/marshalling/document/${stopIndex}`, CHECKPOINTS: (id: string) => `/train-scheduling/schedules/${id}/checkpoints`, CHECKPOINT: (id: string, sequenceNo: number) => diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx index 3d74c0fb5..2b1961362 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx @@ -15,7 +15,7 @@ import { Route, TrainFront, } from "lucide-react"; -import { Box, Button, Group, Loader, Stack, Text } from "@mantine/core"; +import { Box, Button, Group, Loader, Menu, Stack, Text } from "@mantine/core"; import { CheckpointTimeModal } from "@/components/trainScheduling/CheckpointTimeModal"; import { CheckpointLogTable } from "@/components/trainScheduling/CheckpointLogTable"; @@ -119,18 +119,34 @@ export default function TrainScheduleTrackPage() { enabled: Boolean(scheduleId) && trackQuery.data?.status === "DISPATCHED", }), ); - // Marshalling 2: the current on-board list, reprinted after station work. + // Marshalling: the on-board list, reprinted after station work. Numbered + // per corridor stop that actually coupled/uncoupled something (Marshalling + // 2, 3, 4…) — falls back to the single "current position" doc when nothing + // has happened yet. + const marshallingStopsQuery = useQuery( + api.trainScheduling.marshallingStops.queryOptions({ + input: { id: scheduleId ?? "" }, + enabled: + Boolean(scheduleId) && + ["DISPATCHED", "ARRIVED"].includes(trackQuery.data?.status ?? ""), + }), + ); + const marshallingStops = marshallingStopsQuery.data ?? []; const intercityMarshalling = useMutation({ - mutationFn: () => - trainSchedulingService.downloadIntercityMarshallingDocument(scheduleId ?? ""), + mutationFn: (stopIndex?: number) => + stopIndex != null + ? trainSchedulingService.downloadMarshallingDocumentAt(scheduleId ?? "", stopIndex) + : trainSchedulingService.downloadIntercityMarshallingDocument(scheduleId ?? ""), }); - const openIntercityMarshalling = async () => { + const openIntercityMarshalling = async (stopIndex?: number) => { const pdfWindow = window.open("", "_blank"); try { - const blob = await intercityMarshalling.mutateAsync(); - const opened = openPdfBlob(blob, `intercity-marshalling-${scheduleId}.pdf`, pdfWindow); + const blob = await intercityMarshalling.mutateAsync(stopIndex); + const filename = + stopIndex != null ? `marshalling-${stopIndex}-${scheduleId}.pdf` : `intercity-marshalling-${scheduleId}.pdf`; + const opened = openPdfBlob(blob, filename, pdfWindow); toast({ - title: "Intercity marshalling ready", + title: stopIndex != null ? `Marshalling ${stopIndex} ready` : "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.", @@ -313,7 +329,7 @@ export default function TrainScheduleTrackPage() { - {inTransit || arrived ? ( + {(inTransit || arrived) && marshallingStops.length === 0 ? ( + + + {marshallingStops.map((stop) => ( + void openIntercityMarshalling(stop.stopIndex)} + > + {`Marshalling ${stop.stopIndex} — ${stop.yardLabel}`} + + ))} + + + ) : null} {/* ── Two-column work surface ── */} diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx index 17fdbb6e4..26d9d7f88 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx @@ -257,13 +257,34 @@ export default function TrainScheduleV2DetailPage() { const switchGov = useMutation(api.trainScheduling.switchGovernmentBooking.mutationOptions()); const dispatch = useMutation(api.trainScheduling.dispatchSchedule.mutationOptions()); const downloadMarshalling = useMutation({ - mutationFn: ({ id, direction, variant }: { id: string; direction?: string | null; variant?: "INTERCITY" }) => - variant === "INTERCITY" - ? trainSchedulingService.downloadIntercityMarshallingDocument(id) - : direction === "EXPORT" - ? trainSchedulingService.downloadExportLoadListDocument(id) - : trainSchedulingService.downloadImportDjiboutiLoadListDocument(id), + mutationFn: ({ + id, + direction, + variant, + stopIndex, + }: { + id: string; + direction?: string | null; + variant?: "INTERCITY"; + stopIndex?: number; + }) => + stopIndex != null + ? trainSchedulingService.downloadMarshallingDocumentAt(id, stopIndex) + : variant === "INTERCITY" + ? trainSchedulingService.downloadIntercityMarshallingDocument(id) + : direction === "EXPORT" + ? trainSchedulingService.downloadExportLoadListDocument(id) + : trainSchedulingService.downloadImportDjiboutiLoadListDocument(id), }); + // Numbered marshalling docs (Marshalling 2, 3, 4…) — one per corridor stop + // that actually coupled/uncoupled something. Empty when nothing has yet. + const marshallingStopsQuery = useQuery( + api.trainScheduling.marshallingStops.queryOptions({ + input: { id: scheduleId ?? "" }, + enabled: Boolean(scheduleId) && ["DISPATCHED", "ARRIVED"].includes(schedule?.status ?? ""), + }), + ); + const marshallingStops = marshallingStopsQuery.data ?? []; useEffect(() => { const operation = gatepassQuery.data; @@ -506,6 +527,7 @@ export default function TrainScheduleV2DetailPage() { successDescription?: string; errorTitle?: string; variant?: "INTERCITY"; + stopIndex?: number; }) => { const pdfWindow = window.open("", "_blank"); try { @@ -513,13 +535,16 @@ export default function TrainScheduleV2DetailPage() { id: scheduleId, direction: schedule.direction, variant: options?.variant, + stopIndex: options?.stopIndex, }); const prefix = - options?.variant === "INTERCITY" - ? "intercity-marshalling" - : schedule.direction === "EXPORT" - ? "export-marshalling" - : "import-marshalling"; + options?.stopIndex != null + ? `marshalling-${options.stopIndex}` + : 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({ @@ -1108,7 +1133,7 @@ export default function TrainScheduleV2DetailPage() { Marshalling PDF ) : null} - {["DISPATCHED", "ARRIVED"].includes(schedule.status) ? ( + {["DISPATCHED", "ARRIVED"].includes(schedule.status) && marshallingStops.length === 0 ? ( } disabled={downloadMarshalling.isPending} @@ -1122,6 +1147,25 @@ export default function TrainScheduleV2DetailPage() { Intercity Marshalling ) : null} + {/* One item per corridor stop that actually coupled/uncoupled + something (Marshalling 2, 3, 4…) — replaces the single + "current position" item once anything has happened. */} + {marshallingStops.map((stop) => ( + } + disabled={downloadMarshalling.isPending} + onClick={() => + void openMarshallingDocument({ + title: `Marshalling ${stop.stopIndex} ready`, + successDescription: `${stop.yardLabel} — coupled/uncoupled wagons included.`, + stopIndex: stop.stopIndex, + }) + } + > + {`Marshalling ${stop.stopIndex} — ${stop.yardLabel}`} + + ))} {["DISPATCHED", "ARRIVED"].includes(schedule.status) ? ( QUERY_KEYS.TRAIN_SCHEDULING.track(id), ), + marshallingStops: endpoint<{ id: string }, MarshallingStop[]>( + "train-scheduling", + "marshalling-stops", + ({ id }) => trainSchedulingService.getMarshallingStops(id), + ({ id }) => QUERY_KEYS.TRAIN_SCHEDULING.marshallingStops(id), + ), + unassignedBookings: endpoint< { scheduleId: string }, UnassignedBookingsResponse diff --git a/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts b/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts index 5ca505cf3..f983baf15 100644 --- a/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts @@ -33,6 +33,7 @@ import type { UpdateCheckpointPayload, DispatchSchedulePayload, StaffBookingWindow, + MarshallingStop, ScheduleMergePreview, TrainScheduleDetail, UpdateScheduleWindowRulePayload, @@ -760,6 +761,24 @@ export const trainSchedulingService = { return response.data; }, + getMarshallingStops: async (scheduleId: string): Promise => { + const response = await client.get( + URL_CONSTANTS.TRAIN_SCHEDULING.MARSHALLING_STOPS(scheduleId), + ); + return unwrap(response.data); + }, + + downloadMarshallingDocumentAt: async ( + scheduleId: string, + stopIndex: number, + ): Promise => { + const response = await client.get( + URL_CONSTANTS.TRAIN_SCHEDULING.MARSHALLING_DOCUMENT_AT(scheduleId, stopIndex), + { responseType: "blob" }, + ); + return response.data; + }, + getTrack: async (scheduleId: string): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId), diff --git a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts index 3af6cc959..3536cddf9 100644 --- a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts @@ -933,6 +933,19 @@ export interface TrainCheckpoint { note: string | null; } +/** + * One corridor stop with a logged consist change (coupled/uncoupled/switched) + * — its own numbered marshalling document exists. `stopIndex` starts at 2; + * origin is always Marshalling 1 (the plain import/export load list, not + * this list). A stop with only a routine checkpoint never appears here. + */ +export interface MarshallingStop { + stopIndex: number; + yardId: string; + yardLabel: string; + firstOccurredAt: string; +} + /** The four station-work stamps, as a payload fragment both endpoints accept. */ export interface CheckpointHandlingTimes { unloadingStartedAt?: string | null;