Warehouse Enhancemendt

This commit is contained in:
hagiye
2026-06-20 11:49:22 +03:00
1393 changed files with 334619 additions and 20282 deletions

View File

@@ -1,4 +1,4 @@
import { WagonReadiness, WagonStatus } from '@edr/types';
import { WagonStatus } from '@edr/types';
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsEnum } from 'class-validator';
export class CreateWagonDto {
@@ -17,10 +17,6 @@ export class CreateWagonDto {
@Min(1)
sequenceNumber?: number;
@IsOptional()
@IsUUID()
currentLocationYardId?: string;
@IsNumber()
@Min(0)
tareWeight!: number;
@@ -34,8 +30,8 @@ export class CreateWagonDto {
status?: WagonStatus;
@IsOptional()
@IsEnum(WagonReadiness)
readiness?: WagonReadiness;
@IsUUID()
currentYardId?: string;
@IsOptional()
@IsString()

View File

@@ -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;
}

View File

@@ -1,5 +1,5 @@
// apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts
import { WagonReadiness, WagonStatus } from '@edr/types';
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';
@@ -7,7 +7,6 @@ import { TrainSchedule } from '../../train-schedules/entities/train-schedule.ent
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,
@@ -18,16 +17,10 @@ export const WAGON_STATUSES = [
WagonStatus.Retired,
] as const;
export const WAGON_READINESS_VALUES = [
WagonReadiness.ImportReady,
WagonReadiness.ExportReady,
] as const;
export type WagonStatusType = (typeof WAGON_STATUSES)[number];
export type WagonReadinessType = (typeof WAGON_READINESS_VALUES)[number];
@Entity({ name: 'wagons', schema: 'freight' })
@Index(['readiness'])
@Index(['currentYardId'])
export class Wagon extends BaseEntity {
@Column({ unique: true, name: 'wagon_number' })
wagonNumber!: string;
@@ -35,23 +28,12 @@ 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;
@@ -61,8 +43,12 @@ export class Wagon extends BaseEntity {
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
status!: WagonStatusType;
@Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
readiness!: WagonReadinessType;
@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;
@@ -89,4 +75,4 @@ export class Wagon extends BaseEntity {
// Relationship to Container
@OneToMany(() => Container, (container) => container.wagon)
containers!: Container[];
}
}

View File

@@ -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 trainspecific 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);

View File

@@ -1,14 +1,14 @@
import { WagonReadiness, WagonStatus } from '@edr/types';
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 {
@@ -17,8 +17,6 @@ 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,
) {}
@@ -26,25 +24,24 @@ export class WagonsService {
const wagon = this.wagonRepo.create({
...dto,
status: dto.status ?? WagonStatus.Available,
readiness: dto.readiness ?? WagonReadiness.ImportReady,
});
// 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 readiness = query.readiness?.trim();
const trainId = query.trainId?.trim();
const filters = {
...(status ? { status: status as Wagon['status'] } : {}),
...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}),
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) {
@@ -54,13 +51,14 @@ export class WagonsService {
});
}
const sortBy = ['wagonNumber', 'tareWeight', 'maxPayloadWeight', 'status', 'readiness', '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 : 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,
@@ -68,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;
}
@@ -76,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);
}
@@ -120,22 +118,6 @@ export class WagonsService {
return this.wagonRepo.save(wagon);
}
private async statusForLocation(
yardId?: string | null,
fallback: Wagon['status'] = WagonStatus.Available,
): Promise<Wagon['status']> {
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 WagonStatus.ExportReady;
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') {
return WagonStatus.ImportReady;
}
return fallback;
}
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();