feat(train-scheduling): implement container movement between wagons

- Added functionality to move containers between wagons in the train scheduling system.
- Introduced  API endpoint and service method to handle container movement.
- Updated  component to support drag-and-drop for rearranging containers.
- Enhanced  to allow moving containers to other wagons via a context menu.
- Implemented UI feedback for container movement actions, including loading states and success/error notifications.
- Updated relevant types and constants to accommodate new container movement logic.
- Added tests for the rule engine to ensure proper handling of hazardous bookings.
This commit is contained in:
Marshal
2026-07-21 23:02:06 +00:00
parent 00a81fda15
commit 835c9e111c
35 changed files with 1896 additions and 154 deletions

View File

@@ -525,6 +525,40 @@ export function validateMixedTrainLimits(
);
}
/**
* Leg-aware limit check: with a real stop list, a slot only counts on the
* edges it actually rides (boardYardId→alightYardId; null = the schedule's
* own endpoint). Each edge is validated as its own consist, so an intercity
* wagon on Gelan→Adama never counts against a train that is full only on
* Adama→Doraleh. Two stops (or fewer) degrade to the whole-train check.
*/
export function validateMixedTrainLimitsPerEdge(
wagonPlan: WagonPlanSlot[],
wagonTypes: Array<Pick<WagonType, 'lengthMeters'>>,
limits: TrainLimitConfig | undefined,
stops: string[],
): string[] {
if (stops.length <= 2) return validateMixedTrainLimits(wagonPlan, wagonTypes, limits);
const lastIdx = stops.length - 1;
const spans = wagonPlan.map((slot) => {
const from = slot.boardYardId ? stops.indexOf(slot.boardYardId) : 0;
const to = slot.alightYardId ? stops.indexOf(slot.alightYardId) : lastIdx;
// A yard missing from the stop list keeps the slot on the whole route.
return { from: from >= 0 ? from : 0, to: to > 0 ? to : lastIdx };
});
const violations = new Set<string>();
for (let edge = 0; edge < lastIdx; edge += 1) {
const active = wagonPlan.filter(
(_, i) => spans[i].from <= edge && edge < spans[i].to,
);
if (!active.length) continue;
for (const violation of validateMixedTrainLimits(active, wagonTypes, limits)) {
violations.add(violation);
}
}
return [...violations];
}
export function validate20ftContainerRules(
units: ContainerUnitRow[],
placements: ContainerPlacementInput[],