resolve confilict

This commit is contained in:
marshal
2026-06-05 13:06:16 +03:00
82 changed files with 3296 additions and 65 deletions

View File

@@ -0,0 +1,70 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateCargoDto } from './dto/create-cargo.dto';
import { UpdateCargoDto } from './dto/update-cargo.dto';
import { LoadCargoDto } from './dto/load-cargo.dto';
import { DeliverCargoDto } from './dto/deliver-cargo.dto';
import { CargoesService } from './cargoes.service';
@ApiTags('cargoes')
@Controller('cargoes')
export class CargoesController {
constructor(private readonly cargoesService: CargoesService) {}
@Post()
@ApiOperation({ summary: 'Create a new cargo' })
create(@Body() dto: CreateCargoDto) {
return this.cargoesService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List all cargoes' })
findAll() {
return this.cargoesService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a cargo by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.cargoesService.findById(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a cargo' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoDto) {
return this.cargoesService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a cargo' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.cargoesService.remove(id);
}
@Post(':id/load')
@ApiOperation({ summary: 'Load cargo into a container' })
load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadCargoDto) {
return this.cargoesService.loadCargo(id, dto);
}
@Post(':id/unload')
@ApiOperation({ summary: 'Unload cargo from container' })
unload(@Param('id', ParseUUIDPipe) id: string) {
return this.cargoesService.unloadCargo(id);
}
@Post(':id/deliver')
@ApiOperation({ summary: 'Mark cargo as delivered' })
deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto?: DeliverCargoDto) {
return this.cargoesService.deliverCargo(id, dto);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cargo } from './entities/cargoes.entity';
import { Container } from '../container-management/entities/container.entity';
import { CargoesController } from './cargoes.controller';
import { CargoesService } from './cargoes.service';
@Module({
imports: [TypeOrmModule.forFeature([Cargo, Container])],
controllers: [CargoesController],
providers: [CargoesService],
exports: [CargoesService],
})
export class CargoesModule {}

View File

@@ -0,0 +1,15 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cargo } from './entities/cargoes.entity';
@Injectable()
export class CargoesRepository extends BaseRepository<Cargo> {
constructor(
@InjectRepository(Cargo)
repository: Repository<Cargo>,
) {
super(repository);
}
}

View File

@@ -0,0 +1,104 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCargoDto } from './dto/create-cargo.dto';
import { UpdateCargoDto } from './dto/update-cargo.dto';
import { LoadCargoDto } from './dto/load-cargo.dto';
import { DeliverCargoDto } from './dto/deliver-cargo.dto';
import { Cargo } from './entities/cargoes.entity';
import { Container } from '../container-management/entities/container.entity';
@Injectable()
export class CargoesService {
constructor(
@InjectRepository(Cargo)
private readonly cargoRepo: Repository<Cargo>,
@InjectRepository(Container)
private readonly containerRepo: Repository<Container>,
) {}
async create(dto: CreateCargoDto): Promise<Cargo> {
const cargo = this.cargoRepo.create(dto);
return this.cargoRepo.save(cargo);
}
async findAll(): Promise<Cargo[]> {
return this.cargoRepo.find({ order: { cargoReference: 'ASC' } });
}
async findById(id: string): Promise<Cargo> {
const cargo = await this.cargoRepo.findOne({ where: { id } });
if (!cargo) throw new NotFoundException(`Cargo ${id} not found`);
return cargo;
}
async update(id: string, dto: UpdateCargoDto): Promise<Cargo> {
const cargo = await this.findById(id);
Object.assign(cargo, dto);
return this.cargoRepo.save(cargo);
}
async remove(id: string): Promise<void> {
const cargo = await this.findById(id);
await this.cargoRepo.remove(cargo);
}
async loadCargo(id: string, dto: LoadCargoDto): Promise<Cargo> {
const cargo = await this.cargoRepo.findOne({
where: { id },
relations: { container: true }, // ✅ fixed
});
if (!cargo) throw new NotFoundException('Cargo not found');
if (cargo.status !== 'PENDING') {
throw new ConflictException('Cargo already loaded or delivered');
}
cargo.status = 'LOADED';
cargo.loadedAt = new Date();
cargo.quantity = dto.quantity;
cargo.weight = dto.weight;
cargo.volume = dto.volume ?? null;
if (dto.description) cargo.description = dto.description;
if (cargo.container) {
cargo.container.status = 'LOADED';
await this.containerRepo.save(cargo.container);
}
return this.cargoRepo.save(cargo);
}
async unloadCargo(id: string): Promise<Cargo> {
const cargo = await this.findById(id);
if (cargo.status !== 'LOADED') {
throw new ConflictException('Cargo is not loaded');
}
cargo.status = 'UNLOADED';
cargo.unloadedAt = new Date();
return this.cargoRepo.save(cargo);
}
async deliverCargo(id: string, dto?: DeliverCargoDto): Promise<Cargo> {
const cargo = await this.cargoRepo.findOne({
where: { id },
relations: { container: true }, // ✅ fixed
});
if (!cargo) throw new NotFoundException('Cargo not found');
if (cargo.status !== 'LOADED') {
throw new ConflictException('Only loaded cargo can be delivered');
}
cargo.status = 'DELIVERED';
if (dto?.deliveryRemarks) cargo.description = dto.deliveryRemarks;
const remaining = await this.cargoRepo.count({
where: { containerId: cargo.containerId, status: 'LOADED' },
});
if (remaining === 0 && cargo.container) {
cargo.container.status = 'AVAILABLE';
await this.containerRepo.save(cargo.container);
}
return this.cargoRepo.save(cargo);
}
}

