Train scheduling API,Routes and UI

This commit is contained in:
hagiye
2026-06-08 16:31:28 +03:00
parent 3d1f972e52
commit 7facbeda22
38 changed files with 1993 additions and 696 deletions

View File

@@ -16,6 +16,10 @@ export class CreateWagonDto {
@Min(1)
sequenceNumber?: number;
@IsOptional()
@IsUUID()
currentLocationYardId?: string;
@IsNumber()
@Min(0)
tareWeight!: number;
@@ -25,10 +29,10 @@ export class CreateWagonDto {
maxPayloadWeight!: number;
@IsOptional()
@IsIn(['AVAILABLE', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
status?: string;
@IsOptional()
@IsString()
notes?: string;
}
}

View File

@@ -3,6 +3,8 @@ import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { BaseEntity } from '@edr/api-common';
import { Train } from '../../trains/entities/train.entity';
import { Container } from '../../container-management/entities/container.entity';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
@Entity({ name: 'wagons', schema: 'freight' })
export class Wagon extends BaseEntity {
@@ -12,12 +14,23 @@ export class Wagon extends BaseEntity {
@Column({ name: 'wagon_type_id', type: 'uuid' })
wagonTypeId!: string;
@ManyToOne(() => WagonType, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'wagon_type_id' })
wagonType?: WagonType;
@Column({ name: 'train_id', type: 'uuid', nullable: true })
trainId!: string | null;
@Column({ name: 'sequence_number', type: 'int', nullable: true })
sequenceNumber!: number | null;
@Column({ name: 'current_location_yard_id', type: 'uuid', nullable: true })
currentLocationYardId!: string | null;
@ManyToOne(() => Yard, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'current_location_yard_id' })
currentLocationYard?: Yard | null;
@Column({ name: 'tare_weight', type: 'decimal', precision: 10, scale: 2 })
tareWeight!: number;
@@ -25,7 +38,7 @@ export class Wagon extends BaseEntity {
maxPayloadWeight!: number;
@Column({ type: 'varchar', default: 'AVAILABLE' })
status!: string; // AVAILABLE, ASSIGNED, MAINTENANCE, RETIRED
status!: string; // AVAILABLE, IMPORT_READY, EXPORT_READY, ASSIGNED, MAINTENANCE, RETIRED
@Column({ type: 'text', nullable: true })
notes!: string | null;
@@ -38,4 +51,4 @@ export class Wagon extends BaseEntity {
// Relationship to Container
@OneToMany(() => Container, (container) => container.wagon)
containers!: Container[];
}
}

View File

@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Wagon } from './entities/wagon.entity';
import { Train } from '../trains/entities/train.entity';
import { Yard } from '../rule-engine/entities/yard.entity';
import { WagonsController, TrainWagonsReorderController } from './wagons.controller';
import { WagonsService } from './wagons.service';
@Module({
imports: [TypeOrmModule.forFeature([Wagon, Train])],
imports: [TypeOrmModule.forFeature([Wagon, Train, Yard])],
controllers: [WagonsController, TrainWagonsReorderController],
providers: [WagonsService],
exports: [WagonsService],
})
export class WagonsModule {}
export class WagonsModule {}

View File

@@ -7,6 +7,7 @@ import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
import { Wagon } from './entities/wagon.entity';
import { Train } from '../trains/entities/train.entity';
import { Yard } from '../rule-engine/entities/yard.entity';
@Injectable()
export class WagonsService {
@@ -15,6 +16,8 @@ export class WagonsService {
private readonly wagonRepo: Repository<Wagon>,
@InjectRepository(Train)
private readonly trainRepo: Repository<Train>,
@InjectRepository(Yard)
private readonly yardRepo: Repository<Yard>,
private readonly dataSource: DataSource,
) {}
@@ -23,6 +26,7 @@ export class WagonsService {
// Convert undefined to null for nullable fields
if (dto.trainId === undefined) wagon.trainId = null;
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
return this.wagonRepo.save(wagon);
}
@@ -31,12 +35,14 @@ export class WagonsService {
const search = query.search?.trim();
const status = query.status?.trim();
const trainId = query.trainId?.trim();
const currentLocationYardId = query.currentLocationYardId?.trim();
if (search) {
where.push({
wagonNumber: ILike(`%${search}%`),
...(status ? { status } : {}),
...(trainId ? { trainId } : {}),
...(currentLocationYardId ? { currentLocationYardId } : {}),
});
}
@@ -46,7 +52,8 @@ export class WagonsService {
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
return this.wagonRepo.find({
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}) },
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
relations: { currentLocationYard: true, wagonType: true },
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
take: query.limit ? Number(query.limit) : undefined,
@@ -54,7 +61,7 @@ export class WagonsService {
}
async findById(id: string): Promise<Wagon> {
const wagon = await this.wagonRepo.findOne({ where: { id } });
const wagon = await this.wagonRepo.findOne({ where: { id }, relations: { currentLocationYard: true, wagonType: true } });
if (!wagon) throw new NotFoundException(`Wagon ${id} not found`);
return wagon;
}
@@ -62,6 +69,9 @@ export class WagonsService {
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
const wagon = await this.findById(id);
Object.assign(wagon, dto);
if (dto.currentLocationYardId !== undefined) {
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
}
return this.wagonRepo.save(wagon);
}
@@ -99,10 +109,21 @@ export class WagonsService {
const wagon = await this.findById(wagonId);
wagon.trainId = null;
wagon.sequenceNumber = null;
wagon.status = 'AVAILABLE';
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
return this.wagonRepo.save(wagon);
}
private async statusForLocation(yardId?: string | null, fallback = 'AVAILABLE') {
if (!yardId) return fallback;
const yard = await this.yardRepo.findOne({ where: { id: yardId } });
const country = yard?.country?.trim().toLowerCase();
if (country === 'ethiopia' || country === 'et') return 'EXPORT_READY';
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') return 'IMPORT_READY';
return fallback;
}
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();