Replace wagon/locomotive readiness with yard tracking, assign unassigned bookings from origin-yard fleet, standardize rates on USD with CBE ETB conversion, and update fleet/scheduling UI

This commit is contained in:
marshal
2026-06-14 01:32:39 +03:00
parent b73bf2154e
commit 87b0ce6339
45 changed files with 1249 additions and 520 deletions

View File

@@ -1,9 +1,8 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsIn, IsNumber, IsOptional, IsString, MaxLength, Min } from 'class-validator';
import { IsIn, IsNumber, IsOptional, IsString, MaxLength, Min, IsUUID } from 'class-validator';
import {
LOCOMOTIVE_READINESS_VALUES,
LOCOMOTIVE_STATUSES,
LOCOMOTIVE_TYPES,
} from '../entities/locomotive.entity';
@@ -28,10 +27,10 @@ export class CreateLocomotiveDto {
@IsIn([...LOCOMOTIVE_STATUSES])
status!: string;
@ApiPropertyOptional({ enum: LOCOMOTIVE_READINESS_VALUES, default: 'IMPORT_READY' })
@ApiPropertyOptional({ description: 'Current yard location' })
@IsOptional()
@IsIn([...LOCOMOTIVE_READINESS_VALUES])
readiness?: string;
@IsUUID()
currentYardId?: string;
@ApiProperty({ example: 3500 })
@Transform(({ value }) => Number(value))

View File

@@ -1,8 +1,7 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsOptional } from 'class-validator';
import { IsIn, IsOptional, IsUUID } from 'class-validator';
import {
LOCOMOTIVE_READINESS_VALUES,
LOCOMOTIVE_STATUSES,
LOCOMOTIVE_TYPES,
} from '../entities/locomotive.entity';
@@ -18,8 +17,8 @@ export class FilterLocomotivesDto {
@IsIn([...LOCOMOTIVE_TYPES])
locomotiveType?: string;
@ApiPropertyOptional({ enum: LOCOMOTIVE_READINESS_VALUES })
@ApiPropertyOptional({ description: 'Filter by current yard' })
@IsOptional()
@IsIn([...LOCOMOTIVE_READINESS_VALUES])
readiness?: string;
@IsUUID()
currentYardId?: string;
}

View File

@@ -1,8 +1,8 @@
import { BaseEntity } from '@edr/api-common';
import { WagonReadiness } from '@edr/types';
import { Column, Entity, Index, OneToMany } from 'typeorm';
import { Column, Entity, Index, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
import { TrainSet } from '../../train-sets/entities/train-set.entity';
import { Yard } from '../../rule-engine/entities/yard.entity';
export const LOCOMOTIVE_STATUSES = [
'AVAILABLE',
@@ -13,20 +13,13 @@ export const LOCOMOTIVE_STATUSES = [
export const LOCOMOTIVE_TYPES = ['DIESEL', 'ELECTRIC'] as const;
/** Locomotives reuse the wagon readiness values (IMPORT_READY / EXPORT_READY). */
export const LOCOMOTIVE_READINESS_VALUES = [
WagonReadiness.ImportReady,
WagonReadiness.ExportReady,
] as const;
export type LocomotiveStatus = (typeof LOCOMOTIVE_STATUSES)[number];
export type LocomotiveType = (typeof LOCOMOTIVE_TYPES)[number];
export type LocomotiveReadiness = (typeof LOCOMOTIVE_READINESS_VALUES)[number];
@Entity({ schema: 'freight', name: 'locomotives' })
@Index(['code'])
@Index(['status'])
@Index(['readiness'])
@Index(['currentYardId'])
export class Locomotive extends BaseEntity {
@Column({ name: 'code', type: 'varchar', length: 32, unique: true })
code!: string;
@@ -46,8 +39,12 @@ export class Locomotive extends BaseEntity {
@Column({ name: 'status', type: 'varchar', length: 20, default: 'AVAILABLE' })
status!: LocomotiveStatus;
@Column({ name: 'readiness', type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
readiness!: LocomotiveReadiness;
@Column({ name: 'current_yard_id', type: 'uuid', nullable: true })
currentYardId!: string | null;
@ManyToOne(() => Yard, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'current_yard_id' })
currentYard?: Yard | null;
@Column({ name: 'power_kw', type: 'numeric', precision: 10, scale: 3, nullable: true })
powerKw?: number | null;

View File

@@ -3,11 +3,9 @@ import { ConflictException, Injectable, NotFoundException } from '@nestjs/common
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
import { WagonReadiness } from '@edr/types';
import {
Locomotive,
type LocomotiveReadiness,
type LocomotiveStatus,
type LocomotiveType,
} from './entities/locomotive.entity';
@@ -24,8 +22,9 @@ export class LocomotivesService {
...(filter.locomotiveType
? { locomotiveType: filter.locomotiveType as LocomotiveType }
: {}),
...(filter.readiness ? { readiness: filter.readiness as LocomotiveReadiness } : {}),
...(filter.currentYardId ? { currentYardId: filter.currentYardId } : {}),
},
relations: { currentYard: true },
order: { code: 'ASC' },
});
}
@@ -42,7 +41,7 @@ export class LocomotivesService {
name: dto.name?.trim() || null,
locomotiveType: dto.locomotiveType as LocomotiveType,
status: dto.status as LocomotiveStatus,
readiness: (dto.readiness as LocomotiveReadiness) ?? WagonReadiness.ImportReady,
currentYardId: dto.currentYardId ?? null,
maxPullWeightTons: dto.maxPullWeightTons,
maxTrainLengthMeters: dto.maxTrainLengthMeters,
powerKw: dto.powerKw ?? null,
@@ -52,7 +51,9 @@ export class LocomotivesService {
}
async findById(id: string): Promise<Locomotive> {
const locomotive = await this.locomotivesRepository.findById(id);
const locomotive = await this.locomotivesRepository.findById(id, {
relations: { currentYard: true },
});
if (!locomotive) {
throw new NotFoundException(`Locomotive ${id} not found`);
@@ -76,10 +77,10 @@ export class LocomotivesService {
locomotiveType:
dto.locomotiveType === undefined ? locomotive.locomotiveType : dto.locomotiveType as LocomotiveType,
status: dto.status === undefined ? locomotive.status : dto.status as LocomotiveStatus,
readiness:
dto.readiness === undefined
? locomotive.readiness
: (dto.readiness as LocomotiveReadiness),
currentYardId:
dto.currentYardId === undefined
? locomotive.currentYardId
: (dto.currentYardId ?? null),
name: dto.name === undefined ? locomotive.name : dto.name?.trim() || null,
powerKw: dto.powerKw === undefined ? locomotive.powerKw : dto.powerKw ?? null,
tractionForceKn: