mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
automation of loading and unloading
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsIn } from 'class-validator';
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsEnum } from 'class-validator';
|
||||
|
||||
export class CreateWagonDto {
|
||||
@IsString()
|
||||
@@ -16,10 +17,6 @@ export class CreateWagonDto {
|
||||
@Min(1)
|
||||
sequenceNumber?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
currentLocationYardId?: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
tareWeight!: number;
|
||||
@@ -29,8 +26,12 @@ export class CreateWagonDto {
|
||||
maxPayloadWeight!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
||||
status?: string;
|
||||
@IsEnum(WagonStatus)
|
||||
status?: WagonStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
currentYardId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
|
||||
|
||||
export class ListWagonsQueryDto {
|
||||
@ApiPropertyOptional({ description: 'Search wagon number (partial match)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: WagonStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(WagonStatus)
|
||||
status?: WagonStatus;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by current yard' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
currentYardId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
wagonTypeId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
trainId?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: 'wagonNumber' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'ASC' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
|
||||
@ApiPropertyOptional({ minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ minimum: 1, maximum: 500 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(500)
|
||||
limit?: number;
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
// apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts
|
||||
import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { Entity, Column, ManyToOne, OneToMany, JoinColumn, Index } from 'typeorm';
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Train } from '../../trains/entities/train.entity';
|
||||
import { TrainSchedule } from '../../train-schedules/entities/train-schedule.entity';
|
||||
import { TrainSetWagon } from '../../train-sets/entities/train-set-wagon.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';
|
||||
|
||||
export const WAGON_STATUSES = [
|
||||
WagonStatus.Available,
|
||||
WagonStatus.Assigned,
|
||||
WagonStatus.Maintenance,
|
||||
WagonStatus.Retired,
|
||||
] as const;
|
||||
|
||||
export type WagonStatusType = (typeof WAGON_STATUSES)[number];
|
||||
|
||||
@Entity({ name: 'wagons', schema: 'freight' })
|
||||
@Index(['currentYardId'])
|
||||
export class Wagon extends BaseEntity {
|
||||
@Column({ unique: true, name: 'wagon_number' })
|
||||
wagonNumber!: string;
|
||||
@@ -14,36 +26,46 @@ 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;
|
||||
|
||||
@Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 })
|
||||
maxPayloadWeight!: number;
|
||||
|
||||
@Column({ type: 'varchar', default: 'AVAILABLE' })
|
||||
status!: string; // AVAILABLE, IMPORT_READY, EXPORT_READY, ASSIGNED, MAINTENANCE, RETIRED
|
||||
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
|
||||
status!: WagonStatusType;
|
||||
|
||||
@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({ type: 'text', nullable: true })
|
||||
notes!: string | null;
|
||||
|
||||
// Relationship to Train
|
||||
@Column({ name: 'train_set_wagon_id', type: 'uuid', nullable: true })
|
||||
trainSetWagonId!: string | null;
|
||||
|
||||
@ManyToOne(() => TrainSetWagon, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'train_set_wagon_id' })
|
||||
trainSetWagon?: TrainSetWagon | null;
|
||||
|
||||
@Column({ name: 'current_train_schedule_id', type: 'uuid', nullable: true })
|
||||
currentTrainScheduleId!: string | null;
|
||||
|
||||
@ManyToOne(() => TrainSchedule, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'current_train_schedule_id' })
|
||||
currentTrainSchedule?: TrainSchedule | null;
|
||||
|
||||
/** Fleet master consist grouping — separate from operational train_schedules. */
|
||||
@ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'train_id' })
|
||||
train!: Train | null;
|
||||
@@ -51,4 +73,4 @@ export class Wagon extends BaseEntity {
|
||||
// Relationship to Container
|
||||
@OneToMany(() => Container, (container) => container.wagon)
|
||||
containers!: Container[];
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||
import { CreateWagonDto } from './dto/create-wagon.dto';
|
||||
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
|
||||
import { UpdateWagonDto } from './dto/update-wagon.dto';
|
||||
import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
|
||||
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
|
||||
@@ -18,10 +20,12 @@ import { WagonsService } from './wagons.service';
|
||||
|
||||
@ApiTags('wagons')
|
||||
@Controller('wagons')
|
||||
@FleetView()
|
||||
export class WagonsController {
|
||||
constructor(private readonly wagonsService: WagonsService) {}
|
||||
|
||||
@Post()
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Create a new wagon' })
|
||||
create(@Body() dto: CreateWagonDto) {
|
||||
return this.wagonsService.create(dto);
|
||||
@@ -29,7 +33,7 @@ export class WagonsController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List all wagons' })
|
||||
findAll(@Query() query: Record<string, string | undefined>) {
|
||||
findAll(@Query() query: ListWagonsQueryDto) {
|
||||
return this.wagonsService.findAll(query);
|
||||
}
|
||||
|
||||
@@ -40,24 +44,28 @@ export class WagonsController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Update a wagon' })
|
||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) {
|
||||
return this.wagonsService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Delete a wagon' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.wagonsService.remove(id);
|
||||
}
|
||||
|
||||
@Post(':id/assign-train')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Assign wagon to a train' })
|
||||
assignToTrain(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignWagonToTrainDto) {
|
||||
return this.wagonsService.assignToTrain(id, dto);
|
||||
}
|
||||
|
||||
@Post(':id/unassign-train')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Unassign wagon from train' })
|
||||
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.wagonsService.unassignFromTrain(id);
|
||||
@@ -66,10 +74,12 @@ export class WagonsController {
|
||||
|
||||
// Separate controller for train‑specific reorder (registered in module)
|
||||
@Controller('trains/:trainId/reorder-wagons')
|
||||
@FleetView()
|
||||
export class TrainWagonsReorderController {
|
||||
constructor(private readonly wagonsService: WagonsService) {}
|
||||
|
||||
@Post()
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Reorder wagons of a train' })
|
||||
reorder(@Param('trainId', ParseUUIDPipe) trainId: string, @Body() dto: ReorderWagonsDto) {
|
||||
return this.wagonsService.reorderWagons(trainId, dto);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, FindOptionsOrder, FindOptionsWhere, ILike } from 'typeorm';
|
||||
import { CreateWagonDto } from './dto/create-wagon.dto';
|
||||
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
|
||||
import { UpdateWagonDto } from './dto/update-wagon.dto';
|
||||
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 {
|
||||
@@ -16,44 +17,48 @@ 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,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateWagonDto): Promise<Wagon> {
|
||||
const wagon = this.wagonRepo.create(dto);
|
||||
const wagon = this.wagonRepo.create({
|
||||
...dto,
|
||||
status: dto.status ?? WagonStatus.Available,
|
||||
});
|
||||
// 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);
|
||||
if (dto.currentYardId === undefined) wagon.currentYardId = null;
|
||||
return this.wagonRepo.save(wagon);
|
||||
}
|
||||
|
||||
async findAll(query: Record<string, string | undefined> = {}): Promise<Wagon[]> {
|
||||
async findAll(query: ListWagonsQueryDto = {}): Promise<Wagon[]> {
|
||||
const where: FindOptionsWhere<Wagon>[] | FindOptionsWhere<Wagon> = [];
|
||||
const search = query.search?.trim();
|
||||
const status = query.status?.trim();
|
||||
const trainId = query.trainId?.trim();
|
||||
const currentLocationYardId = query.currentLocationYardId?.trim();
|
||||
const wagonTypeId = query.wagonTypeId?.trim();
|
||||
const filters: FindOptionsWhere<Wagon> = {
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.currentYardId ? { currentYardId: query.currentYardId } : {}),
|
||||
...(trainId ? { trainId } : {}),
|
||||
...(wagonTypeId ? { wagonTypeId } : {}),
|
||||
};
|
||||
|
||||
if (search) {
|
||||
where.push({
|
||||
wagonNumber: ILike(`%${search}%`),
|
||||
...(status ? { status } : {}),
|
||||
...(trainId ? { trainId } : {}),
|
||||
...(currentLocationYardId ? { currentLocationYardId } : {}),
|
||||
...filters,
|
||||
});
|
||||
}
|
||||
|
||||
const sortBy = ['wagonNumber', 'tareWeight', 'maxPayloadWeight', 'status', 'sequenceNumber'].includes(query.sortBy ?? '')
|
||||
const sortBy = ['wagonNumber', 'tareWeight', 'maxPayloadWeight', 'status', 'currentYardId', 'sequenceNumber'].includes(query.sortBy ?? '')
|
||||
? (query.sortBy as keyof Wagon)
|
||||
: 'wagonNumber';
|
||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
return this.wagonRepo.find({
|
||||
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
|
||||
relations: { currentLocationYard: true, wagonType: true },
|
||||
where: search ? where : filters,
|
||||
relations: { currentYard: 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,
|
||||
@@ -61,7 +66,10 @@ export class WagonsService {
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Wagon> {
|
||||
const wagon = await this.wagonRepo.findOne({ where: { id }, relations: { currentLocationYard: true, wagonType: true } });
|
||||
const wagon = await this.wagonRepo.findOne({
|
||||
where: { id },
|
||||
relations: { currentYard: true },
|
||||
});
|
||||
if (!wagon) throw new NotFoundException(`Wagon ${id} not found`);
|
||||
return wagon;
|
||||
}
|
||||
@@ -69,9 +77,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -82,7 +87,7 @@ export class WagonsService {
|
||||
|
||||
async assignToTrain(wagonId: string, dto: AssignWagonToTrainDto): Promise<Wagon> {
|
||||
const wagon = await this.findById(wagonId);
|
||||
if (wagon.status === 'ASSIGNED') {
|
||||
if (wagon.status === WagonStatus.Assigned) {
|
||||
throw new ConflictException('Wagon already assigned to a train');
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ export class WagonsService {
|
||||
|
||||
wagon.trainId = train.id;
|
||||
wagon.sequenceNumber = sequence;
|
||||
wagon.status = 'ASSIGNED';
|
||||
wagon.status = WagonStatus.Assigned;
|
||||
return this.wagonRepo.save(wagon);
|
||||
}
|
||||
|
||||
@@ -109,21 +114,10 @@ export class WagonsService {
|
||||
const wagon = await this.findById(wagonId);
|
||||
wagon.trainId = null;
|
||||
wagon.sequenceNumber = null;
|
||||
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
|
||||
wagon.status = WagonStatus.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();
|
||||
|
||||
Reference in New Issue
Block a user