Merge pull request #1457 from Tria-plc/Wrehousechanges

fix(train-scheduling): drop not-yet-boarded wagons from every marshal…
This commit is contained in:
Hagernesh Tadesse
2026-08-29 12:28:56 +03:00
committed by GitHub
2 changed files with 41 additions and 51 deletions

View File

@@ -1134,7 +1134,7 @@ describe('TrainSchedulingService', () => {
expect(html).toContain('2 (1 empty)');
});
it('marks a leg slot on the import document as TO BE LOADED and keeps it out of the loaded tallies', () => {
it('drops a leg slot entirely from the import document — not part of the departing consist', () => {
const loadList = {
generatedAt: '2026-07-17T08:00:00.000Z',
trainScheduleId: 'schedule-1',
@@ -1176,15 +1176,16 @@ describe('TrainSchedulingService', () => {
buildImportLoadListHtml: (l: unknown) => string;
}).buildImportLoadListHtml(loadList);
expect(html).toContain('TO BE LOADED AT DIRE DAWA PORT');
// Departure station of the leg slot is its board yard, not the origin.
expect(html).toContain('<td>Dire Dawa Port</td>');
// Only the origin-loaded container counts; the leg slot's tallies separately.
// The leg slot (W-ICY, boards later at Dire Dawa) gets no row at all —
// it isn't on the departing consist. Only W-IMP appears.
expect(html).not.toContain('W-ICY');
expect(html).not.toContain('ICY-001');
expect(html).toContain('W-IMP');
expect(html).toContain('<span>Wagons</span><strong>1</strong>');
expect(html).toContain('<span>Total containers</span><strong>1</strong>');
expect(html).toContain('<span>To load en route</span><strong>1 containers</strong>');
});
it('marks a leg slot on the export document as TO LOAD AT its board yard and keeps it out of the tallies', () => {
it('drops a leg slot entirely from the export document — not part of the departing consist', () => {
const sizedAllocation = {
...loadedAllocation,
containerItems: [{ containerNumber: 'CONT-001', containerType: { sizeFt: 20 } }],
@@ -1204,9 +1205,10 @@ describe('TrainSchedulingService', () => {
pendingBoardYardLabelBySlot: new Map([['slot-leg', 'Dire Dawa Port']]),
});
expect(html).toContain('TO LOAD AT DIRE DAWA PORT');
// The leg slot (W-LEG, boards later at Dire Dawa) gets no row at all.
expect(html).not.toContain('W-LEG');
expect(html).toContain('<span>Wagons</span><strong>1</strong>');
expect(html).toContain('<span>Total containers</span><strong>1</strong>');
expect(html).toContain('<span>To load en route</span><strong>1 containers</strong>');
});
it('prints the consist-changes table for this stop, and omits it when there are none', () => {

View File

@@ -3866,10 +3866,15 @@ export class TrainSchedulingService {
const time = (value: unknown) => (value ? new Date(value as string | Date).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) : '-');
const bookingById = new Map((schedule.scheduleBookings ?? []).map((link) => [link.bookingId, link.booking]));
// The document is checked against the physical train, so it has to run in
// consist order — the relation comes back unordered.
const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])].sort(
(a, b) => Number(a.sequenceNo ?? 0) - Number(b.sequenceNo ?? 0),
);
// consist order — the relation comes back unordered. Slots planned to
// couple at a LATER stop (pendingBoardYardLabelBySlot, origin docs only —
// intercity calls never pass it, their wagons list is already on-board
// only) are dropped here, not just tallied around: they are not part of
// the departing consist, so they get no row and no count on this document.
// Their own coupling shows up on THAT stop's own marshalling document.
const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])]
.filter((wagon) => !opts?.pendingBoardYardLabelBySlot?.get(wagon.id))
.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[]>();
@@ -3916,7 +3921,6 @@ export class TrainSchedulingService {
</tr>`,
];
}
const pendingAt = opts?.pendingBoardYardLabelBySlot?.get(wagon.id);
return allocations.map((allocation) => {
const booking = allocation.booking ?? bookingById.get(allocation.bookingId);
const cargoType = (booking as unknown as { cargoType?: { name?: string; code?: string } } | undefined)?.cargoType;
@@ -3928,7 +3932,7 @@ export class TrainSchedulingService {
const chassisNumbers = containerItems.map((item) => item.chassisNumber).filter(Boolean).join(', ');
return `<tr>
${wagonCells}
<td>${pendingAt ? `TO LOAD AT ${esc(pendingAt).toUpperCase()}` : ''}${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)}</td>
<td>${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)}</td>
<td>${esc(companyName)}</td>
<td>${esc(containerNumbers || firstContainer?.containerNumber)}</td>
<td>${esc(chassisNumbers)}</td>
@@ -3965,27 +3969,20 @@ export class TrainSchedulingService {
(wagon.allocations ?? []).length === 0 &&
!emptiesByWagon.get(Number(wagon.sequenceNo))?.length,
).length;
const loadsHere = (wagon: TrainSetWagon) => !opts?.pendingBoardYardLabelBySlot?.get(wagon.id);
const totalWeight = wagons.reduce(
(sum, wagon) =>
sum +
(loadsHere(wagon)
? (wagon.allocations ?? []).reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0)
: 0),
sum + (wagon.allocations ?? []).reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0),
0,
);
// 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.
// Cargo boarding downstream is not on this train yet — it tallies separately.
let count40ft = 0, count20ft = 0, pendingContainers = 0;
// physically on the train, so they count, and are called out on their own
// tile. Cargo boarding downstream never enters this loop — `wagons` above
// already excludes those slots.
let count40ft = 0, count20ft = 0;
wagons.forEach((wagon) => {
(wagon.allocations ?? []).forEach((allocation) => {
(allocation.containerItems ?? []).forEach((item) => {
if (!loadsHere(wagon)) {
pendingContainers++;
return;
}
const size = this.resolveContainerItemSize(item);
if (size === 40) count40ft++;
else if (size === 20) count20ft++;
@@ -4053,7 +4050,6 @@ 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>
${pendingContainers ? `<div class="tile"><span>To load en route</span><strong>${esc(pendingContainers)} containers</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>
@@ -4253,30 +4249,23 @@ export class TrainSchedulingService {
.replace(/'/g, '&#39;');
const date = (value: unknown) => (value ? new Date(value as string | Date).toLocaleString('en-GB') : '-');
const status = loadList.operation.status;
// A leg slot (boardYard set) couples mid-corridor — its cargo is NOT on the
// physical train this Djibouti-side document is checked against, so it must
// stay out of the loaded tallies or the gate count stops matching.
const loadsHere = (wagon: (typeof loadList.wagons)[number]) => !wagon.boardYard;
const totalAllocations = loadList.wagons.reduce((sum, wagon) => sum + wagon.allocations.length, 0);
const totalWeight = loadList.wagons.reduce(
(sum, wagon) =>
sum +
(loadsHere(wagon)
? wagon.allocations.reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0)
: 0),
// A leg slot (boardYard set) couples mid-corridor — it is not part of the
// consist this Djibouti-side document is checked against yet, so it gets
// no row and no count here at all. Its own coupling shows up on THAT
// stop's own marshalling document once it actually happens.
const wagons = loadList.wagons.filter((wagon) => !wagon.boardYard);
const totalAllocations = wagons.reduce((sum, wagon) => sum + wagon.allocations.length, 0);
const totalWeight = wagons.reduce(
(sum, wagon) => sum + wagon.allocations.reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0),
0,
);
const emptyWagons = loadList.wagons.filter((wagon) => wagon.allocations.length === 0).length;
const emptyWagons = wagons.filter((wagon) => wagon.allocations.length === 0).length;
// Container count summary (40ft, 20ft) — loaded at origin vs. en route
let count40ft = 0, count20ft = 0, pendingContainers = 0;
loadList.wagons.forEach((wagon) => {
// Container count summary (40ft, 20ft)
let count40ft = 0, count20ft = 0;
wagons.forEach((wagon) => {
wagon.allocations.forEach((allocation) => {
(allocation.containerItems ?? []).forEach((item) => {
if (!loadsHere(wagon)) {
pendingContainers++;
return;
}
const size = this.resolveContainerItemSize(item);
if (size === 40) count40ft++;
else if (size === 20) count20ft++;
@@ -4284,7 +4273,7 @@ export class TrainSchedulingService {
});
});
const allocationRows = loadList.wagons
const allocationRows = wagons
.flatMap((wagon) => {
const wagonCells = `<td>${esc(wagon.sequenceNo)}</td>
<td>${esc(wagon.wagonNumber)}</td>
@@ -4317,7 +4306,7 @@ export class TrainSchedulingService {
<td>${esc(allocation.loadType)}</td>
<td>${esc(allocation.containerNumbers.length ? allocation.containerNumbers.join(', ') : '-')}</td>
<td>${esc(sealNumbers || '-')}</td>
<td>${wagon.boardYard ? `TO BE LOADED AT ${esc(wagon.boardYard).toUpperCase()}` : ''}</td>
<td></td>
<td class="num">${esc(Number(allocation.allocatedWeightTons || 0).toFixed(3))}</td>
</tr>`;
},
@@ -4384,13 +4373,12 @@ export class TrainSchedulingService {
<div class="tile"><span>Origin</span><strong>${esc(loadList.origin)}</strong></div>
<div class="tile"><span>Destination</span><strong>${esc(loadList.destination)}</strong></div>
<div class="tile"><span>Total bookings</span><strong>${esc(loadList.totalBookings)}</strong></div>
<div class="tile"><span>Wagons</span><strong>${esc(loadList.wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}</strong></div>
<div class="tile"><span>Wagons</span><strong>${esc(wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}</strong></div>
<div class="tile"><span>Allocations</span><strong>${esc(totalAllocations)}</strong></div>
<div class="tile"><span>Total weight</span><strong>${esc(totalWeight.toFixed(3))} T</strong></div>
<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>
${pendingContainers ? `<div class="tile"><span>To load en route</span><strong>${esc(pendingContainers)} containers</strong></div>` : ''}
<div class="tile"><span>Gatepass granted</span><strong>${esc(date(loadList.operation.gatepassGrantedAt))}</strong></div>
</div>