mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 14:48:18 +00:00
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:
@@ -187,3 +187,115 @@ describe('applyWagonOrderReversal', () => {
|
||||
expect(plan.map((s) => s.wagonTypeId)).toEqual(['wt-a', 'wt-b', 'wt-c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('planWagonsWithStock — leg-aware stock (intercity ride-along)', () => {
|
||||
const allowed = {
|
||||
byContainerTypeId: new Map([['ct-1', [nw6]]]),
|
||||
byCargoTypeId: new Map(),
|
||||
};
|
||||
// Corridor Gelan(0) → Adama(1) → Doraleh(2): edges 0 and 1.
|
||||
const legs = (entries: Array<[string, { from: number; to: number }]>) =>
|
||||
new Map(entries);
|
||||
|
||||
it('lets an intercity booking ride the empty leg of a train that is full on the other leg', () => {
|
||||
// 1 wagon in stock. Export rides edge 1 only; intercity rides edge 0 only.
|
||||
const result = planWagonsWithStock({
|
||||
bookings: [
|
||||
containerBooking('EXPORT-1', 1, 1),
|
||||
containerBooking('INTERCITY-1', 1, 1),
|
||||
],
|
||||
allowed,
|
||||
stock: {
|
||||
mode: 'TRAIN',
|
||||
remainingByTypeId: new Map([[nw6.id, 1]]),
|
||||
codesByTypeId: new Map([[nw6.id, nw6.code]]),
|
||||
},
|
||||
legs: legs([
|
||||
['EXPORT-1', { from: 1, to: 2 }],
|
||||
['INTERCITY-1', { from: 0, to: 1 }],
|
||||
]),
|
||||
edgeCount: 2,
|
||||
});
|
||||
|
||||
expect(result.deferred).toHaveLength(0);
|
||||
expect(result.fitting.map((b) => b.id).sort()).toEqual([
|
||||
'EXPORT-1',
|
||||
'INTERCITY-1',
|
||||
]);
|
||||
// Two slots planned, but both drawn from the single physical wagon.
|
||||
expect(result.plan).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('still defers when the legs overlap and stock is exhausted', () => {
|
||||
const result = planWagonsWithStock({
|
||||
bookings: [
|
||||
containerBooking('EXPORT-1', 1, 1),
|
||||
containerBooking('INTERCITY-1', 1, 1),
|
||||
],
|
||||
allowed,
|
||||
stock: {
|
||||
mode: 'TRAIN',
|
||||
remainingByTypeId: new Map([[nw6.id, 1]]),
|
||||
codesByTypeId: new Map([[nw6.id, nw6.code]]),
|
||||
},
|
||||
legs: legs([
|
||||
// Both ride edge 0 — they compete for the one wagon.
|
||||
['EXPORT-1', { from: 0, to: 2 }],
|
||||
['INTERCITY-1', { from: 0, to: 1 }],
|
||||
]),
|
||||
edgeCount: 2,
|
||||
});
|
||||
|
||||
expect(result.fitting.map((b) => b.id)).toEqual(['EXPORT-1']);
|
||||
expect(result.deferred).toHaveLength(1);
|
||||
expect(result.deferred[0]!.reference).toBe('INTERCITY-1');
|
||||
expect(result.deferred[0]!.reason).toContain('Train has no free NW6 wagon left');
|
||||
});
|
||||
|
||||
it('never packs bookings with different legs into the same wagon slot', () => {
|
||||
// Two 20ft units with room to share one wagon by TEU — but disjoint legs
|
||||
// must open separate slots (each with its own leg), not one mixed slot.
|
||||
const result = planWagonsWithStock({
|
||||
bookings: [
|
||||
containerBooking('EXPORT-1', 1, 1),
|
||||
containerBooking('INTERCITY-1', 1, 1),
|
||||
],
|
||||
allowed,
|
||||
stock: {
|
||||
mode: 'TRAIN',
|
||||
remainingByTypeId: new Map([[nw6.id, 2]]),
|
||||
codesByTypeId: new Map([[nw6.id, nw6.code]]),
|
||||
},
|
||||
legs: legs([
|
||||
['EXPORT-1', { from: 1, to: 2 }],
|
||||
['INTERCITY-1', { from: 0, to: 1 }],
|
||||
]),
|
||||
edgeCount: 2,
|
||||
});
|
||||
|
||||
expect(result.plan).toHaveLength(2);
|
||||
const bookingsPerSlot = result.plan.map((s) =>
|
||||
[...new Set(s.allocations.map((a) => a.bookingId))].sort(),
|
||||
);
|
||||
expect(bookingsPerSlot).toEqual([['EXPORT-1'], ['INTERCITY-1']]);
|
||||
});
|
||||
|
||||
it('behaves exactly like the whole-route planner when no legs are given', () => {
|
||||
const result = planWagonsWithStock({
|
||||
bookings: [
|
||||
containerBooking('EXPORT-1', 1, 1),
|
||||
containerBooking('INTERCITY-1', 1, 1),
|
||||
],
|
||||
allowed,
|
||||
stock: {
|
||||
mode: 'TRAIN',
|
||||
remainingByTypeId: new Map([[nw6.id, 1]]),
|
||||
codesByTypeId: new Map([[nw6.id, nw6.code]]),
|
||||
},
|
||||
});
|
||||
|
||||
// One wagon, two 20ft bookings: they TEU-share the single slot (legacy).
|
||||
expect(result.deferred).toHaveLength(0);
|
||||
expect(result.plan).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user