View File

@@ -0,0 +1,45 @@
import { IsString, IsUUID, IsOptional, IsNumber, Min, IsIn, IsDateString } from 'class-validator';
export class CreateCargoDto {
@IsString()
cargoReference!: string;
@IsUUID()
shipmentId!: string;
@IsUUID()
containerId!: string;
@IsOptional()
@IsUUID()
cargoTypeId?: string;
@IsOptional()
@IsString()
description?: string;
@IsNumber()
@Min(0.001)
quantity!: number;
@IsNumber()
@Min(0)
weight!: number;
@IsOptional()
@IsNumber()
@Min(0)
volume?: number;
@IsOptional()
@IsIn(['PENDING', 'LOADED', 'IN_TRANSIT', 'DELIVERED', 'UNLOADED'])
status?: string;
@IsOptional()
@IsDateString()
loadedAt?: string;
@IsOptional()
@IsDateString()
unloadedAt?: string;
}

View File

@@ -0,0 +1,7 @@
import { IsOptional, IsString } from 'class-validator';
export class DeliverCargoDto {
@IsOptional()
@IsString()
deliveryRemarks?: string;
}

View File

@@ -0,0 +1,20 @@
import { IsNumber, Min, IsOptional, IsString } from 'class-validator';
export class LoadCargoDto {
@IsNumber()
@Min(0.001)
quantity!: number;
@IsNumber()
@Min(0)
weight!: number;
@IsOptional()
@IsNumber()
@Min(0)
volume?: number;
@IsOptional()
@IsString()
description?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCargoDto } from './create-cargo.dto';
export class UpdateCargoDto extends PartialType(CreateCargoDto) {}

View File

@@ -0,0 +1,45 @@
// apps/edr-freight-api/src/modules/cargoes/entities/cargo.entity.ts
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { BaseEntity } from '@edr/api-common';
import { Container } from '../../container-management/entities/container.entity';
@Entity({ name: 'cargoes', schema: 'freight' })
export class Cargo extends BaseEntity {
@Column({ unique: true, name: 'cargo_reference' })
cargoReference!: string;
@Column({ name: 'shipment_id', type: 'uuid' })
shipmentId!: string;
@Column({ name: 'container_id', type: 'uuid' })
containerId!: string;
@Column({ name: 'cargo_type_id', type: 'uuid', nullable: true })
cargoTypeId!: string | null; // optional link to cargo_types table
@Column({ type: 'text', nullable: true })
description!: string | null;
@Column({ type: 'decimal', precision: 12, scale: 3 })
quantity!: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
weight!: number; // kg
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
volume!: number | null; // m³
@Column({ type: 'varchar', default: 'PENDING' })
status!: string; // PENDING, LOADED, IN_TRANSIT, DELIVERED, UNLOADED
@Column({ name: 'loaded_at', type: 'timestamp', nullable: true })
loadedAt!: Date | null;
@Column({ name: 'unloaded_at', type: 'timestamp', nullable: true })
unloadedAt!: Date | null;
// Relationship to Container
@ManyToOne(() => Container, (container) => container.cargoes, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'container_id' })
container!: Container;
}

