Refactor wagon type handling and container wagon calculations

- Removed maxWagonsPerTrain from WagonType entity and related DTOs.
- Updated containerWagonsForLines function to calculate required wagons based on container lines more accurately.
- Added unit tests for containerWagonsForLines to ensure correct calculations.
- Adjusted related services and scripts to reflect the removal of maxWagonsPerTrain.
- Enhanced booking and contract components to use new status labels for better user experience.
- Implemented validation for unique container numbers in shipment forms.
This commit is contained in:
Marshal
2026-07-09 03:36:35 +00:00
parent addee34d5d
commit ddddcfb71f
39 changed files with 753 additions and 121 deletions

View File

@@ -89,18 +89,38 @@ export function containersPerWagonFromType(wagonsPerUnit: number): number {
return Math.max(1, Math.round(1 / wpu));
}
function lineWagonsRequired(line: {
type ContainerLine = {
quantity?: number | null;
wagonsRequired?: number | null;
containerType?: { wagonsPerUnit?: number | null; sizeFt?: number | null } | null;
}): number {
};
/**
* RAW (un-ceiled) wagon fraction one container line occupies: qty × wagonsPerUnit
* (40ft = 1, 20ft = 0.5). Two 20ft = 1.0, three 20ft = 1.5. Kept fractional so
* the BOOKING total is ceiled once — ceiling per line over-counts a booking that
* splits its 20ft units across several lines (3×20 + 3×20 = 3 wagons, not 4).
*/
function lineWagonsRaw(line: ContainerLine): number {
const qty = Number(line.quantity ?? 0);
if (qty <= 0) return 0;
const wpu = Number(line.containerType?.wagonsPerUnit);
if (Number.isFinite(wpu) && wpu > 0) {
return Math.ceil(qty * wpu);
return qty * wpu;
}
return Math.max(1, Math.ceil(Number(line.wagonsRequired ?? 1)));
// No wagonsPerUnit on the type: fall back to the line's stored fraction, else
// treat the whole line as one wagon.
const stored = Number(line.wagonsRequired);
return Number.isFinite(stored) && stored > 0 ? stored : 1;
}
/**
* Whole wagons a set of container lines needs: ceil the summed RAW fraction so a
* half-full 20ft wagon rounds up ONCE at the booking level. Empty set → 0.
*/
export function containerWagonsForLines(lines: ContainerLine[]): number {
const raw = lines.reduce((sum, line) => sum + lineWagonsRaw(line), 0);
return raw > 0 ? Math.ceil(raw) : 0;
}
/**
@@ -110,15 +130,16 @@ export function buildContainerWagonPlan(
bookings: Booking[],
wagonType: WagonType,
): WagonPlanSlot[] {
// Whole wagons PER BOOKING (ceil each booking's total TEU once — a 20ft unit
// can share a wagon with another 20ft of the SAME booking, never across
// bookings), then sum. Ceiling per line instead would over-count a booking
// that splits its 20ft units across several lines.
const totalSlots = bookings.reduce((sum, booking) => {
const lineSlots = (booking.bookingContainers ?? []).reduce(
(lineSum, line) => lineSum + lineWagonsRequired(line),
0,
);
return sum + Math.max(lineSlots, 1);
const bookingSlots = containerWagonsForLines(booking.bookingContainers ?? []);
return sum + Math.max(bookingSlots, 1);
}, 0);
const slots = Math.max(1, Math.ceil(totalSlots));
const slots = Math.max(1, totalSlots);
const basePlan: WagonPlanSlot[] = Array.from({ length: slots }, (_, index) => ({
sequenceNo: index + 1,
wagonTypeId: wagonType.id,
@@ -424,7 +445,7 @@ export function validateBulkWagonSlotWeights(wagonPlan: WagonPlanSlot[]): string
export function validateTrainLimits(
wagonPlan: WagonPlanSlot[],
wagonType: WagonType,
wagonType: Pick<WagonType, 'lengthMeters'>,
limits?: TrainLimitConfig,
): string[] {
const violations: string[] = [];
@@ -478,7 +499,7 @@ export function validateMixedTrainLimits(
return validateTrainLimits(
wagonPlan,
{ maxWagonsPerTrain } as WagonType,
{ lengthMeters: minWagonLength },
{ ...limits, maxWagonsPerTrain },
);
}