mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(train-scheduling): show loaded empties on the marshalling document
Empty containers loaded onto an export departure carry no booking and no wagon allocation, so their wagons printed as EMPTY — no cargo allocated — staff checking the paper against the consist found boxes the list denied. Those wagons now print the container numbers with cargo type EMPTY CONTAINER, count toward the 40ft/20ft tallies, and get their own summary tile; only genuinely bare wagons keep the empty wording.
This commit is contained in:
@@ -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('<span>Empty containers</span><strong>3</strong>');
|
||||
});
|
||||
|
||||
it('renders wagons in consist order regardless of the order the relation returns', () => {
|
||||
const schedule = {
|
||||
id: 'schedule-1',
|
||||
|
||||
@@ -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<EmptyContainerReturn[]> {
|
||||
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<number, EmptyContainerReturn[]>();
|
||||
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 [
|
||||
`<tr>
|
||||
${wagonCells}
|
||||
<td>EMPTY CONTAINER</td>
|
||||
<td>-</td>
|
||||
<td>${esc(empties.map((empty) => empty.containerNumber).filter(Boolean).join(', '))}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`<tr class="empty">
|
||||
${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 `<!doctype html>
|
||||
<html>
|
||||
@@ -3378,6 +3429,7 @@ export class TrainSchedulingService {
|
||||
<div class="tile"><span>Containers 40ft</span><strong>${esc(count40ft)}</strong></div>
|
||||
<div class="tile"><span>Containers 20ft</span><strong>${esc(count20ft)}</strong></div>
|
||||
<div class="tile"><span>Total containers</span><strong>${esc(count40ft + count20ft)}</strong></div>
|
||||
${emptyContainers.length ? `<div class="tile"><span>Empty containers</span><strong>${esc(emptyContainers.length)}</strong></div>` : ''}
|
||||
<div class="tile"><span>Prepared person</span><strong>${esc(schedule.preparedByUserId)}</strong></div>
|
||||
<div class="tile"><span>Check person</span><strong>${esc(schedule.checkedByUserId)}</strong></div>
|
||||
<div class="tile"><span>Wagons</span><strong>${esc(wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}</strong></div>
|
||||
|
||||
Reference in New Issue
Block a user