View File

@@ -0,0 +1,63 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { ContainersService } from './containers.service';
@ApiTags('containers')
@Controller('containers')
export class ContainersController {
constructor(private readonly containersService: ContainersService) {}
@Post()
@ApiOperation({ summary: 'Create a new container' })
create(@Body() dto: CreateContainerDto) {
return this.containersService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List all containers' })
findAll() {
return this.containersService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a container by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.findById(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a container' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) {
return this.containersService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a container' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.remove(id);
}
@Post(':id/assign-wagon')
@ApiOperation({ summary: 'Assign container to a wagon' })
assignToWagon(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignContainerToWagonDto) {
return this.containersService.assignToWagon(id, dto);
}
@Post(':id/unassign-wagon')
@ApiOperation({ summary: 'Unassign container from wagon' })
unassignFromWagon(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.unassignFromWagon(id);
}
}

View File

@@ -0,0 +1,14 @@
// apps/edr-freight-api/src/modules/container-management/containers.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Container } from './entities/container.entity';
import { Wagon } from '../wagons/entities/wagon.entity';
import { ContainersController } from './containers.controller';
import { ContainersService } from './containers.service';
@Module({
imports: [TypeOrmModule.forFeature([Container, Wagon])], // ✅ add Wagon
controllers: [ContainersController],
providers: [ContainersService],
})
export class ContainersModule {}

View File

@@ -0,0 +1,15 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Container } from './entities/container.entity';
@Injectable()
export class ContainersRepository extends BaseRepository<Container> {
constructor(
@InjectRepository(Container)
repository: Repository<Container>,
) {
super(repository);
}
}

View File

@@ -0,0 +1,86 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { Container } from './entities/container.entity';
//import { ContainersRepository } from './containers.repository';
import { WagonsRepository } from '../wagons/wagons.repository';
@Injectable()
export class ContainersService {
constructor(
@InjectRepository(Container)
private readonly containerRepo: Repository<Container>,
private readonly wagonsRepository: WagonsRepository,
) {}
async create(dto: CreateContainerDto): Promise<Container> {
const container = this.containerRepo.create(dto);
// Convert undefined to null for optional fields
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async findAll(): Promise<Container[]> {
return this.containerRepo.find({ order: { containerNumber: 'ASC' } });
}
async findById(id: string): Promise<Container> {
const container = await this.containerRepo.findOne({ where: { id } });
if (!container) throw new NotFoundException(`Container ${id} not found`);
return container;
}
async update(id: string, dto: UpdateContainerDto): Promise<Container> {
const container = await this.findById(id);
Object.assign(container, dto);
// Convert undefined to null for nullable fields
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async remove(id: string): Promise<void> {
const container = await this.findById(id);
await this.containerRepo.remove(container);
}
async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot reassign a loaded container');
}
const wagon = await this.wagonsRepository.findById(dto.wagonId);
if (!wagon) throw new NotFoundException('Wagon not found');
let position: number | null = dto.position ?? null; // convert undefined to null
if (position === null) {
const maxPos = await this.containerRepo
.createQueryBuilder('c')
.select('MAX(c.position)', 'max')
.where('c.wagonId = :wagonId', { wagonId: wagon.id })
.getRawOne();
position = (maxPos?.max ?? 0) + 1;
}
container.wagonId = wagon.id;
container.position = position; // now position is number | null, safe
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
async unassignFromWagon(containerId: string): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot unassign a loaded container');
}
container.wagonId = null;
container.position = null;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
}

View File

@@ -0,0 +1,85 @@
// apps/edr-freight-api/src/modules/container-management/containers.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { Container } from './entities/container.entity';
import { Wagon } from '../wagons/entities/wagon.entity';
@Injectable()
export class ContainersService {
constructor(
@InjectRepository(Container)
private readonly containerRepo: Repository<Container>,
@InjectRepository(Wagon)
private readonly wagonRepo: Repository<Wagon>, // ✅ use raw repository
) {}
async create(dto: CreateContainerDto): Promise<Container> {
const container = this.containerRepo.create(dto);
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async findAll(): Promise<Container[]> {
return this.containerRepo.find({ order: { containerNumber: 'ASC' } });
}
async findById(id: string): Promise<Container> {
const container = await this.containerRepo.findOne({ where: { id } });
if (!container) throw new NotFoundException(`Container ${id} not found`);
return container;
}
async update(id: string, dto: UpdateContainerDto): Promise<Container> {
const container = await this.findById(id);
Object.assign(container, dto);
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async remove(id: string): Promise<void> {
const container = await this.findById(id);
await this.containerRepo.remove(container);
}
async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot reassign a loaded container');
}
const wagon = await this.wagonRepo.findOne({ where: { id: dto.wagonId } });
if (!wagon) throw new NotFoundException(`Wagon ${dto.wagonId} not found`);
let position: number | null = dto.position ?? null;
if (position === null) {
const maxPos = await this.containerRepo
.createQueryBuilder('c')
.select('MAX(c.position)', 'max')
.where('c.wagonId = :wagonId', { wagonId: wagon.id })
.getRawOne();
position = (maxPos?.max ?? 0) + 1;
}
container.wagonId = wagon.id;
container.position = position;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
async unassignFromWagon(containerId: string): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot unassign a loaded container');
}
container.wagonId = null;
container.position = null;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
}

View File

@@ -0,0 +1,11 @@
import { IsUUID, IsOptional, IsInt, Min } from 'class-validator';
export class AssignContainerToWagonDto {
@IsUUID()
wagonId!: string;
@IsOptional()
@IsInt()
@Min(1)
position?: number;
}

View File

@@ -0,0 +1,34 @@
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsIn } from 'class-validator';
export class CreateContainerDto {
@IsString()
containerNumber!: string;
@IsUUID()
containerTypeId!: string;
@IsOptional()
@IsUUID()
wagonId?: string;
@IsOptional()
@IsInt()
@Min(1)
position?: number;
@IsNumber()
@Min(0)
tareWeight!: number;
@IsNumber()
@Min(0)
maxGrossWeight!: number;
@IsOptional()
@IsString()
sealNumber?: string;
@IsOptional()
@IsIn(['AVAILABLE', 'LOADED', 'IN_TRANSIT', 'MAINTENANCE', 'DAMAGED'])
status?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContainerDto } from './create-container.dto';
export class UpdateContainerDto extends PartialType(CreateContainerDto) {}

