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 10ebaf6ee..43a169058 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 @@ -1132,6 +1132,36 @@ describe('TrainSchedulingService', () => { expect(html).toContain('2 (1 empty)'); }); + it('lists loaded empty containers by number and states they are empty', () => { + const schedule = { + id: 'schedule-1', + trainNumber: '8301', + direction: 'EXPORT', + trainSet: { + wagons: [makeWagon(1, 'W-001', []), makeWagon(2, 'W-002', []), makeWagon(3, 'W-003', [])], + }, + scheduleBookings: [], + }; + + const html = (service as never as { + buildExportLoadListHtml: (s: unknown, o?: unknown) => string; + }).buildExportLoadListHtml(schedule, { + emptyContainers: [ + { containerNumber: 'CMU9876543', containerSize: '40', wagonSequenceNo: 1 }, + { containerNumber: 'TEMU1112223', containerSize: '20', wagonSequenceNo: 2 }, + { containerNumber: 'TEMU4445556', containerSize: '20', wagonSequenceNo: 2 }, + ], + }); + + expect(html).toContain('CMU9876543'); + expect(html).toContain('TEMU1112223, TEMU4445556'); + expect(html.match(/EMPTY CONTAINER/g)).toHaveLength(2); + // Wagon 3 carries nothing at all, so it keeps the bare-wagon wording. + expect(html.match(/EMPTY — no cargo allocated/g)).toHaveLength(1); + expect(html).toContain('3 (1 empty)'); + expect(html).toContain('Empty containers3'); + }); + it('renders wagons in consist order regardless of the order the relation returns', () => { 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 12fb3dec6..780d65305 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 @@ -91,6 +91,7 @@ import { ImportDjiboutiOperation, type ImportDjiboutiDocumentType, } from '../entities/import-djibouti-operation.entity'; +import { EmptyContainerReturn } from '../../import-operations/entities/empty-container-return.entity'; import { ImportDjiboutiActionDto, UploadImportDjiboutiDocumentDto, @@ -3106,6 +3107,7 @@ export class TrainSchedulingService { } const html = this.buildExportLoadListHtml(schedule, { + emptyContainers: await this.loadedEmptyContainers(scheduleId), logoImageUrl: await this.logoSettings.getLogoImageUrl(), }); // Styled table-aware fallback (marshalling grid) — see importLoadListDocument. @@ -3179,6 +3181,7 @@ export class TrainSchedulingService { positionLabel, wagons, unassignedBookings, + emptyContainers: await this.loadedEmptyContainers(scheduleId), logoImageUrl: await this.logoSettings.getLogoImageUrl(), }); // Styled table-aware fallback (marshalling grid) — see importLoadListDocument. @@ -3216,6 +3219,18 @@ export class TrainSchedulingService { return null; } + /** + * Empty containers riding this departure back to Djibouti. They carry no + * booking and no wagon allocation, so the marshalling document would show + * their wagons as bare — staff checking the paper against the train would + * find boxes that the list denies are there. + */ + private loadedEmptyContainers(scheduleId: string): Promise { + return this.dataSource + .getRepository(EmptyContainerReturn) + .find({ where: { trainScheduleId: scheduleId } }); + } + private buildExportLoadListHtml( schedule: TrainSchedule, opts?: { @@ -3223,6 +3238,7 @@ export class TrainSchedulingService { positionLabel?: string; wagons?: TrainSetWagon[]; unassignedBookings?: Booking[]; + emptyContainers?: EmptyContainerReturn[]; logoImageUrl?: string | null; }, ): string { @@ -3241,6 +3257,16 @@ export class TrainSchedulingService { const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])].sort( (a, b) => Number(a.sequenceNo ?? 0) - Number(b.sequenceNo ?? 0), ); + // Empties sit on wagons that carry no booking allocation, keyed by the wagon + // slot recorded when they were loaded. + const emptiesByWagon = new Map(); + for (const empty of opts?.emptyContainers ?? []) { + if (empty.wagonSequenceNo == null) continue; + emptiesByWagon.set(empty.wagonSequenceNo, [ + ...(emptiesByWagon.get(empty.wagonSequenceNo) ?? []), + empty, + ]); + } const rows = wagons .flatMap((wagon) => { // Wagon identity is the same on every row the wagon produces, loaded or not. @@ -3255,6 +3281,21 @@ export class TrainSchedulingService { // check this document against the physical train — a wagon with no row // reads as a wagon that is not there, and the count stops matching. if (allocations.length === 0) { + const empties = emptiesByWagon.get(Number(wagon.sequenceNo)) ?? []; + // Empty boxes returning to Djibouti: numbers listed like any other + // container, state spelled out so nobody reads them as laden. + if (empties.length) { + return [ + ` + ${wagonCells} + EMPTY CONTAINER + - + ${esc(empties.map((empty) => empty.containerNumber).filter(Boolean).join(', '))} + - + - + `, + ]; + } return [ ` ${wagonCells} @@ -3305,14 +3346,19 @@ export class TrainSchedulingService { }) .join('') : ''; - const emptyWagons = wagons.filter((wagon) => (wagon.allocations ?? []).length === 0).length; + const emptyWagons = wagons.filter( + (wagon) => + (wagon.allocations ?? []).length === 0 && + !emptiesByWagon.get(Number(wagon.sequenceNo))?.length, + ).length; const totalWeight = wagons.reduce( (sum, wagon) => sum + (wagon.allocations ?? []).reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0), 0, ); - // Container count summary (40ft, 20ft) + // Container count summary (40ft, 20ft) — empties returning to Djibouti are + // physically on the train, so they count, and are called out on their own tile. let count40ft = 0, count20ft = 0; wagons.forEach((wagon) => { (wagon.allocations ?? []).forEach((allocation) => { @@ -3323,6 +3369,11 @@ export class TrainSchedulingService { }); }); }); + const emptyContainers = [...emptiesByWagon.values()].flat(); + for (const empty of emptyContainers) { + if (empty.containerSize?.includes('20')) count20ft++; + else count40ft++; + } return ` @@ -3378,6 +3429,7 @@ export class TrainSchedulingService {
Containers 40ft${esc(count40ft)}
Containers 20ft${esc(count20ft)}
Total containers${esc(count40ft + count20ft)}
+ ${emptyContainers.length ? `
Empty containers${esc(emptyContainers.length)}
` : ''}
Prepared person${esc(schedule.preparedByUserId)}
Check person${esc(schedule.checkedByUserId)}
Wagons${esc(wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}