mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #1052 from Tria-plc/freight_feature/usermanagement
fix issue
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { orderSlotsByWagonSequence } from './train-builder.service';
|
||||
|
||||
describe('orderSlotsByWagonSequence', () => {
|
||||
const slot = (sequenceNo: number, physicalWagonId: string | null) => ({
|
||||
sequenceNo,
|
||||
physicalWagonId,
|
||||
});
|
||||
|
||||
it('reorders pinned slots to the wagons’ new positions, unpinned trail in old order', () => {
|
||||
// Built train reordered to w3, w1, w2. Slots 1..5: three pinned + two empty.
|
||||
const newSeq = new Map([
|
||||
['w3', 1],
|
||||
['w1', 2],
|
||||
['w2', 3],
|
||||
]);
|
||||
const slots = [
|
||||
slot(1, 'w1'),
|
||||
slot(2, 'w2'),
|
||||
slot(3, 'w3'),
|
||||
slot(4, null),
|
||||
slot(5, null),
|
||||
];
|
||||
expect(orderSlotsByWagonSequence(slots, newSeq).map((s) => s.physicalWagonId)).toEqual([
|
||||
'w3',
|
||||
'w1',
|
||||
'w2',
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
// Unpinned keep their old relative order (4 before 5).
|
||||
expect(orderSlotsByWagonSequence(slots, newSeq).map((s) => s.sequenceNo)).toEqual([
|
||||
3, 1, 2, 4, 5,
|
||||
]);
|
||||
});
|
||||
|
||||
it('slots pinned to wagons outside the reorder trail like unpinned ones', () => {
|
||||
const newSeq = new Map([['w2', 1]]);
|
||||
const slots = [slot(1, 'w-foreign'), slot(2, 'w2')];
|
||||
expect(orderSlotsByWagonSequence(slots, newSeq).map((s) => s.physicalWagonId)).toEqual([
|
||||
'w2',
|
||||
'w-foreign',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { combinedLocomotiveLimits } from '../train-scheduling/train-capacity.uti
|
||||
import { ScheduleWagonAdjustmentLog } from '../train-schedules/entities/schedule-wagon-adjustment-log.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
@@ -601,26 +602,6 @@ export class TrainBuilderService {
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/** Batched form of {@link isWagonPinnedToLiveSchedule} for a whole consist. */
|
||||
private async isAnyWagonPinnedToLiveSchedule(
|
||||
manager: EntityManager,
|
||||
wagonIds: string[],
|
||||
): Promise<boolean> {
|
||||
if (!wagonIds.length) return false;
|
||||
const rows: { exists: boolean }[] = await manager.query(
|
||||
`SELECT TRUE AS exists
|
||||
FROM freight.train_set_wagons tsw
|
||||
JOIN freight.train_schedules ts ON ts.train_set_id = tsw.train_set_id
|
||||
WHERE tsw.physical_wagon_id = ANY($1::uuid[])
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
|
||||
AND ts.deleted_at IS NULL
|
||||
AND tsw.deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[wagonIds],
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/** Persist a drag-reorder: `wagonIds` is the full consist in its new order. */
|
||||
async reorderWagons(id: string, dto: ReorderTrainWagonsDto) {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
@@ -633,20 +614,63 @@ export class TrainBuilderService {
|
||||
if (current.size !== incoming.size || [...current].some((wid) => !incoming.has(wid))) {
|
||||
throw new BadRequestException('Reorder must include every wagon of the train exactly once');
|
||||
}
|
||||
// A live schedule (DRAFT/SCHEDULED/DISPATCHED) reads each wagon's slot at
|
||||
// its OWN frozen sequenceNo, never the wagon's live sequenceNumber — so
|
||||
// renumbering here would silently desync that schedule's drawn consist
|
||||
// from the built train's real order (loaded slots keep the old order,
|
||||
// empty ones show the new one). Same guard as remove/maintenance.
|
||||
if (await this.isAnyWagonPinnedToLiveSchedule(manager, [...current])) {
|
||||
// Only a rolling train is frozen. Pre-dispatch (DRAFT/SCHEDULED) reorder
|
||||
// is allowed — the pinned schedules' consists are resequenced below so
|
||||
// they can never desync from the built train's real order.
|
||||
const dispatched: { exists: boolean }[] = await manager.query(
|
||||
`SELECT TRUE AS exists
|
||||
FROM freight.train_set_wagons tsw
|
||||
JOIN freight.train_schedules ts ON ts.train_set_id = tsw.train_set_id
|
||||
WHERE tsw.physical_wagon_id = ANY($1::uuid[])
|
||||
AND ts.status = 'DISPATCHED'
|
||||
AND ts.deleted_at IS NULL
|
||||
AND tsw.deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[[...current]],
|
||||
);
|
||||
if (dispatched.length > 0) {
|
||||
throw new ConflictException(
|
||||
'This train has wagons pinned to an active schedule and cannot be reordered — ' +
|
||||
"it would desync the schedule's consist view from the built train's real order.",
|
||||
'This train is dispatched — wagons cannot be reordered while it is rolling.',
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < dto.wagonIds.length; i++) {
|
||||
await manager.getRepository(Wagon).update(dto.wagonIds[i], { sequenceNumber: i + 1 });
|
||||
}
|
||||
|
||||
// Propagate the new order to every live (DRAFT/SCHEDULED) schedule of
|
||||
// this train: slots pinned to a reordered wagon adopt the wagon's new
|
||||
// position, unpinned slots trail in their old relative order. Allocations
|
||||
// ride the slot row (by id), so cargo stays with its physical wagon.
|
||||
const newSeq = new Map(dto.wagonIds.map((wid, i) => [wid, i + 1]));
|
||||
const sets: { train_set_id: string }[] = await manager.query(
|
||||
`SELECT DISTINCT ts.train_set_id
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.train_sets tset ON tset.id = ts.train_set_id
|
||||
WHERE tset.train_id = $1
|
||||
AND ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED')`,
|
||||
[id],
|
||||
);
|
||||
for (const { train_set_id: trainSetId } of sets) {
|
||||
const slots = await manager.getRepository(TrainSetWagon).find({
|
||||
where: { trainSetId },
|
||||
order: { sequenceNo: 'ASC' },
|
||||
});
|
||||
const sorted = orderSlotsByWagonSequence(slots, newSeq);
|
||||
// (train_set_id, sequence_no) is unique — shift to a temp range first
|
||||
// so the final renumbering can't collide mid-loop.
|
||||
await manager.query(
|
||||
`UPDATE freight.train_set_wagons
|
||||
SET sequence_no = sequence_no + 100000
|
||||
WHERE train_set_id = $1 AND deleted_at IS NULL`,
|
||||
[trainSetId],
|
||||
);
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
await manager
|
||||
.getRepository(TrainSetWagon)
|
||||
.update(sorted[i].id, { sequenceNo: i + 1 });
|
||||
}
|
||||
}
|
||||
});
|
||||
return this.getComposition(id);
|
||||
}
|
||||
@@ -1067,3 +1091,20 @@ export class TrainBuilderService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New consist order for a schedule's slots after a built-train reorder: slots
|
||||
* pinned to a reordered wagon adopt the wagon's new position; unpinned slots
|
||||
* trail behind in their previous relative order.
|
||||
*/
|
||||
export function orderSlotsByWagonSequence<
|
||||
T extends Pick<TrainSetWagon, 'sequenceNo' | 'physicalWagonId'>,
|
||||
>(slots: T[], newSeq: Map<string, number>): T[] {
|
||||
const key = (s: T): number =>
|
||||
(s.physicalWagonId ? newSeq.get(s.physicalWagonId) : undefined) ?? Infinity;
|
||||
return [...slots].sort((a, b) => {
|
||||
const sa = key(a);
|
||||
const sb = key(b);
|
||||
return sa !== sb ? sa - sb : a.sequenceNo - b.sequenceNo;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user