View File

@@ -0,0 +1,45 @@
// apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts
import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { BaseEntity } from '@edr/api-common';
import { Wagon } from '../../wagons/entities/wagon.entity';
import { Cargo } from '../../cargoes/entities/cargoes.entity';
@Entity({ name: 'containers', schema: 'freight' })
export class Container extends BaseEntity {
@Column({ unique: true, name: 'container_number' })
containerNumber!: string;
@Column({ name: 'container_type_id', type: 'uuid' })
containerTypeId!: string;
@Column({ name: 'wagon_id', type: 'uuid', nullable: true })
wagonId!: string | null;
@Column({ type: 'int', nullable: true })
position!: number | null; // position on the wagon (1..N)
@Column({ name: 'tare_weight', type: 'decimal', precision: 10, scale: 2 })
tareWeight!: number;
@Column({ name: 'max_gross_weight', type: 'decimal', precision: 10, scale: 2 })
maxGrossWeight!: number;
@Column({
name: 'seal_number',
type: 'varchar',
nullable: true,
})
sealNumber!: string | null;
@Column({ type: 'varchar', default: 'AVAILABLE' })
status!: string; // AVAILABLE, LOADED, IN_TRANSIT, MAINTENANCE, DAMAGED
// Relationship to Wagon
@ManyToOne(() => Wagon, (wagon) => wagon.containers, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'wagon_id' })
wagon!: Wagon | null;
// Relationship to Cargo
@OneToMany(() => Cargo, (cargo) => cargo.container)
cargoes!: Cargo[];
}

