diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts
index 9f839f613..64af1a56f 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts
@@ -1242,6 +1242,44 @@ describe('TrainSchedulingService', () => {
expect(withoutChanges).not.toContain('Consist Changed At This Stop');
});
+ it('shows per-row Departure/Arrival Station — schedule endpoints for a whole-route wagon, its own board/alight yard for a leg slot', () => {
+ const wholeRoute = { ...makeWagon(1, 'W-001', [loadedAllocation]), id: 'slot-1' };
+ const legSlot = {
+ ...makeWagon(2, 'W-LEG', [loadedAllocation]),
+ id: 'slot-leg',
+ boardYardId: 'yard-dire',
+ alightYardId: 'yard-adama',
+ };
+ const schedule = {
+ id: 'schedule-1',
+ trainNumber: '8302',
+ direction: 'EXPORT',
+ originStation: { label: 'DCT/SGTD' },
+ destinationStation: { label: 'GMP (Gelan Multipurpose Port)' },
+ trainSet: { wagons: [wholeRoute, legSlot] },
+ scheduleBookings: [],
+ };
+ const build = (service as never as {
+ buildExportLoadListHtml: (s: unknown, o?: unknown) => string;
+ }).buildExportLoadListHtml.bind(service);
+
+ const html = build(schedule, {
+ yardLabelById: new Map([
+ ['yard-dire', 'Dire Dawa Port'],
+ ['yard-adama', 'Adama'],
+ ]),
+ });
+
+ expect(html).toContain('
Departure Station | ');
+ expect(html).toContain('Arrival Station | ');
+ // Whole-route wagon: schedule's own endpoints.
+ expect(html).toContain('DCT/SGTD | ');
+ expect(html).toContain('GMP (Gelan Multipurpose Port) | ');
+ // Leg slot: its own board/alight yard, not the schedule's endpoints.
+ expect(html).toContain('Dire Dawa Port | ');
+ expect(html).toContain('Adama | ');
+ });
+
it('lists loaded empty containers by number and states they are empty', () => {
const schedule = {
id: 'schedule-1',
diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts
index ccbaa08c7..07d386df3 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts
@@ -3557,17 +3557,19 @@ export class TrainSchedulingService {
// Leg slots couple mid-corridor — this origin document must say where their
// cargo boards instead of listing it as loaded here (see the import list).
- const slotYardLabels = await this.yardLabelsById(
- (schedule.trainSet?.wagons ?? []).map((wagon) => wagon.boardYardId),
+ // Also doubles as the per-row Departure/Arrival Station lookup below.
+ const yardLabelById = await this.yardLabelsById(
+ (schedule.trainSet?.wagons ?? []).flatMap((wagon) => [wagon.boardYardId, wagon.alightYardId]),
);
const pendingBoardYardLabelBySlot = new Map(
(schedule.trainSet?.wagons ?? [])
.filter((wagon) => wagon.boardYardId)
- .map((wagon) => [wagon.id, slotYardLabels.get(wagon.boardYardId!) ?? 'en route']),
+ .map((wagon) => [wagon.id, yardLabelById.get(wagon.boardYardId!) ?? 'en route']),
);
const html = this.buildExportLoadListHtml(schedule, {
pendingBoardYardLabelBySlot,
+ yardLabelById,
emptyContainers: await this.loadedEmptyContainers(scheduleId),
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
});
@@ -3761,6 +3763,12 @@ export class TrainSchedulingService {
where: { trainScheduleId: scheduleId, yardId: stop.yardId },
order: { occurredAt: 'ASC' },
});
+ // Per-row Departure/Arrival Station: a whole-route wagon reads the
+ // schedule's own origin/destination, a leg-slot wagon reads where IT
+ // boards/alights instead.
+ const yardLabelById = await this.yardLabelsById(
+ (schedule.trainSet?.wagons ?? []).flatMap((wagon) => [wagon.boardYardId, wagon.alightYardId]),
+ );
const html = this.buildExportLoadListHtml(schedule, {
title: `Intercity Marshalling Document / Load List (Marshalling ${stopIndex})`,
positionLabel: `At ${stop.yardLabel}`,
@@ -3769,6 +3777,7 @@ export class TrainSchedulingService {
emptyContainers: await this.loadedEmptyContainers(scheduleId),
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
consistChangesAtStop: this.consistChangesAt(schedule, logRows),
+ yardLabelById,
});
// Styled table-aware fallback (marshalling grid) — see importLoadListDocument.
const buffer = await this.pdfDocuments.renderTabularDocument(html, `Marshalling ${stopIndex}`);
@@ -3808,6 +3817,9 @@ export class TrainSchedulingService {
? `After ${last.yard?.label ?? last.yard?.code ?? 'checkpoint'}`
: `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? 'origin'} — no checkpoint recorded`;
const { wagons, unassignedBookings } = this.intercityOnBoardView(schedule);
+ const yardLabelById = await this.yardLabelsById(
+ (schedule.trainSet?.wagons ?? []).flatMap((wagon) => [wagon.boardYardId, wagon.alightYardId]),
+ );
const html = this.buildExportLoadListHtml(schedule, {
title: 'Intercity Marshalling Document / Load List (Marshalling 2)',
positionLabel,
@@ -3815,6 +3827,7 @@ export class TrainSchedulingService {
unassignedBookings,
emptyContainers: await this.loadedEmptyContainers(scheduleId),
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
+ yardLabelById,
});
// Styled table-aware fallback (marshalling grid) — see importLoadListDocument.
const buffer = await this.pdfDocuments.renderTabularDocument(html, 'Intercity marshalling / load list');
@@ -3884,6 +3897,10 @@ export class TrainSchedulingService {
// Slots that couple to the train downstream (slot id → board yard label).
// Their cargo renders as TO LOAD AT and stays out of the loaded tallies.
pendingBoardYardLabelBySlot?: Map;
+ // yardId → label, for the per-row Departure/Arrival Station columns
+ // (falls back to the schedule's own origin/destination when a wagon's
+ // boardYardId/alightYardId is null — i.e. it rides the whole corridor).
+ yardLabelById?: Map;
// Numbered marshalling docs only (see marshallingDocumentAt /
// consistChangesAt) — couples/uncouples/switches logged at THIS stop.
// Origin import/export docs never pass this, so they render no such box.
@@ -3924,15 +3941,28 @@ export class TrainSchedulingService {
empty,
]);
}
+ const originLabel = schedule.originStation?.label ?? schedule.originStation?.code;
+ const destinationLabel = schedule.destinationStation?.label ?? schedule.destinationStation?.code;
const rows = wagons
.flatMap((wagon) => {
+ // Departure/Arrival Station per row: a leg-slot wagon boards/alights
+ // somewhere other than the schedule's own endpoints; a whole-route
+ // wagon just reads origin/destination.
+ const departureLabel = wagon.boardYardId
+ ? (opts?.yardLabelById?.get(wagon.boardYardId) ?? 'en route')
+ : originLabel;
+ const arrivalLabel = wagon.alightYardId
+ ? (opts?.yardLabelById?.get(wagon.alightYardId) ?? 'en route')
+ : destinationLabel;
// Wagon identity is the same on every row the wagon produces, loaded or not.
const wagonCells = `${esc(wagon.sequenceNo)} |
${esc(wagon.physicalWagon?.wagonNumber)} |
${esc(wagon.wagonType?.code ?? wagon.wagonType?.name)} |
${esc(Number(wagon.lengthMeters || 0).toFixed(3))} |
${esc(Number(wagon.wagonType?.tareWeightTons ?? 0).toFixed(2))} |
- ${esc(Number(wagon.capacityTons || 0).toFixed(3))} | `;
+ ${esc(Number(wagon.capacityTons || 0).toFixed(3))} |
+ ${esc(departureLabel)} |
+ ${esc(arrivalLabel)} | `;
const allocations = wagon.allocations ?? [];
// An empty wagon still runs in the consist, so it still gets a line. Staff
// check this document against the physical train — a wagon with no row
@@ -3956,7 +3986,7 @@ export class TrainSchedulingService {
return [
`
${wagonCells}
- | EMPTY — no cargo allocated |
+ EMPTY — no cargo allocated |
`,
];
}
@@ -3984,7 +4014,7 @@ export class TrainSchedulingService {
// they are still physically on the train, so they get rows of their own.
const unassigned = opts?.unassignedBookings ?? [];
const unassignedRows = unassigned.length
- ? `| ON BOARD — WAGON NOT RECORDED |
` +
+ ? `| ON BOARD — WAGON NOT RECORDED |
` +
unassigned
.map((booking) => {
const containerNumbers = (booking.bookingContainers ?? [])
@@ -3993,7 +4023,7 @@ export class TrainSchedulingService {
.join(', ');
const leg = `${booking.originYard?.label ?? booking.originYard?.code ?? '-'} → ${booking.destinationYard?.label ?? booking.destinationYard?.code ?? '-'}`;
return `
- | ${esc(booking.reference)} — ${esc(leg)} |
+ ${esc(booking.reference)} — ${esc(leg)} |
${esc(booking.cargoType?.cargoTypeName ?? booking.cargoType?.code)} |
${esc(booking.company?.name)} |
${esc(containerNumbers)} |
@@ -4134,6 +4164,8 @@ export class TrainSchedulingService {
Equated Length |
Tare Weight |
Load Capacity |
+ Departure Station |
+ Arrival Station |
Cargo Type |
Company |
Container No |
@@ -4142,7 +4174,7 @@ export class TrainSchedulingService {
- ${rows || '| No wagons on this train set. |
'}
+ ${rows || '| No wagons on this train set. |
'}
${unassignedRows}