issue fix

This commit is contained in:
Marshal
2026-08-19 10:10:04 +00:00
parent 13f66dfb66
commit ad479bdcf2

View File

@@ -577,11 +577,7 @@ export class TrainBuilderService {
if (!wagon || wagon.trainId !== train.id) {
throw new NotFoundException(`Wagon ${wagonId} is not part of this train`);
}
if (await this.isWagonPinnedToLiveSchedule(manager, wagon.id)) {
throw new ConflictException(
`Wagon ${wagon.wagonNumber} is pinned to an active schedule and cannot be removed`,
);
}
await this.assertDetachableAndReleaseStaleSlots(manager, wagon);
await manager.getRepository(Wagon).update(wagon.id, {
trainId: null,
sequenceNumber: null,
@@ -618,11 +614,7 @@ export class TrainBuilderService {
if (!wagon || wagon.trainId !== train.id) {
throw new NotFoundException(`Wagon ${wagonId} is not part of this train`);
}
if (await this.isWagonPinnedToLiveSchedule(manager, wagon.id)) {
throw new ConflictException(
`Wagon ${wagon.wagonNumber} is pinned to an active schedule and cannot be removed`,
);
}
await this.assertDetachableAndReleaseStaleSlots(manager, wagon);
const previousStatus = wagon.status;
const notes = buildMaintenanceNotes(formatTrainRunLabel(train), note);
await manager.getRepository(Wagon).update(wagon.id, {
@@ -705,6 +697,57 @@ export class TrainBuilderService {
return rows.length > 0;
}
/**
* Detach guard for removeWagon / sendWagonToMaintenance. A wagon is truly
* pinned only while a live schedule still NEEDS it: a slot carrying booking
* allocations, or any slot on a DISPATCHED run. An empty (allocation-free)
* slot on a DRAFT/SCHEDULED schedule is a stale reservation — its load was
* moved to another wagon (moveWagonLoad keeps the emptied slot) or its
* booking left through a path that didn't clean up — and used to pin the
* wagon forever. Release those slots here instead of blocking, with the
* same recount removeTrainSetWagonSlot does (wagonCount / totalLengthMeters
* feed the schedule capacity math).
*/
private async assertDetachableAndReleaseStaleSlots(
manager: EntityManager,
wagon: Wagon,
): Promise<void> {
const rows: { id: string; train_set_id: string; status: string; allocs: string }[] =
await manager.query(
`SELECT tsw.id, tsw.train_set_id, ts.status,
(SELECT count(*)
FROM freight.wagon_booking_allocations a
WHERE a.train_set_wagon_id = tsw.id
AND a.deleted_at IS NULL) AS allocs
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 = $1
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
AND ts.deleted_at IS NULL
AND tsw.deleted_at IS NULL`,
[wagon.id],
);
if (!rows.length) return;
if (rows.some((r) => Number(r.allocs) > 0 || r.status === 'DISPATCHED')) {
throw new ConflictException(
`Wagon ${wagon.wagonNumber} is pinned to an active schedule and cannot be removed`,
);
}
await manager.getRepository(TrainSetWagon).delete(rows.map((r) => r.id));
for (const trainSetId of [...new Set(rows.map((r) => r.train_set_id))]) {
const remaining = await manager.getRepository(TrainSetWagon).find({
where: { trainSetId },
select: { id: true, lengthMeters: true },
});
await manager.getRepository(TrainSet).update(trainSetId, {
wagonCount: remaining.length,
totalLengthMeters: round(
remaining.reduce((sum, w) => sum + (Number(w.lengthMeters) || 0), 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) => {