View File

@@ -82,6 +82,17 @@ export class TrainSchedulingService {
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
const bookingRepository = this.dataSource.getRepository(Booking);
const queryBuilder = bookingRepository
<<<<<<< HEAD
.createQueryBuilder('booking')
.leftJoinAndSelect('booking.company', 'company')
.leftJoinAndSelect('booking.originYard', 'originYard')
.leftJoinAndSelect('booking.destinationYard', 'destinationYard')
.leftJoinAndSelect('booking.bookingContainers', 'bookingContainer')
.leftJoinAndSelect('bookingContainer.containerType', 'containerType')
.leftJoin(TrainScheduleBooking, 'scheduleBooking', 'scheduleBooking.booking_id = booking.id')
.where('booking.freightType = :freightType', { freightType: 'CONTAINER' })
.andWhere('scheduleBooking.id IS NULL');
=======
.createQueryBuilder("booking")
.leftJoinAndSelect("booking.customer", "customer")
.leftJoinAndSelect("booking.originYard", "originYard")
@@ -95,6 +106,7 @@ export class TrainSchedulingService {
)
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
.andWhere("scheduleBooking.id IS NULL");
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
if (query.originStationId) {
queryBuilder.andWhere("booking.originYardId = :originStationId", {
@@ -132,6 +144,13 @@ export class TrainSchedulingService {
const items: EligibleBookingItem[] = bookings.map((booking) => ({
id: booking.id,
reference: booking.reference,
<<<<<<< HEAD
customer: booking.company?.name ?? booking.company?.email ?? 'Unknown customer',
containerType: booking.bookingContainers
?.map((container) => container.containerType?.label ?? container.containerType?.code ?? 'Container')
.join(', ') ?? 'Container',
quantity: booking.bookingContainers?.reduce((sum, container) => sum + Number(container.quantity ?? 0), 0) ?? 0,
=======
customer:
booking.company?.name ?? booking.company?.email ?? "Unknown customer",
containerType:
@@ -148,6 +167,7 @@ export class TrainSchedulingService {
(sum, container) => sum + Number(container.quantity ?? 0),
0,
) ?? 0,
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
weightTons: this.roundTons(booking.cargoTotalWeightVgm),
origin:
booking.originYard?.label ??
@@ -654,6 +674,17 @@ export class TrainSchedulingService {
}
async getContainerTrainScheduleById(id: string) {
<<<<<<< HEAD
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
where: { id },
relations: {
trainSet: { locomotive: true, wagons: { wagonType: true, allocations: { booking: true } } },
originStation: true,
destinationStation: true,
scheduleBookings: { booking: { company: true, originYard: true, destinationYard: true } },
},
});
=======
const schedule = await this.dataSource
.getRepository(TrainSchedule)
.findOne({
@@ -670,6 +701,7 @@ export class TrainSchedulingService {
},
},
});
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
if (!schedule) {
throw new NotFoundException(`Train schedule ${id} not found`);

View File

@@ -1,5 +1,5 @@
import { Freight } from "@edr/types";
import { IsEnum, IsNumber, IsOptional, IsString, Min } from "class-validator";
import { IsString, IsNumber, IsOptional, IsUUID, IsDateString, Min, IsEnum } from 'class-validator';
import { Freight } from '@edr/types';
export class CreateTrainDto {
@IsString()
@@ -11,9 +11,45 @@ export class CreateTrainDto {
@IsOptional()
@IsEnum(Freight.TrainStatus)
status?: Freight.TrainStatus;
status?: Freight.TrainStatus; // ✅ uses enum, not string
@IsOptional()
@IsString()
notes?: string;
}
@IsOptional()
@IsString()
trainNumber?: string;
@IsOptional()
@IsString()
trainName?: string;
@IsOptional()
@IsUUID()
routeId?: string;
@IsOptional()
@IsUUID()
originStationId?: string;
@IsOptional()
@IsUUID()
destinationStationId?: string;
@IsOptional()
@IsDateString()
departureTime?: string;
@IsOptional()
@IsDateString()
arrivalTime?: string;
@IsOptional()
@IsString()
locomotiveNumber?: string;
@IsOptional()
@IsString()
remarks?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateTrainDto } from './create-train.dto';
export class UpdateTrainDto extends PartialType(CreateTrainDto) {}

View File

@@ -1,23 +1,58 @@
import { BaseEntity } from "@edr/api-common";
import { Freight } from "@edr/types";
import { Column, Entity } from "typeorm";
// apps/edr-freight-api/src/modules/trains/entities/train.entity.ts
import { BaseEntity } from '@edr/api-common';
import { Freight } from '@edr/types';
import { Column, Entity, OneToMany } from 'typeorm';
import { Wagon } from '../../wagons/entities/wagon.entity';
@Entity({ schema:"freight",name: "trains" })
@Entity({ schema: 'freight', name: 'trains' })
export class Train extends BaseEntity {
@Column({ name: "code", type: "varchar", length: 32, unique: true })
// --- existing fields (keep for backward compatibility) ---
@Column({ name: 'code', type: 'varchar', length: 32, unique: true })
code!: string;
@Column({ name: "capacity_tons", type: "numeric", precision: 10, scale: 2 })
@Column({ name: 'capacity_tons', type: 'numeric', precision: 10, scale: 2 })
capacityTons!: number;
@Column({
name: "status",
type: "enum",
name: 'status',
type: 'enum',
enum: Freight.TrainStatus,
default: Freight.TrainStatus.Available,
})
status!: Freight.TrainStatus;
@Column({ name: "notes", type: "text", nullable: true })
@Column({ name: 'notes', type: 'text', nullable: true })
notes?: string | null;
}
// --- new required fields ---
@Column({ name: 'train_number', type: 'varchar', length: 20, unique: true, nullable: true })
trainNumber?: string;
@Column({ name: 'train_name', type: 'varchar', length: 100, nullable: true })
trainName?: string;
@Column({ name: 'route_id', type: 'uuid', nullable: true })
routeId?: string;
@Column({ name: 'origin_station_id', type: 'uuid', nullable: true })
originStationId?: string;
@Column({ name: 'destination_station_id', type: 'uuid', nullable: true })
destinationStationId?: string;
@Column({ name: 'departure_time', type: 'timestamp', nullable: true })
departureTime?: Date;
@Column({ name: 'arrival_time', type: 'timestamp', nullable: true })
arrivalTime?: Date;
@Column({ name: 'locomotive_number', type: 'varchar', length: 50, nullable: true })
locomotiveNumber?: string;
@Column({ name: 'remarks', type: 'text', nullable: true })
remarks?: string;
// --- relationships ---
@OneToMany(() => Wagon, (wagon) => wagon.train)
wagons!: Wagon[]; // fixed typo: was 'wagens'
}

View File

@@ -1,15 +1,14 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Train } from "./entities/train.entity";
import { TrainsController } from "./trains.controller";
import { TrainsRepository } from "./trains.repository";
import { TrainsService } from "./trains.service";
// apps/edr-freight-api/src/modules/trains/trains.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Train } from './entities/train.entity';
import { TrainsController } from './trains.controller';
import { TrainsService } from './trains.service';
@Module({
imports: [TypeOrmModule.forFeature([Train])],
controllers: [TrainsController],
providers: [TrainsService, TrainsRepository],
exports: [TrainsService],
providers: [TrainsService],
exports: [TrainsService], // if other modules need it
})
export class TrainsModule {}
export class TrainsModule {}

View File

@@ -1,29 +1,41 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { CreateTrainDto } from "./dto/create-train.dto";
import { Train } from "./entities/train.entity";
import { TrainsRepository } from "./trains.repository";
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTrainDto } from './dto/create-train.dto';
import { UpdateTrainDto } from './dto/update-train.dto';
import { Train } from './entities/train.entity';
@Injectable()
export class TrainsService {
constructor(private readonly trainsRepository: TrainsRepository) {}
constructor(
@InjectRepository(Train)
private readonly trainRepo: Repository<Train>,
) {}
/** Register a new train in the fleet. */
create(dto: CreateTrainDto): Promise<Train> {
return this.trainsRepository.create(dto);
const train = this.trainRepo.create(dto);
return this.trainRepo.save(train);
}
/** List every active train. */
findAll(): Promise<Train[]> {
return this.trainsRepository.findAll({ order: { code: "ASC" } });
return this.trainRepo.find({ order: { code: 'ASC' } });
}
/** Get a single train by ID. */
async findById(id: string): Promise<Train> {
const train = await this.trainsRepository.findById(id);
if (!train) {
throw new NotFoundException(`Train ${id} not found`);
}
const train = await this.trainRepo.findOne({ where: { id } });
if (!train) throw new NotFoundException(`Train ${id} not found`);
return train;
}
}
async update(id: string, dto: UpdateTrainDto): Promise<Train> {
const train = await this.findById(id);
Object.assign(train, dto);
// Convert undefined to null for optional fields if needed
return this.trainRepo.save(train);
}
async remove(id: string): Promise<void> {
const train = await this.findById(id);
await this.trainRepo.remove(train);
}
}

View File

@@ -0,0 +1,11 @@
import { IsUUID, IsOptional, IsInt, Min } from 'class-validator';
export class AssignWagonToTrainDto {
@IsUUID()
trainId!: string;
@IsOptional()
@IsInt()
@Min(1)
sequenceNumber?: number;
}

View File

@@ -0,0 +1,34 @@
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsIn } from 'class-validator';
export class CreateWagonDto {
@IsString()
wagonNumber!: string;
@IsUUID()
wagonTypeId!: string;
@IsOptional()
@IsUUID()
trainId?: string;
@IsOptional()
@IsInt()
@Min(1)
sequenceNumber?: number;
@IsNumber()
@Min(0)
tareWeight!: number;
@IsNumber()
@Min(0)
maxPayloadWeight!: number;
@IsOptional()
@IsIn(['AVAILABLE', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
status?: string;
@IsOptional()
@IsString()
notes?: string;
}

View File

@@ -0,0 +1,7 @@
import { IsArray, IsUUID } from 'class-validator';
export class ReorderWagonsDto {
@IsArray()
@IsUUID(4, { each: true })
wagonIds!: string[];
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateWagonDto } from './create-wagon.dto';
export class UpdateWagonDto extends PartialType(CreateWagonDto) {}

View File

@@ -0,0 +1,41 @@
// apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts
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';
@Entity({ name: 'wagons', schema: 'freight' })
export class Wagon extends BaseEntity {
@Column({ unique: true, name: 'wagon_number' })
wagonNumber!: string;
@Column({ name: 'wagon_type_id', type: 'uuid' })
wagonTypeId!: string;
@Column({ name: 'train_id', type: 'uuid', nullable: true })
trainId!: string | null;
@Column({ name: 'sequence_number', type: 'int', nullable: true })
sequenceNumber!: number | 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, ASSIGNED, MAINTENANCE, RETIRED
@Column({ type: 'text', nullable: true })
notes!: string | null;
// Relationship to Train
@ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'train_id' })
train!: Train | null;
// Relationship to Container
@OneToMany(() => Container, (container) => container.wagon)
containers!: Container[];
}

View File

@@ -0,0 +1,76 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateWagonDto } from './dto/create-wagon.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 { WagonsService } from './wagons.service';
@ApiTags('wagons')
@Controller('wagons')
export class WagonsController {
constructor(private readonly wagonsService: WagonsService) {}
@Post()
@ApiOperation({ summary: 'Create a new wagon' })
create(@Body() dto: CreateWagonDto) {
return this.wagonsService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List all wagons' })
findAll() {
return this.wagonsService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a wagon by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.findById(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a wagon' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) {
return this.wagonsService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a wagon' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.remove(id);
}
@Post(':id/assign-train')
@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')
@ApiOperation({ summary: 'Unassign wagon from train' })
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.unassignFromTrain(id);
}
}
// Separate controller for trainspecific reorder (registered in module)
@Controller('trains/:trainId/reorder-wagons')
export class TrainWagonsReorderController {
constructor(private readonly wagonsService: WagonsService) {}
@Post()
@ApiOperation({ summary: 'Reorder wagons of a train' })
reorder(@Param('trainId', ParseUUIDPipe) trainId: string, @Body() dto: ReorderWagonsDto) {
return this.wagonsService.reorderWagons(trainId, dto);
}
}

View File

@@ -0,0 +1,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 { WagonsController, TrainWagonsReorderController } from './wagons.controller';
import { WagonsService } from './wagons.service';
@Module({
imports: [TypeOrmModule.forFeature([Wagon, Train])],
controllers: [WagonsController, TrainWagonsReorderController],
providers: [WagonsService],
exports: [WagonsService],
})
export class WagonsModule {}

View File

@@ -0,0 +1,15 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Wagon } from './entities/wagon.entity';
@Injectable()
export class WagonsRepository extends BaseRepository<Wagon> {
constructor(
@InjectRepository(Wagon)
repository: Repository<Wagon>,
) {
super(repository);
}
}

View File

@@ -0,0 +1,101 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreateWagonDto } from './dto/create-wagon.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';
@Injectable()
export class WagonsService {
constructor(
@InjectRepository(Wagon)
private readonly wagonRepo: Repository<Wagon>,
@InjectRepository(Train)
private readonly trainRepo: Repository<Train>,
private readonly dataSource: DataSource,
) {}
async create(dto: CreateWagonDto): Promise<Wagon> {
const wagon = this.wagonRepo.create(dto);
// Convert undefined to null for nullable fields
if (dto.trainId === undefined) wagon.trainId = null;
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
return this.wagonRepo.save(wagon);
}
async findAll(): Promise<Wagon[]> {
return this.wagonRepo.find({ order: { wagonNumber: 'ASC' } });
}
async findById(id: string): Promise<Wagon> {
const wagon = await this.wagonRepo.findOne({ where: { id } });
if (!wagon) throw new NotFoundException(`Wagon ${id} not found`);
return wagon;
}
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
const wagon = await this.findById(id);
Object.assign(wagon, dto);
if (dto.trainId === undefined) wagon.trainId = null;
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
return this.wagonRepo.save(wagon);
}
async remove(id: string): Promise<void> {
const wagon = await this.findById(id);
await this.wagonRepo.remove(wagon);
}
async assignToTrain(wagonId: string, dto: AssignWagonToTrainDto): Promise<Wagon> {
const wagon = await this.findById(wagonId);
if (wagon.status === 'ASSIGNED') {
throw new ConflictException('Wagon already assigned to a train');
}
const train = await this.trainRepo.findOne({ where: { id: dto.trainId } });
if (!train) throw new NotFoundException('Train not found');
let sequence: number | null = dto.sequenceNumber ?? null;
if (sequence === null) {
const maxSeq = await this.wagonRepo
.createQueryBuilder('w')
.select('MAX(w.sequenceNumber)', 'max')
.where('w.trainId = :trainId', { trainId: train.id })
.getRawOne();
sequence = (maxSeq?.max ?? 0) + 1;
}
wagon.trainId = train.id;
wagon.sequenceNumber = sequence;
wagon.status = 'ASSIGNED';
return this.wagonRepo.save(wagon);
}
async unassignFromTrain(wagonId: string): Promise<Wagon> {
const wagon = await this.findById(wagonId);
wagon.trainId = null;
wagon.sequenceNumber = null;
wagon.status = 'AVAILABLE';
return this.wagonRepo.save(wagon);
}
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
for (let i = 0; i < dto.wagonIds.length; i++) {
await queryRunner.manager.update(Wagon, dto.wagonIds[i], { sequenceNumber: i + 1 });
}
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}