Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement

This commit is contained in:
Marshal
2026-07-30 11:31:02 +00:00
215 changed files with 15300 additions and 1450 deletions

View File

@@ -48,10 +48,12 @@ export class FacilityHandlingService {
if (!facility?.hasFacility) return null;
const occurredAt = input.occurredAt ?? new Date();
// Mapped to the goods owner, same as every warehouse-raised GRN.
const grnNumber = generateGrnNumber(
booking.tradeDirection ?? 'DOMESTIC',
booking.id,
occurredAt,
booking.company?.name ?? null,
);
// Link the storage record when this facility keeps cargo — that link is
@@ -67,6 +69,21 @@ export class FacilityHandlingService {
inventoryId = inv?.id ?? null;
}
// The handed-over weight: the booking's declared VGM, else what its
// containers actually carry. A GRN without a weight is not a receipt.
let weightTons = Number(booking.cargoTotalWeightVgm) || null;
if (!weightTons) {
const [sum]: Array<{ tons: string | null }> = await manager.query(
`SELECT SUM(bcu.vgm_tons) AS tons
FROM freight.booking_container_units bcu
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL`,
[booking.id],
);
weightTons = Number(sum?.tons) || null;
}
const repo = manager.getRepository(FacilityHandlingEvent);
await repo.save(
repo.create({
@@ -75,7 +92,7 @@ export class FacilityHandlingService {
trainScheduleId: input.trainScheduleId ?? null,
eventType,
grnNumber,
weightTons: Number(booking.cargoTotalWeightVgm) || null,
weightTons,
inventoryId,
performedBy: input.performedBy ?? null,
occurredAt,

View File

@@ -2784,11 +2784,13 @@ export class TrainSchedulingService {
allocations: (wagon.allocations ?? []).map((allocation) => ({
bookingId: allocation.bookingId,
bookingReference: allocation.booking?.reference ?? null,
booking: allocation.booking,
loadType: allocation.loadType ?? null,
allocatedWeightTons: Number(allocation.allocatedWeightTons) || 0,
containerNumbers: (allocation.containerItems ?? [])
.map((item) => item.containerNumber)
.filter(Boolean),
containerItems: allocation.containerItems ?? [],
})),
})),
operation: await this.getImportDjiboutiOperation(schedule.id),
@@ -2829,6 +2831,32 @@ export class TrainSchedulingService {
};
}
/**
* A container item's size in feet, for the marshalling document's 40ft/20ft
* tally. Two independent sources, since only one is populated depending on
* how the item was created:
* - `item.containerType` — the item's own container_type_id FK, set for
* manually-entered items (no booking-container line behind them).
* - `item.bookingContainer.containerType.sizeFt` / `.containerSize` — the
* booking-line fallback for items generated from an allocation.
* (`findByIdWithFullGraph` must load both relations or every item here
* silently resolves to null and the tally stays zero.)
*/
private resolveContainerItemSize(item: {
containerType?: { sizeFt?: number | null } | null;
bookingContainer?: {
containerSize?: string | null;
containerType?: { sizeFt?: number | null } | null;
} | null;
}): number | null {
const fromSizeFt = item.containerType?.sizeFt ?? item.bookingContainer?.containerType?.sizeFt;
if (fromSizeFt === 20 || fromSizeFt === 40) return fromSizeFt;
const label = item.bookingContainer?.containerSize;
if (label?.includes('40')) return 40;
if (label?.includes('20')) return 20;
return null;
}
private buildExportLoadListHtml(schedule: TrainSchedule): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -2869,6 +2897,7 @@ export class TrainSchedulingService {
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;
const companyName = (booking as unknown as { company?: { name?: string } } | undefined)?.company?.name ?? '-';
const containerItems = allocation.containerItems ?? [];
const firstContainer = containerItems[0];
const containerNumbers = containerItems.map((item) => item.containerNumber).filter(Boolean).join(', ');
@@ -2877,6 +2906,7 @@ export class TrainSchedulingService {
return `<tr>
${wagonCells}
<td>${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)}</td>
<td>${esc(companyName)}</td>
<td>${esc(containerNumbers || firstContainer?.containerNumber)}</td>
<td>${esc(chassisNumbers)}</td>
<td>${esc(sealNumbers)}</td>
@@ -2891,6 +2921,18 @@ export class TrainSchedulingService {
0,
);
// Container count summary (40ft, 20ft)
let count40ft = 0, count20ft = 0;
wagons.forEach((wagon) => {
(wagon.allocations ?? []).forEach((allocation) => {
(allocation.containerItems ?? []).forEach((item) => {
const size = this.resolveContainerItemSize(item);
if (size === 40) count40ft++;
else if (size === 20) count20ft++;
});
});
});
return `<!doctype html>
<html>
<head>
@@ -2940,6 +2982,9 @@ export class TrainSchedulingService {
<div class="tile"><span>Departure station</span><strong>${esc(schedule.originStation?.label ?? schedule.originStation?.code)}</strong></div>
<div class="tile"><span>Arrival station</span><strong>${esc(schedule.destinationStation?.label ?? schedule.destinationStation?.code)}</strong></div>
<div class="tile"><span>Total loaded 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>
<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>
@@ -2958,6 +3003,7 @@ export class TrainSchedulingService {
<th class="num">Tare Weight</th>
<th class="num">Load Capacity</th>
<th>Cargo Type</th>
<th>Company</th>
<th>Container No</th>
<th>Chassis No</th>
<th>Seal No</th>
@@ -3040,6 +3086,19 @@ export class TrainSchedulingService {
0,
);
const emptyWagons = loadList.wagons.filter((wagon) => wagon.allocations.length === 0).length;
// Container count summary (40ft, 20ft)
let count40ft = 0, count20ft = 0;
loadList.wagons.forEach((wagon) => {
wagon.allocations.forEach((allocation) => {
(allocation.containerItems ?? []).forEach((item) => {
const size = this.resolveContainerItemSize(item);
if (size === 40) count40ft++;
else if (size === 20) count20ft++;
});
});
});
const allocationRows = loadList.wagons
.flatMap((wagon) => {
const wagonCells = `<td>${esc(wagon.sequenceNo)}</td>
@@ -3055,13 +3114,17 @@ export class TrainSchedulingService {
];
}
return wagon.allocations.map(
(allocation) => `<tr>
(allocation) => {
const companyName = (allocation.booking as unknown as { company?: { name?: string } } | undefined)?.company?.name ?? '-';
return `<tr>
${wagonCells}
<td>${esc(allocation.bookingReference ?? allocation.bookingId)}</td>
<td>${esc(companyName)}</td>
<td>${esc(allocation.loadType)}</td>
<td>${esc(allocation.containerNumbers.length ? allocation.containerNumbers.join(', ') : '-')}</td>
<td class="num">${esc(Number(allocation.allocatedWeightTons || 0).toFixed(3))}</td>
</tr>`,
</tr>`;
},
);
})
.join('');
@@ -3126,6 +3189,9 @@ export class TrainSchedulingService {
<div class="tile"><span>Wagons</span><strong>${esc(loadList.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>
<div class="tile"><span>Gatepass granted</span><strong>${esc(date(loadList.operation.gatepassGrantedAt))}</strong></div>
</div>
@@ -3145,6 +3211,7 @@ export class TrainSchedulingService {
<th>Seq</th>
<th>Wagon</th>
<th>Booking</th>
<th>Company</th>
<th>Load</th>
<th>Container numbers</th>
<th class="num">Weight T</th>