This commit is contained in:
Marshal
2026-08-19 08:41:51 +00:00
parent f4a654f273
commit 13f66dfb66
15 changed files with 249 additions and 16 deletions

View File

@@ -42,6 +42,7 @@ import { TrainBuilderService } from './train-builder.service';
FREIGHT_PERMS.trains.delete,
FREIGHT_PERMS.trains.changeLocomotives,
FREIGHT_PERMS.trains.changeYard,
FREIGHT_PERMS.trains.changeWagonYard,
FREIGHT_PERMS.trains.toggleActive,
FREIGHT_PERMS.trains.disband,
])
@@ -107,6 +108,26 @@ export class TrainBuilderController {
return this.trainBuilderService.setYard(id, dto.currentYardId);
}
@Patch(':id/wagons/:wagonId/yard')
@FleetManage(FREIGHT_PERMS.trains.changeWagonYard)
@ApiOperation({
summary:
'Move one coupled wagon to another yard — refused while any live schedule has the wagon allocated',
})
setWagonYard(
@Param('id', ParseUUIDPipe) id: string,
@Param('wagonId', ParseUUIDPipe) wagonId: string,
@Body() dto: UpdateTrainYardDto,
@CurrentUser() user: AuthUserPayload,
) {
return this.trainBuilderService.setWagonYard(
id,
wagonId,
dto.currentYardId,
resolveAuthUserId(user),
);
}
@Post(':id/wagons')
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })

View File

@@ -514,6 +514,42 @@ export class TrainBuilderService {
return this.getComposition(id);
}
/**
* Move ONE coupled wagon to another yard (the train and the rest of the
* consist stay put). Refused while any live (DRAFT/SCHEDULED/DISPATCHED)
* schedule has the wagon allocated to a slot — its standing yard is part of
* that schedule's route validation. Ledger row mirrors `setYard`.
*/
async setWagonYard(id: string, wagonId: string, currentYardId: string, userId?: string | null) {
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 coupled to train ${train.code}`);
}
if (wagon.currentYardId === currentYardId) return;
const yard = await manager.getRepository(Yard).findOne({ where: { id: currentYardId } });
if (!yard) throw new NotFoundException(`Yard ${currentYardId} not found`);
if (await this.isWagonPinnedToLiveSchedule(manager, wagon.id)) {
throw new ConflictException(
`Wagon ${wagon.wagonNumber} is allocated to a scheduled or dispatched run; its yard cannot be changed`,
);
}
await manager.getRepository(Wagon).update(wagon.id, { currentYardId: yard.id });
await manager.getRepository(WagonMovement).save(
manager.getRepository(WagonMovement).create({
wagonId: wagon.id,
fromYardId: wagon.currentYardId ?? null,
toYardId: yard.id,
kind: WagonMovementKind.Manual,
movedByUserId: userId ?? null,
occurredAt: new Date(),
}),
);
});
return this.getComposition(id);
}
/** Append AVAILABLE, unassigned wagons (any yard) to the consist. */
async assignWagons(id: string, dto: AssignTrainWagonsDto, userId?: string | null) {
await this.dataSource.transaction(async (manager) => {