Merge pull request #1460 from Tria-plc/Wrehousechanges

feat(train-scheduling): per-row Departure/Arrival Station on marshall…
This commit is contained in:
Hagernesh Tadesse
2026-08-29 13:06:31 +03:00
committed by GitHub
2 changed files with 78 additions and 8 deletions

View File

@@ -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('<th>Departure Station</th>');
expect(html).toContain('<th>Arrival Station</th>');
// Whole-route wagon: schedule's own endpoints.
expect(html).toContain('<td>DCT/SGTD</td>');
expect(html).toContain('<td>GMP (Gelan Multipurpose Port)</td>');
// Leg slot: its own board/alight yard, not the schedule's endpoints.
expect(html).toContain('<td>Dire Dawa Port</td>');
expect(html).toContain('<td>Adama</td>');
});
it('lists loaded empty containers by number and states they are empty', () => {
const schedule = {
id: 'schedule-1',

View File

@@ -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<string, string>;
// 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<string, string>;
// 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 = `<td>${esc(wagon.sequenceNo)}</td>
<td>${esc(wagon.physicalWagon?.wagonNumber)}</td>
<td>${esc(wagon.wagonType?.code ?? wagon.wagonType?.name)}</td>
<td class="num">${esc(Number(wagon.lengthMeters || 0).toFixed(3))}</td>
<td class="num">${esc(Number(wagon.wagonType?.tareWeightTons ?? 0).toFixed(2))}</td>
<td class="num">${esc(Number(wagon.capacityTons || 0).toFixed(3))}</td>`;
<td class="num">${esc(Number(wagon.capacityTons || 0).toFixed(3))}</td>
<td>${esc(departureLabel)}</td>
<td>${esc(arrivalLabel)}</td>`;
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 [
`<tr class="empty">
${wagonCells}
<td colspan="4">EMPTY — no cargo allocated</td>
<td colspan="5">EMPTY — no cargo allocated</td>
</tr>`,
];
}
@@ -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
? `<tr class="empty"><td colspan="11">ON BOARD — WAGON NOT RECORDED</td></tr>` +
? `<tr class="empty"><td colspan="13">ON BOARD — WAGON NOT RECORDED</td></tr>` +
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 `<tr>
<td colspan="6">${esc(booking.reference)}${esc(leg)}</td>
<td colspan="8">${esc(booking.reference)}${esc(leg)}</td>
<td>${esc(booking.cargoType?.cargoTypeName ?? booking.cargoType?.code)}</td>
<td>${esc(booking.company?.name)}</td>
<td>${esc(containerNumbers)}</td>
@@ -4134,6 +4164,8 @@ export class TrainSchedulingService {
<th class="num">Equated Length</th>
<th class="num">Tare Weight</th>
<th class="num">Load Capacity</th>
<th>Departure Station</th>
<th>Arrival Station</th>
<th>Cargo Type</th>
<th>Company</th>
<th>Container No</th>
@@ -4142,7 +4174,7 @@ export class TrainSchedulingService {
</tr>
</thead>
<tbody>
${rows || '<tr><td colspan="10">No wagons on this train set.</td></tr>'}
${rows || '<tr><td colspan="13">No wagons on this train set.</td></tr>'}
${unassignedRows}
</tbody>
</table>