implement wagon maintenance feature: add functionality to detach wagons and move them to maintenance status

This commit is contained in:
Marshal
2026-07-15 10:32:09 +00:00
parent 11771e5f92
commit c71a0043d6
9 changed files with 166 additions and 50 deletions

View File

@@ -59,11 +59,7 @@ export class TrainBuilderService {
}
const trainId = await this.dataSource.transaction(async (manager) => {
const code = dto.code.trim();
const existing = await manager.getRepository(Train).findOne({ where: { code } });
if (existing) {
throw new ConflictException(`Train code ${code} is already in use`);
}
const code = await this.generateTrainCode(manager);
// Friendly 409 before the partial unique indexes (the race-proof backstop):
// the typed pair may not collide with any train's pair or legacy number.
@@ -418,6 +414,33 @@ export class TrainBuilderService {
return this.getComposition(id);
}
/**
* Detach one wagon AND flag it for maintenance: it leaves the consist and
* moves to MAINTENANCE status (not AVAILABLE), so it is not re-coupled until
* it clears maintenance. The freed sequence gap is closed.
*/
async sendWagonToMaintenance(id: string, wagonId: string) {
await this.dataSource.transaction(async (manager) => {
const train = await this.getEditableTrain(manager, id);
const wagon = await manager.getRepository(Wagon).findOne({ where: { id: wagonId } });
if (!wagon || wagon.trainId !== train.id) {
throw new NotFoundException(`Wagon ${wagonId} is not part of this train`);
}
if (wagon.currentTrainScheduleId) {
throw new ConflictException(
`Wagon ${wagon.wagonNumber} is pinned to an active schedule and cannot be removed`,
);
}
await manager.getRepository(Wagon).update(wagon.id, {
trainId: null,
sequenceNumber: null,
status: WagonStatus.Maintenance,
});
await this.resequenceWagons(manager, train.id);
});
return this.getComposition(id);
}
/** 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) => {
@@ -469,6 +492,29 @@ export class TrainBuilderService {
// ---------------------------------------------------------------- internals
/**
* System-assigned train code `TR-NNNNN`. Draws the next number from the
* highest existing `TR-` code and probes past any manual collision so the
* unique constraint never rejects the build.
*/
private async generateTrainCode(manager: EntityManager): Promise<string> {
const [row]: { max_seq: string | null }[] = await manager.query(
`SELECT MAX(CAST(SUBSTRING(code FROM '^TR-([0-9]+)$') AS INTEGER)) AS max_seq
FROM freight.trains
WHERE code ~ '^TR-[0-9]+$'`,
);
let seq = Number(row?.max_seq ?? 0) + 1;
for (let attempt = 0; attempt < 50; attempt += 1) {
const code = `TR-${String(seq).padStart(5, '0')}`;
const exists = await manager
.getRepository(Train)
.findOne({ where: { code }, withDeleted: true });
if (!exists) return code;
seq += 1;
}
throw new ConflictException('Could not allocate a unique train code');
}
private mapSummary(train: Train, activeSchedule: ActiveScheduleRef | null) {
const locomotives = [...(train.locomotives ?? [])]
.sort((a, b) => a.sequenceNo - b.sequenceNo)