feat(train-sets): implement multi-locomotive support for train sets

- Added TrainSetLocomotive entity to link multiple locomotives to a train set.
- Updated TrainSet entity to include a OneToMany relationship with TrainSetLocomotive.
- Modified the train scheduling logic to require at least two locomotives for a train set.
- Enhanced the UI components to support selecting multiple locomotives.
- Introduced new permissions for viewing customs clearance.
- Updated migrations to create the train_set_locomotives table and backfill existing data.
- Implemented utility functions for managing train numbers based on cargo type and direction.
- Added tests for train number utilities to ensure correct functionality.
This commit is contained in:
Marshal
2026-06-24 23:54:45 +00:00
parent 15ab9f906e
commit e0c3044933
28 changed files with 817 additions and 118 deletions

View File

@@ -0,0 +1,31 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Locomotive } from '../../locomotives/entities/locomotive.entity';
import { TrainSet } from './train-set.entity';
/**
* Link row joining a train set to one of its locomotives. A train set must be
* pulled by at least two locomotives (front + back); `sequenceNo` is a plain
* order index — no front/rear semantics are modelled yet.
*/
@Entity({ schema: 'freight', name: 'train_set_locomotives' })
@Index(['trainSetId', 'locomotiveId'], { unique: true })
export class TrainSetLocomotive extends BaseEntity {
@Column({ name: 'train_set_id', type: 'uuid' })
trainSetId!: string;
@ManyToOne(() => TrainSet, (trainSet) => trainSet.locomotives, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'train_set_id' })
trainSet?: TrainSet;
@Column({ name: 'locomotive_id', type: 'uuid' })
locomotiveId!: string;
@ManyToOne(() => Locomotive)
@JoinColumn({ name: 'locomotive_id' })
locomotive?: Locomotive;
@Column({ name: 'sequence_no', type: 'int', default: 0 })
sequenceNo!: number;
}

View File

@@ -3,6 +3,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, OneToOne } fro
import { Locomotive } from '../../locomotives/entities/locomotive.entity';
import { TrainSchedule } from '../../train-schedules/entities/train-schedule.entity';
import { TrainSetLocomotive } from './train-set-locomotive.entity';
import { TrainSetWagon } from './train-set-wagon.entity';
export const TRAIN_SET_STATUSES = [
@@ -19,6 +20,7 @@ export type TrainSetStatus = (typeof TRAIN_SET_STATUSES)[number];
@Index(['locomotiveId'])
@Index(['status'])
export class TrainSet extends BaseEntity {
/** Primary locomotive (first of the set). Kept for back-compat with single-loco read paths. */
@Column({ name: 'locomotive_id', type: 'uuid' })
locomotiveId!: string;
@@ -26,6 +28,10 @@ export class TrainSet extends BaseEntity {
@JoinColumn({ name: 'locomotive_id' })
locomotive?: Locomotive;
/** All locomotives pulling this train set (minimum 2). */
@OneToMany(() => TrainSetLocomotive, (link) => link.trainSet)
locomotives?: TrainSetLocomotive[];
@Column({ name: 'total_weight_tons', type: 'numeric', precision: 10, scale: 3 })
totalWeightTons!: number;

View File

@@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TrainSet } from './entities/train-set.entity';
import { TrainSetLocomotive } from './entities/train-set-locomotive.entity';
import { TrainSetWagon } from './entities/train-set-wagon.entity';
import { TrainSetWagonsRepository } from './train-set-wagons.repository';
import { TrainSetsRepository } from './train-sets.repository';
@Module({
imports: [TypeOrmModule.forFeature([TrainSet, TrainSetWagon])],
imports: [TypeOrmModule.forFeature([TrainSet, TrainSetWagon, TrainSetLocomotive])],
providers: [TrainSetsRepository, TrainSetWagonsRepository],
exports: [TrainSetsRepository, TrainSetWagonsRepository],
})