mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
train
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class UpdateTrainYardDto {
|
||||
@ApiProperty({
|
||||
format: 'uuid',
|
||||
description:
|
||||
'Yard the train now sits in. The coupled locomotives and wagons are relocated with it.',
|
||||
})
|
||||
@IsUUID()
|
||||
currentYardId!: string;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
@@ -19,6 +20,7 @@ import { BuildTrainDto } from './dto/build-train.dto';
|
||||
import { ListBuiltTrainsQueryDto } from './dto/list-built-trains-query.dto';
|
||||
import { ReorderTrainWagonsDto } from './dto/reorder-train-wagons.dto';
|
||||
import { UpdateTrainLocomotivesDto } from './dto/update-train-locomotives.dto';
|
||||
import { UpdateTrainYardDto } from './dto/update-train-yard.dto';
|
||||
import { TrainBuilderService } from './train-builder.service';
|
||||
|
||||
@ApiTags('train-builder')
|
||||
@@ -57,6 +59,15 @@ export class TrainBuilderController {
|
||||
return this.trainBuilderService.setLocomotives(id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/yard')
|
||||
@FleetManage()
|
||||
@ApiOperation({
|
||||
summary: 'Relocate the train — its locomotives and wagons move to the new yard with it',
|
||||
})
|
||||
setYard(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateTrainYardDto) {
|
||||
return this.trainBuilderService.setYard(id, dto.currentYardId);
|
||||
}
|
||||
|
||||
@Post(':id/wagons')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Freight, WagonStatus } from '@edr/types';
|
||||
import { Freight, WagonMovementKind, WagonStatus } from '@edr/types';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
@@ -10,6 +10,7 @@ import { DataSource, EntityManager, ILike, In } from 'typeorm';
|
||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { minLocomotiveLimits } from '../train-scheduling/train-capacity.util';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { AssignTrainWagonsDto } from './dto/assign-train-wagons.dto';
|
||||
import { BuildTrainDto } from './dto/build-train.dto';
|
||||
@@ -202,7 +203,6 @@ export class TrainBuilderService {
|
||||
const totalLengthMeters = round(
|
||||
wagons.reduce((sum, w) => sum + (w.wagonType?.lengthMeters ?? 0), 0),
|
||||
);
|
||||
const maxGrossTons = round(totalTareTons + totalCapacityTons);
|
||||
const maxPullWeightTons = round(limits?.maxPullWeightTons ?? 0);
|
||||
const maxTrainLengthMeters = round(limits?.maxTrainLengthMeters ?? 0);
|
||||
|
||||
@@ -221,14 +221,17 @@ export class TrainBuilderService {
|
||||
totals: {
|
||||
wagonCount: wagons.length,
|
||||
totalTareTons,
|
||||
// Informational only — building never checks against full capacity;
|
||||
// the real gross check (cargo + tare vs haul limit) runs at allocation.
|
||||
totalCapacityTons,
|
||||
maxGrossTons,
|
||||
totalLengthMeters,
|
||||
maxPullWeightTons,
|
||||
maxTrainLengthMeters,
|
||||
// Fully loaded gross vs. what the weakest locomotive can haul.
|
||||
weightUtilizationPct: maxPullWeightTons
|
||||
? round((maxGrossTons / maxPullWeightTons) * 100)
|
||||
// Cargo the locomotives can still haul once pulling the empty consist.
|
||||
payloadCapacityTons: round(Math.max(0, maxPullWeightTons - totalTareTons)),
|
||||
// Share of the haul limit consumed by the empty wagons alone.
|
||||
tareUtilizationPct: maxPullWeightTons
|
||||
? round((totalTareTons / maxPullWeightTons) * 100)
|
||||
: null,
|
||||
lengthUtilizationPct: maxTrainLengthMeters
|
||||
? round((totalLengthMeters / maxTrainLengthMeters) * 100)
|
||||
@@ -269,6 +272,54 @@ export class TrainBuilderService {
|
||||
return this.getComposition(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relocate the train to another yard. The consist moves as one unit: every
|
||||
* coupled locomotive and wagon follows to the new yard (so their current
|
||||
* yards always match the train's), and each wagon gets a movement-ledger row.
|
||||
* Blocked while the train is out on a dispatched run.
|
||||
*/
|
||||
async setYard(id: string, currentYardId: string) {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const train = await this.getEditableTrain(manager, id);
|
||||
if (train.currentYardId === currentYardId) return;
|
||||
const yard = await manager.getRepository(Yard).findOne({ where: { id: currentYardId } });
|
||||
if (!yard) throw new NotFoundException(`Yard ${currentYardId} not found`);
|
||||
|
||||
await manager.getRepository(Train).update(train.id, { currentYardId: yard.id });
|
||||
|
||||
const links = await manager
|
||||
.getRepository(TrainLocomotive)
|
||||
.find({ where: { trainId: train.id } });
|
||||
if (links.length) {
|
||||
await manager
|
||||
.getRepository(Locomotive)
|
||||
.update(
|
||||
{ id: In(links.map((link) => link.locomotiveId)) },
|
||||
{ currentYardId: yard.id },
|
||||
);
|
||||
}
|
||||
|
||||
const wagons = await manager.getRepository(Wagon).find({ where: { trainId: train.id } });
|
||||
const now = new Date();
|
||||
for (const wagon of wagons) {
|
||||
if (wagon.currentYardId === yard.id) continue;
|
||||
await manager.getRepository(Wagon).update(wagon.id, { currentYardId: yard.id });
|
||||
// Ledger row keeps the wagon's yard history auditable (mirrors the
|
||||
// manual-relocation path in the wagons service).
|
||||
await manager.getRepository(WagonMovement).save(
|
||||
manager.getRepository(WagonMovement).create({
|
||||
wagonId: wagon.id,
|
||||
fromYardId: wagon.currentYardId ?? null,
|
||||
toYardId: yard.id,
|
||||
kind: WagonMovementKind.Manual,
|
||||
occurredAt: now,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return this.getComposition(id);
|
||||
}
|
||||
|
||||
/** Append AVAILABLE wagons from the train's own yard to the consist. */
|
||||
async assignWagons(id: string, dto: AssignTrainWagonsDto) {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
@@ -361,12 +412,8 @@ export class TrainBuilderService {
|
||||
.map((link) => link.locomotive)
|
||||
.filter((loco): loco is Locomotive => Boolean(loco));
|
||||
const wagons = train.wagons ?? [];
|
||||
const maxGrossTons = round(
|
||||
wagons.reduce(
|
||||
(sum, w) =>
|
||||
sum + (Number(w.wagonType?.tareWeightTons) || 0) + (Number(w.wagonType?.capacityTons) || 0),
|
||||
0,
|
||||
),
|
||||
const totalTareTons = round(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.tareWeightTons) || 0), 0),
|
||||
);
|
||||
return {
|
||||
id: train.id,
|
||||
@@ -379,7 +426,7 @@ export class TrainBuilderService {
|
||||
: null,
|
||||
locomotives: locomotives.map((loco) => ({ id: loco.id, code: loco.code, name: loco.name ?? null })),
|
||||
wagonCount: wagons.length,
|
||||
maxGrossTons,
|
||||
totalTareTons,
|
||||
totalLengthMeters: round(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.lengthMeters) || 0), 0),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user