ui design for schedule

This commit is contained in:
Marshal
2026-06-09 23:25:52 +00:00
parent 5774d7db9d
commit 257428be18
19 changed files with 2088 additions and 408 deletions

View File

@@ -1201,6 +1201,15 @@ export class TrainSchedulingService {
const lineEntry = lineById.get(placement.bookingContainerId);
if (!lineEntry) continue;
// Durably persist the container number on the booking container line first, so it
// survives a refresh regardless of whether a wagon allocation slot can be matched
// below. booking_container is the source of truth re-read into the preview units.
if (placement.containerNumber && placement.containerNumber.trim()) {
await manager.getRepository(BookingContainer).update(placement.bookingContainerId, {
containerNumber: placement.containerNumber.trim(),
});
}
const allocationId = allocationBySlotBooking.get(
`${placement.sequenceNo}:${lineEntry.bookingId}`,
);
@@ -1226,13 +1235,6 @@ export class TrainSchedulingService {
bookingContainerId: placement.bookingContainerId,
});
}
// Save container number to booking_container when staff enters a new container number
if (placement.containerNumber && placement.containerNumber.trim()) {
await manager.getRepository(BookingContainer).update(placement.bookingContainerId, {
containerNumber: placement.containerNumber.trim(),
});
}
}
if (containerItems.length) {

View File

@@ -53,6 +53,7 @@ export type ContainerUnitRow = {
wagonsPerUnit?: number;
containersPerWagon?: number;
teuSlots?: number;
containerNumber?: string | null;
};
export type ContainerPlacementInput = {
@@ -121,7 +122,7 @@ export function buildContainerWagonPlan(
allocations: [],
}));
return allocateBookingsToSlots(bookings, basePlan, AllocationLoadType.Container).map((slot) => ({
return allocateContainersToSlots(bookings, basePlan).map((slot) => ({
...slot,
slotLoadType: 'CONTAINER' as SlotLoadType,
}));
@@ -223,6 +224,7 @@ export function expandBookingContainerUnits(bookings: Booking[]): ContainerUnitR
wagonsPerUnit,
containersPerWagon: perWagon,
teuSlots,
containerNumber: line.containerNumber ?? null,
});
}
}
@@ -290,6 +292,65 @@ function allocateBookingsToSlots(
});
}
/**
* Allocate container bookings across wagon slots by TEU capacity. A wagon holds at most
* 2 TEU, so it carries either one 40ft container (2 TEU) or two 20ft containers (1 TEU
* each) — a 40ft is NEVER mixed onto the same wagon as a 20ft. Every physical container
* maps to a real wagon allocation, and this mirrors the frontend auto-fill packing
* exactly so a placement's sequenceNo always lands on a slot that holds an allocation
* for its booking.
*
* Weight-based packing (allocateBookingsToSlots) is wrong for containers: it collapses
* several light containers into the first wagons by tonnage and leaves later container
* units without an allocation slot, which silently drops their container items on assign.
*/
function allocateContainersToSlots(
bookings: Booking[],
basePlan: WagonPlanSlot[],
): WagonPlanSlot[] {
const slots = basePlan.map((slot) => ({
...slot,
assignedWeightTons: 0,
allocations: [] as WagonAllocationRecord[],
}));
if (!slots.length) return slots;
const units = expandBookingContainerUnits(bookings);
let currentSlotIndex = 0;
let teuInCurrentSlot = 0;
for (const unit of units) {
const teu = unit.teuSlots ?? teuSlotsForSizeFt(unit.sizeFt ?? 20);
// Move to the next wagon once this one can't fit the container's TEU. This keeps a
// 40ft (2 TEU) alone on its wagon and never pairs it with a 20ft.
if (teuInCurrentSlot > 0 && teuInCurrentSlot + teu > MAX_TEU_SLOTS_PER_WAGON) {
currentSlotIndex += 1;
teuInCurrentSlot = 0;
}
const slot = slots[Math.min(currentSlotIndex, slots.length - 1)]!;
let allocation = slot.allocations.find((a) => a.bookingId === unit.bookingId);
if (!allocation) {
allocation = {
bookingId: unit.bookingId,
bookingReference: unit.bookingReference,
allocatedWeightTons: 0,
loadType: AllocationLoadType.Container,
};
slot.allocations.push(allocation);
}
allocation.allocatedWeightTons = roundTons(
allocation.allocatedWeightTons + unit.grossWeightTons,
);
slot.assignedWeightTons = roundTons(slot.assignedWeightTons + unit.grossWeightTons);
teuInCurrentSlot += teu;
}
return slots;
}
export function expandContainerItems(
booking: Booking,
allocationId: string,