From 71ac89edc78628909d4f3a9e03f942d1a73dafac Mon Sep 17 00:00:00 2001 From: hagiye Date: Thu, 4 Jun 2026 15:56:29 +0300 Subject: [PATCH] trains, wagons,containers and cargoes schema and API --- apps/edr-freight-api/package.json | 4 +- apps/edr-freight-api/src/app.module.ts | 13 ++- apps/edr-freight-api/src/data-source.ts | 20 ++++ ...8900000000-MoveCustomersToFreightSchema.ts | 8 +- .../src/modules/cargoes/cargoes.controller.ts | 70 ++++++++++++ .../src/modules/cargoes/cargoes.module.ts | 14 +++ .../src/modules/cargoes/cargoes.repository.ts | 15 +++ .../src/modules/cargoes/cargoes.service.ts | 104 ++++++++++++++++++ .../modules/cargoes/dto/create-cargo.dto.ts | 45 ++++++++ .../modules/cargoes/dto/deliver-cargo.dto.ts | 7 ++ .../src/modules/cargoes/dto/load-cargo.dto.ts | 20 ++++ .../modules/cargoes/dto/unload-cargo.dto.ts | 0 .../modules/cargoes/dto/update-cargo.dto.ts | 4 + .../cargoes/entities/cargoes.entity.ts | 45 ++++++++ .../containers.controller.ts | 63 +++++++++++ .../container-management/containers.module.ts | 14 +++ .../containers.repository.ts | 15 +++ .../containers.service copy.ts | 86 +++++++++++++++ .../containers.service.ts | 85 ++++++++++++++ .../dto/assign-container-to-wagon.dto.ts | 11 ++ .../dto/create-container.dto.ts | 34 ++++++ .../dto/update-container.dto.ts | 4 + .../entities/container.entity.ts | 45 ++++++++ .../modules/trains/dto/create-train.dto.ts | 44 +++++++- .../modules/trains/dto/update-train.dto.ts | 4 + .../modules/trains/entities/train.entity.ts | 55 +++++++-- .../src/modules/trains/trains.module.ts | 19 ++-- .../src/modules/trains/trains.service.ts | 44 +++++--- .../wagons/dto/assign-wagon-to-train.dto.ts | 11 ++ .../modules/wagons/dto/create-wagon.dto.ts | 34 ++++++ .../modules/wagons/dto/reorder-wagons.dto.ts | 7 ++ .../modules/wagons/dto/update-wagon.dto.ts | 4 + .../modules/wagons/entities/wagon.entity.ts | 41 +++++++ .../src/modules/wagons/wagons.controller.ts | 76 +++++++++++++ .../src/modules/wagons/wagons.module.ts | 14 +++ .../src/modules/wagons/wagons.repository.ts | 15 +++ .../src/modules/wagons/wagons.service.ts | 101 +++++++++++++++++ .../src/components/cargoes/CargoesTable.tsx | 44 ++++++++ .../components/cargoes/LoadCargoDialog.tsx | 38 +++++++ .../AssignContainerDialog.tsx | 41 +++++++ .../container_management/ContainersTable.tsx | 40 +++++++ .../src/components/trains/TrainDetailCard.tsx | 19 ++++ .../src/components/trains/TrainFormDialog.tsx | 59 ++++++++++ .../src/components/trains/TrainsTable.tsx | 45 ++++++++ .../components/wagons/AssignWagonDialog.tsx | 52 +++++++++ .../src/components/wagons/WagonFormDialog.tsx | 71 ++++++++++++ .../src/components/wagons/WagonsTable.tsx | 63 +++++++++++ .../backoffice/src/hooks/useCargoes.ts | 39 +++++++ .../backoffice/src/hooks/useContainers.ts | 31 ++++++ .../backoffice/src/hooks/useTrains.ts | 35 ++++++ .../backoffice/src/hooks/useWagons.ts | 41 +++++++ .../admin/rateMatrix/RateMatrixApproval.tsx | 8 +- .../rateMatrix/RateMatrixRegistration.tsx | 2 +- .../src/pages/cargoes/CargoesPage.tsx | 0 .../containers_management/ContainersPage.tsx | 29 +++++ .../src/pages/trains/TrainDetailPage.tsx | 33 ++++++ .../src/pages/trains/TrainsPage.tsx | 80 ++++++++++++-- .../src/pages/wagons/WagonsPage.tsx | 29 +++++ .../backoffice/src/services/cargo.servcie.ts | 26 +++++ .../src/services/containerService.ts | 21 ++++ .../backoffice/src/services/trains.service.ts | 26 +++++ .../backoffice/src/services/wagon.service.ts | 26 +++++ apps/edr-freight-web/backoffice/tsconfig.json | 14 +-- ...s.timestamp-1780409607954-40aaca5c64a3.mjs | 24 ++++ package.json | 14 ++- pnpm-lock.yaml | 22 ++-- 66 files changed, 2077 insertions(+), 90 deletions(-) create mode 100644 apps/edr-freight-api/src/data-source.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/cargoes.module.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/cargoes.repository.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/dto/create-cargo.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/dto/load-cargo.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/dto/unload-cargo.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/dto/update-cargo.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/containers.controller.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/containers.module.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/containers.repository.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/containers.service copy.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/containers.service.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/dto/assign-container-to-wagon.dto.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/dto/create-container.dto.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/dto/update-container.dto.ts create mode 100644 apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts create mode 100644 apps/edr-freight-api/src/modules/trains/dto/update-train.dto.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/dto/assign-wagon-to-train.dto.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/dto/create-wagon.dto.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/dto/reorder-wagons.dto.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/dto/update-wagon.dto.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/wagons.controller.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/wagons.module.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/wagons.repository.ts create mode 100644 apps/edr-freight-api/src/modules/wagons/wagons.service.ts create mode 100644 apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/container_management/AssignContainerDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/container_management/ContainersTable.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trains/TrainDetailCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trains/TrainFormDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trains/TrainsTable.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/wagons/WagonFormDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useContainers.ts create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useTrains.ts create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useWagons.ts create mode 100644 apps/edr-freight-web/backoffice/src/pages/cargoes/CargoesPage.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/containers_management/ContainersPage.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/wagons/WagonsPage.tsx create mode 100644 apps/edr-freight-web/backoffice/src/services/cargo.servcie.ts create mode 100644 apps/edr-freight-web/backoffice/src/services/containerService.ts create mode 100644 apps/edr-freight-web/backoffice/src/services/trains.service.ts create mode 100644 apps/edr-freight-web/backoffice/src/services/wagon.service.ts create mode 100644 apps/edr-freight-web/portal/vite.config.ts.timestamp-1780409607954-40aaca5c64a3.mjs diff --git a/apps/edr-freight-api/package.json b/apps/edr-freight-api/package.json index 5114e20da..643717ced 100644 --- a/apps/edr-freight-api/package.json +++ b/apps/edr-freight-api/package.json @@ -34,8 +34,7 @@ "minio": "7.1.3", "pg": "^8.13.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "rxjs": "^7.8.1" }, "devDependencies": { "@edr/api-common": "workspace:*", @@ -57,6 +56,7 @@ "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", + "typeorm": "^1.0.0", "typescript": "^5.5.4" }, "jest": { diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 0fcc7a546..454e266ca 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -12,7 +12,7 @@ import databaseConfig from "./config/database.config"; import { BookingsModule } from "./modules/bookings/bookings.module"; import { FilesModule } from "./modules/files/files.module"; import { ConsignmentsModule } from "./modules/consignments/consignments.module"; -import { TrainsModule } from "./modules/trains/trains.module"; + import { CustomersModule } from "./modules/customers/customers.module"; import { TrackingModule } from "./modules/tracking/tracking.module"; import { BillingModule } from "./modules/billing/billing.module"; @@ -29,6 +29,12 @@ import { } from "./seed/edr-freight.seed"; import { EdrOrgSeeder } from "./seed/edr-org.seeder"; import { DemoUsersSeeder } from "./seed/demo-users.seeder"; +//New Trains, Wangons, Container and Cargo management modules +import { TrainsModule } from "./modules/trains/trains.module"; +import { WagonsModule } from './modules/wagons/wagons.module'; +import { ContainersModule } from './modules/container-management/containers.module'; +import { CargoesModule } from './modules/cargoes/cargoes.module'; + @Module({ imports: [ @@ -68,6 +74,11 @@ import { DemoUsersSeeder } from "./seed/demo-users.seeder"; RuleEngineModule, BackofficeModule, DemoPermissionsModule, + //New Modules + TrainsModule, + WagonsModule, + ContainersModule, + CargoesModule, ], providers: [EdrOrgSeeder, DemoUsersSeeder], }) diff --git a/apps/edr-freight-api/src/data-source.ts b/apps/edr-freight-api/src/data-source.ts new file mode 100644 index 000000000..b35d79932 --- /dev/null +++ b/apps/edr-freight-api/src/data-source.ts @@ -0,0 +1,20 @@ +// apps/edr-freight-api/src/data-source.ts +import { DataSource } from 'typeorm'; +//import { ensurePostgresSchemas } from './utils/ensure-postgres-schemas'; // adjust path if needed + +export const AppDataSource = new DataSource({ + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'postgres', + password: '', // Laragon default: empty + database: 'edr_freight', + schema: 'freight', // default schema for entities without an explicit schema + entities: [__dirname + '/**/*.entity{.ts,.js}'], + migrations: [__dirname + '/migrations/*{.ts,.js}'], + synchronize: false, + logging: true, +}); + +// Optional: call ensurePostgresSchemas before initializing +// But you can also run it separately. \ No newline at end of file diff --git a/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts b/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts index 43bb0eb02..36e848e64 100644 --- a/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts +++ b/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts @@ -43,10 +43,10 @@ export class MoveCustomersToFreightSchema1748900000000 implements MigrationInter CREATE INDEX IF NOT EXISTS "IDX_freight_customers_email" ON freight.customers (email); `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_freight_customers_user_id" - ON freight.customers (user_id); - `); + // await queryRunner.query(` + // CREATE INDEX IF NOT EXISTS "IDX_freight_customers_user_id" + // ON freight.customers (user_id); + //`); // Copy rows from public.customers when that legacy table exists await queryRunner.query(` diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts new file mode 100644 index 000000000..5d86a579b --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts @@ -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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.module.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.module.ts new file mode 100644 index 000000000..8a60d2600 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.module.ts @@ -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 {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.repository.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.repository.ts new file mode 100644 index 000000000..cedb217da --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.repository.ts @@ -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 { + constructor( + @InjectRepository(Cargo) + repository: Repository, + ) { + super(repository); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts new file mode 100644 index 000000000..c1a6e213e --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts @@ -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, + @InjectRepository(Container) + private readonly containerRepo: Repository, + ) {} + + async create(dto: CreateCargoDto): Promise { + const cargo = this.cargoRepo.create(dto); + return this.cargoRepo.save(cargo); + } + + async findAll(): Promise { + return this.cargoRepo.find({ order: { cargoReference: 'ASC' } }); + } + + async findById(id: string): Promise { + 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 { + const cargo = await this.findById(id); + Object.assign(cargo, dto); + return this.cargoRepo.save(cargo); + } + + async remove(id: string): Promise { + const cargo = await this.findById(id); + await this.cargoRepo.remove(cargo); + } + + async loadCargo(id: string, dto: LoadCargoDto): Promise { + 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 { + 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 { + 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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/create-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/create-cargo.dto.ts new file mode 100644 index 000000000..8373f5a4b --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/dto/create-cargo.dto.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts new file mode 100644 index 000000000..020e4d630 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class DeliverCargoDto { + @IsOptional() + @IsString() + deliveryRemarks?: string; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/load-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/load-cargo.dto.ts new file mode 100644 index 000000000..9e8573751 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/dto/load-cargo.dto.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/unload-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/unload-cargo.dto.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/update-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/update-cargo.dto.ts new file mode 100644 index 000000000..7596b7dbe --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/dto/update-cargo.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCargoDto } from './create-cargo.dto'; + +export class UpdateCargoDto extends PartialType(CreateCargoDto) {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts b/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts new file mode 100644 index 000000000..7c2f752e5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts new file mode 100644 index 000000000..78759d9b4 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts @@ -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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/containers.module.ts b/apps/edr-freight-api/src/modules/container-management/containers.module.ts new file mode 100644 index 000000000..eea118187 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/containers.module.ts @@ -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 {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/containers.repository.ts b/apps/edr-freight-api/src/modules/container-management/containers.repository.ts new file mode 100644 index 000000000..c6842cdea --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/containers.repository.ts @@ -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 { + constructor( + @InjectRepository(Container) + repository: Repository, + ) { + super(repository); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/containers.service copy.ts b/apps/edr-freight-api/src/modules/container-management/containers.service copy.ts new file mode 100644 index 000000000..f6094a497 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/containers.service copy.ts @@ -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, + private readonly wagonsRepository: WagonsRepository, + ) {} + + async create(dto: CreateContainerDto): Promise { + 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 { + return this.containerRepo.find({ order: { containerNumber: 'ASC' } }); + } + + async findById(id: string): Promise { + 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 { + 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 { + const container = await this.findById(id); + await this.containerRepo.remove(container); + } + + async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise { + 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 { + 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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/containers.service.ts b/apps/edr-freight-api/src/modules/container-management/containers.service.ts new file mode 100644 index 000000000..39f2d8274 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/containers.service.ts @@ -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, + @InjectRepository(Wagon) + private readonly wagonRepo: Repository, // ✅ use raw repository + ) {} + + async create(dto: CreateContainerDto): Promise { + 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 { + return this.containerRepo.find({ order: { containerNumber: 'ASC' } }); + } + + async findById(id: string): Promise { + 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 { + 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 { + const container = await this.findById(id); + await this.containerRepo.remove(container); + } + + async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise { + 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 { + 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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/dto/assign-container-to-wagon.dto.ts b/apps/edr-freight-api/src/modules/container-management/dto/assign-container-to-wagon.dto.ts new file mode 100644 index 000000000..3b7be1d9e --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/dto/assign-container-to-wagon.dto.ts @@ -0,0 +1,11 @@ +import { IsUUID, IsOptional, IsInt, Min } from 'class-validator'; + +export class AssignContainerToWagonDto { + @IsUUID() + wagonId!: string; + + @IsOptional() + @IsInt() + @Min(1) + position?: number; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/dto/create-container.dto.ts b/apps/edr-freight-api/src/modules/container-management/dto/create-container.dto.ts new file mode 100644 index 000000000..1efed1cc9 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/dto/create-container.dto.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/dto/update-container.dto.ts b/apps/edr-freight-api/src/modules/container-management/dto/update-container.dto.ts new file mode 100644 index 000000000..7391bc642 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/dto/update-container.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateContainerDto } from './create-container.dto'; + +export class UpdateContainerDto extends PartialType(CreateContainerDto) {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts b/apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts new file mode 100644 index 000000000..a5c7ee9c1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts @@ -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[]; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/trains/dto/create-train.dto.ts b/apps/edr-freight-api/src/modules/trains/dto/create-train.dto.ts index f41dfa275..f166254a9 100644 --- a/apps/edr-freight-api/src/modules/trains/dto/create-train.dto.ts +++ b/apps/edr-freight-api/src/modules/trains/dto/create-train.dto.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/trains/dto/update-train.dto.ts b/apps/edr-freight-api/src/modules/trains/dto/update-train.dto.ts new file mode 100644 index 000000000..cbd36eed9 --- /dev/null +++ b/apps/edr-freight-api/src/modules/trains/dto/update-train.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTrainDto } from './create-train.dto'; + +export class UpdateTrainDto extends PartialType(CreateTrainDto) {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/trains/entities/train.entity.ts b/apps/edr-freight-api/src/modules/trains/entities/train.entity.ts index c12478ec2..184b9c88d 100644 --- a/apps/edr-freight-api/src/modules/trains/entities/train.entity.ts +++ b/apps/edr-freight-api/src/modules/trains/entities/train.entity.ts @@ -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' +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/trains/trains.module.ts b/apps/edr-freight-api/src/modules/trains/trains.module.ts index 094120f33..61098ff40 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.module.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.module.ts @@ -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 {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/trains/trains.service.ts b/apps/edr-freight-api/src/modules/trains/trains.service.ts index db689a19a..12aa65afe 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.service.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.service.ts @@ -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, + ) {} - /** Register a new train in the fleet. */ create(dto: CreateTrainDto): Promise { - return this.trainsRepository.create(dto); + const train = this.trainRepo.create(dto); + return this.trainRepo.save(train); } - /** List every active train. */ findAll(): Promise { - 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 { - 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 { + 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 { + const train = await this.findById(id); + await this.trainRepo.remove(train); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/dto/assign-wagon-to-train.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/assign-wagon-to-train.dto.ts new file mode 100644 index 000000000..66a837e68 --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/dto/assign-wagon-to-train.dto.ts @@ -0,0 +1,11 @@ +import { IsUUID, IsOptional, IsInt, Min } from 'class-validator'; + +export class AssignWagonToTrainDto { + @IsUUID() + trainId!: string; + + @IsOptional() + @IsInt() + @Min(1) + sequenceNumber?: number; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/dto/create-wagon.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/create-wagon.dto.ts new file mode 100644 index 000000000..c3108d68b --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/dto/create-wagon.dto.ts @@ -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; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/dto/reorder-wagons.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/reorder-wagons.dto.ts new file mode 100644 index 000000000..0395adb8f --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/dto/reorder-wagons.dto.ts @@ -0,0 +1,7 @@ +import { IsArray, IsUUID } from 'class-validator'; + +export class ReorderWagonsDto { + @IsArray() + @IsUUID(4, { each: true }) + wagonIds!: string[]; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/dto/update-wagon.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/update-wagon.dto.ts new file mode 100644 index 000000000..3414d1f2c --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/dto/update-wagon.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateWagonDto } from './create-wagon.dto'; + +export class UpdateWagonDto extends PartialType(CreateWagonDto) {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts b/apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts new file mode 100644 index 000000000..cdff14330 --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/entities/wagon.entity.ts @@ -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[]; +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts new file mode 100644 index 000000000..339948eec --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts @@ -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 train‑specific 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); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.module.ts b/apps/edr-freight-api/src/modules/wagons/wagons.module.ts new file mode 100644 index 000000000..914de4cbd --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/wagons.module.ts @@ -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 {} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.repository.ts b/apps/edr-freight-api/src/modules/wagons/wagons.repository.ts new file mode 100644 index 000000000..f0e12842e --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/wagons.repository.ts @@ -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 { + constructor( + @InjectRepository(Wagon) + repository: Repository, + ) { + super(repository); + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.service.ts b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts new file mode 100644 index 000000000..70909cbd8 --- /dev/null +++ b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts @@ -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, + @InjectRepository(Train) + private readonly trainRepo: Repository, + private readonly dataSource: DataSource, + ) {} + + async create(dto: CreateWagonDto): Promise { + 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 { + return this.wagonRepo.find({ order: { wagonNumber: 'ASC' } }); + } + + async findById(id: string): Promise { + 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 { + 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 { + const wagon = await this.findById(id); + await this.wagonRepo.remove(wagon); + } + + async assignToTrain(wagonId: string, dto: AssignWagonToTrainDto): Promise { + 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 { + 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 { + 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(); + } + } +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx new file mode 100644 index 000000000..599aa96f4 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx @@ -0,0 +1,44 @@ +import { useCargoesByContainer, useDeliverCargo, useUnloadCargo } from '@/hooks/useCargoes'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { LoadCargoDialog } from './LoadCargoDialog'; + +export function CargoesTable({ containerId }: { containerId: string }) { + const { data: cargoes, refetch } = useCargoesByContainer(containerId); + const deliver = useDeliverCargo(); + const unload = useUnloadCargo(); + + if (!cargoes?.length) return
No cargoes for this container.
; + + return ( + + + + Reference + Description + Quantity + Weight (kg) + Status + Actions + + + + {cargoes.map(cargo => ( + + {cargo.cargoReference} + {cargo.description || '-'} + {cargo.quantity} + {cargo.weight} + {cargo.status} + + {cargo.status === 'PENDING' && refetch()} />} + {cargo.status === 'LOADED' && } + {cargo.status === 'LOADED' && } + + + ))} + +
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx new file mode 100644 index 000000000..188726352 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useLoadCargo } from '@/hooks/useCargoes'; +import { useToast } from '@/hooks/use-toast'; + +export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) { + const [open, setOpen] = useState(false); + const [quantity, setQuantity] = useState(0); + const [weight, setWeight] = useState(0); + const [volume, setVolume] = useState(); + const load = useLoadCargo(); + const { toast } = useToast(); + + const handleLoad = async () => { + await load.mutateAsync({ id: cargoId, quantity, weight, volume }); + toast({ title: 'Loaded', description: 'Cargo loaded into container.' }); + setOpen(false); + onSuccess?.(); + }; + + return ( + + + + Load Cargo +
+
setQuantity(parseFloat(e.target.value))} />
+
setWeight(parseFloat(e.target.value))} />
+
setVolume(parseFloat(e.target.value) || undefined)} />
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/container_management/AssignContainerDialog.tsx b/apps/edr-freight-web/backoffice/src/components/container_management/AssignContainerDialog.tsx new file mode 100644 index 000000000..d10955933 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/container_management/AssignContainerDialog.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useContainers, useAssignContainerToWagon } from '@/hooks/useContainers'; +import { useToast } from '@/hooks/use-toast'; +import { Plus } from 'lucide-react'; + +export function AssignContainerDialog({ wagonId }: { wagonId: string }) { + const [open, setOpen] = useState(false); + const [containerId, setContainerId] = useState(''); + const [position, setPosition] = useState(); + const { data: containers } = useContainers(); + const assign = useAssignContainerToWagon(); + const { toast } = useToast(); + + const available = containers?.filter(c => c.status === 'AVAILABLE' && !c.wagonId); + + const handleAssign = async () => { + if (!containerId) return; + await assign.mutateAsync({ containerId, wagonId, position }); + toast({ title: 'Assigned', description: 'Container placed on wagon.' }); + setOpen(false); + }; + + return ( + + + + Assign Container to Wagon +
+
+
setPosition(parseInt(e.target.value) || undefined)} />
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/container_management/ContainersTable.tsx b/apps/edr-freight-web/backoffice/src/components/container_management/ContainersTable.tsx new file mode 100644 index 000000000..703bc0210 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/container_management/ContainersTable.tsx @@ -0,0 +1,40 @@ +import { useContainersByWagon, useUnassignContainer } from '@/hooks/useContainers'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Trash2 } from 'lucide-react'; + +export function ContainersTable({ wagonId }: { wagonId: string }) { + const { data: containers, refetch } = useContainersByWagon(wagonId); + const unassign = useUnassignContainer(); + + if (!containers?.length) return
No containers assigned.
; + + return ( + + + + Number + Type + Position + Status + Actions + + + + {containers.map(container => ( + + {container.containerNumber} + {container.containerTypeId} + {container.position} + {container.status} + + + + + ))} + +
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/trains/TrainDetailCard.tsx b/apps/edr-freight-web/backoffice/src/components/trains/TrainDetailCard.tsx new file mode 100644 index 000000000..1d533a942 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trains/TrainDetailCard.tsx @@ -0,0 +1,19 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Train } from '@/services/trainService'; + +export function TrainDetailCard({ train }: { train: Train }) { + return ( + + {train.trainNumber || train.code} - {train.trainName || 'Unnamed'} + +
Status: {train.status}
+
Capacity: {train.capacityTons} tons
+
Origin: {train.originStationId || '-'}
+
Destination: {train.destinationStationId || '-'}
+
Departure: {train.departureTime ? new Date(train.departureTime).toLocaleString() : '-'}
+
Arrival: {train.arrivalTime ? new Date(train.arrivalTime).toLocaleString() : '-'}
+ {train.remarks &&
Remarks: {train.remarks}
} +
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/trains/TrainFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/trains/TrainFormDialog.tsx new file mode 100644 index 000000000..a73876e7d --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trains/TrainFormDialog.tsx @@ -0,0 +1,59 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useCreateTrain, useUpdateTrain } from '@/hooks/useTrains'; +import { useToast } from '@/hooks/use-toast'; + +interface TrainFormDialogProps { + trigger?: React.ReactNode; + train?: any; + onSuccess?: () => void; +} + +export function TrainFormDialog({ trigger, train, onSuccess }: TrainFormDialogProps) { + const [open, setOpen] = useState(false); + const [form, setForm] = useState({ code: '', capacityTons: 0, trainNumber: '', trainName: '' }); + const createTrain = useCreateTrain(); + const updateTrain = useUpdateTrain(); + const { toast } = useToast(); + + useEffect(() => { + if (train) setForm({ + code: train.code, + capacityTons: train.capacityTons, + trainNumber: train.trainNumber || '', + trainName: train.trainName || '', + }); + }, [train]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (train) await updateTrain.mutateAsync({ id: train.id, data: form }); + else await createTrain.mutateAsync(form); + toast({ title: train ? 'Train updated' : 'Train created', description: `${form.code} saved.` }); + setOpen(false); + onSuccess?.(); + } catch { + toast({ title: 'Error', description: `Failed to ${train ? 'update' : 'create'} train.`, variant: 'destructive' }); + } + }; + + return ( + + {trigger || } + + {train ? 'Edit Train' : 'Create Train'} +
+
setForm({...form, code: e.target.value})} />
+
setForm({...form, capacityTons: parseFloat(e.target.value)})} />
+
setForm({...form, trainNumber: e.target.value})} />
+
setForm({...form, trainName: e.target.value})} />
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/trains/TrainsTable.tsx b/apps/edr-freight-web/backoffice/src/components/trains/TrainsTable.tsx new file mode 100644 index 000000000..2751fbe39 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trains/TrainsTable.tsx @@ -0,0 +1,45 @@ +import { useTrains, useDeleteTrain } from '@/hooks/useTrains'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Eye, Trash2 } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +export function TrainsTable() { + const { data: trains, isLoading } = useTrains(); + const deleteTrain = useDeleteTrain(); + + if (isLoading) return
Loading trains...
; + + return ( + + + + Number + Name + Status + Capacity (tons) + Actions + + + + {trains?.map(train => ( + + {train.trainNumber || train.code} + {train.trainName || '-'} + {train.status} + {train.capacityTons} + + + + + + + + ))} + +
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx new file mode 100644 index 000000000..1f14ecb13 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useWagons, useAssignWagonToTrain } from '@/hooks/useWagons'; +import { useToast } from '@/hooks/use-toast'; +import { Plus } from 'lucide-react'; + +export function AssignWagonDialog({ trainId }: { trainId: string }) { + const [open, setOpen] = useState(false); + const [wagonId, setWagonId] = useState(''); + const [sequence, setSequence] = useState(); + const { data: wagons } = useWagons(); + const assign = useAssignWagonToTrain(); + const { toast } = useToast(); + + const available = wagons?.filter(w => w.status === 'AVAILABLE' || !w.trainId); + + const handleAssign = async () => { + if (!wagonId) return; + await assign.mutateAsync({ wagonId, trainId, sequenceNumber: sequence }); + toast({ title: 'Assigned', description: 'Wagon attached to train.' }); + setOpen(false); + }; + + return ( + + + + Assign Wagon to Train +
+
+ + +
+
+ + setSequence(parseInt(e.target.value) || undefined)} /> +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/WagonFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/WagonFormDialog.tsx new file mode 100644 index 000000000..2fb36cd73 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/wagons/WagonFormDialog.tsx @@ -0,0 +1,71 @@ +// src/components/wagons/WagonFormDialog.tsx +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useCreateWagon, useUpdateWagon } from '@/hooks/useWagons'; +import { useToast } from '@/hooks/use-toast'; + +interface WagonFormDialogProps { + trigger?: React.ReactNode; + wagon?: any; + onSuccess?: () => void; +} + +export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogProps) { + const [open, setOpen] = useState(false); + const [form, setForm] = useState({ + wagonNumber: '', + wagonTypeId: '', + tareWeight: 0, + maxPayloadWeight: 0, + status: 'AVAILABLE', + notes: '' + }); + const createWagon = useCreateWagon(); + const updateWagon = useUpdateWagon(); + const { toast } = useToast(); + + useEffect(() => { + if (wagon) setForm({ + wagonNumber: wagon.wagonNumber, + wagonTypeId: wagon.wagonTypeId, + tareWeight: wagon.tareWeight, + maxPayloadWeight: wagon.maxPayloadWeight, + status: wagon.status, + notes: wagon.notes || '' + }); + }, [wagon]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (wagon) await updateWagon.mutateAsync({ id: wagon.id, data: form }); + else await createWagon.mutateAsync(form); + toast({ title: wagon ? 'Wagon updated' : 'Wagon created', description: `${form.wagonNumber} saved.` }); + setOpen(false); + onSuccess?.(); + } catch { + toast({ title: 'Error', description: `Failed to ${wagon ? 'update' : 'create'} wagon.`, variant: 'destructive' }); + } + }; + + return ( + + {trigger || } + + {wagon ? 'Edit Wagon' : 'Create Wagon'} +
+
setForm({...form, wagonNumber: e.target.value})} />
+
setForm({...form, wagonTypeId: e.target.value})} />
+
setForm({...form, tareWeight: parseFloat(e.target.value)})} />
+
setForm({...form, maxPayloadWeight: parseFloat(e.target.value)})} />
+
setForm({...form, status: e.target.value})} />
+
setForm({...form, notes: e.target.value})} />
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx new file mode 100644 index 000000000..5b372030f --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx @@ -0,0 +1,63 @@ +import { useWagonsByTrain, useUnassignWagon, useReorderWagons } from '@/hooks/useWagons'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Trash2, GripVertical } from 'lucide-react'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; + +export function WagonsTable({ trainId }: { trainId: string }) { + const { data: wagons, refetch } = useWagonsByTrain(trainId); + const unassign = useUnassignWagon(); + const reorder = useReorderWagons(); + + const onDragEnd = (result: any) => { + if (!result.destination) return; + const items = Array.from(wagons || []); + const [removed] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, removed); + reorder.mutate({ trainId, wagonIds: items.map(w => w.id) }); + }; + + if (!wagons?.length) return
No wagons assigned.
; + + return ( + + + {(provided) => ( + + + + + Number + Type + Sequence + Status + Actions + + + + {wagons.map((wagon, idx) => ( + + {(provided) => ( + + + {wagon.wagonNumber} + {wagon.wagonTypeId} + {wagon.sequenceNumber} + {wagon.status} + + + + + )} + + ))} + {provided.placeholder} + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts b/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts new file mode 100644 index 000000000..4873e1dba --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts @@ -0,0 +1,39 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { cargoService } from '@/services/cargoService'; + +export const cargoKeys = { + all: ['cargoes'] as const, + byContainer: (containerId: string) => [...cargoKeys.all, 'container', containerId] as const, +}; + +export function useCargoes() { + return useQuery({ queryKey: cargoKeys.all, queryFn: () => cargoService.getAll().then(res => res.data) }); +} + +export function useCargoesByContainer(containerId: string) { + return useQuery({ queryKey: cargoKeys.byContainer(containerId), queryFn: () => cargoService.getByContainer(containerId).then(res => res.data), enabled: !!containerId }); +} + +export function useLoadCargo() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, quantity, weight, volume }: any) => cargoService.load(id, quantity, weight, volume), + onSuccess: (_, { id }) => qc.invalidateQueries({ queryKey: cargoKeys.all }) + }); +} + +export function useDeliverCargo() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => cargoService.deliver(id), + onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) + }); +} + +export function useUnloadCargo() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => cargoService.unload(id), + onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) + }); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts b/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts new file mode 100644 index 000000000..6eac8fdcf --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts @@ -0,0 +1,31 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { containerService } from '@/services/containerService'; + +export const containerKeys = { + all: ['containers'] as const, + byWagon: (wagonId: string) => [...containerKeys.all, 'wagon', wagonId] as const, +}; + +export function useContainers() { + return useQuery({ queryKey: containerKeys.all, queryFn: () => containerService.getAll().then(res => res.data) }); +} + +export function useContainersByWagon(wagonId: string) { + return useQuery({ queryKey: containerKeys.byWagon(wagonId), queryFn: () => containerService.getByWagon(wagonId).then(res => res.data), enabled: !!wagonId }); +} + +export function useAssignContainerToWagon() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ containerId, wagonId, position }: any) => containerService.assignToWagon(containerId, wagonId, position), + onSuccess: (_, { wagonId }) => qc.invalidateQueries({ queryKey: containerKeys.byWagon(wagonId) }) + }); +} + +export function useUnassignContainer() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: containerService.unassign, + onSuccess: () => qc.invalidateQueries({ queryKey: containerKeys.all }) + }); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts b/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts new file mode 100644 index 000000000..5887f3afc --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts @@ -0,0 +1,35 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { trainService } from '@/services/trainService'; + +export const trainKeys = { + all: ['trains'] as const, + lists: () => [...trainKeys.all, 'list'] as const, + details: () => [...trainKeys.all, 'detail'] as const, + detail: (id: string) => [...trainKeys.details(), id] as const, +}; + +export function useTrains() { + return useQuery({ queryKey: trainKeys.lists(), queryFn: () => trainService.getAll().then(res => res.data) }); +} + +export function useTrain(id: string) { + return useQuery({ queryKey: trainKeys.detail(id), queryFn: () => trainService.getById(id).then(res => res.data), enabled: !!id }); +} + +export function useCreateTrain() { + const qc = useQueryClient(); + return useMutation({ mutationFn: trainService.create, onSuccess: () => qc.invalidateQueries({ queryKey: trainKeys.lists() }) }); +} + +export function useUpdateTrain() { + const qc = useQueryClient(); + return useMutation({ mutationFn: ({ id, data }: any) => trainService.update(id, data), onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: trainKeys.lists() }); + qc.invalidateQueries({ queryKey: trainKeys.detail(id) }); + } }); +} + +export function useDeleteTrain() { + const qc = useQueryClient(); + return useMutation({ mutationFn: trainService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: trainKeys.lists() }) }); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts b/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts new file mode 100644 index 000000000..0e2bd5e25 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts @@ -0,0 +1,41 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { wagonService } from '@/services/wagon.service'; + +export const wagonKeys = { + all: ['wagons'] as const, + byTrain: (trainId: string) => [...wagonKeys.all, 'train', trainId] as const, + details: () => [...wagonKeys.all, 'detail'] as const, +}; + +export function useWagons() { + return useQuery({ queryKey: wagonKeys.all, queryFn: () => wagonService.getAll().then(res => res.data) }); +} + +export function useWagonsByTrain(trainId: string) { + return useQuery({ queryKey: wagonKeys.byTrain(trainId), queryFn: () => wagonService.getByTrain(trainId).then(res => res.data), enabled: !!trainId }); +} + +export function useAssignWagonToTrain() { + const qc = useQueryClient(); + return useMutation({ mutationFn: ({ wagonId, trainId, sequenceNumber }: any) => wagonService.assignToTrain(wagonId, trainId, sequenceNumber), onSuccess: (_, { trainId }) => qc.invalidateQueries({ queryKey: wagonKeys.byTrain(trainId) }) }); +} + +export function useUnassignWagon() { + const qc = useQueryClient(); + return useMutation({ mutationFn: wagonService.unassign, onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); +} + +export function useReorderWagons() { + const qc = useQueryClient(); + return useMutation({ mutationFn: ({ trainId, wagonIds }: any) => wagonService.reorder(trainId, wagonIds), onSuccess: (_, { trainId }) => qc.invalidateQueries({ queryKey: wagonKeys.byTrain(trainId) }) }); +} + +export function useCreateWagon() { + const qc = useQueryClient(); + return useMutation({ mutationFn: wagonService.create, onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); +} + +export function useUpdateWagon() { + const qc = useQueryClient(); + return useMutation({ mutationFn: ({ id, data }: any) => wagonService.update(id, data), onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx index b44b5f3d4..723821936 100644 --- a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx @@ -7,15 +7,9 @@ import { Badge } from '@/components/ui/badge'; import { LoadingScreen } from '@/ui/LoadingScreen'; import { useRateMatrixAuth } from '@/auth/hooks/useAuth'; import { queryKeys } from '../../../constants/QUERY_KEYS'; +import { API_URLS } from '@/constants/URL_CONSTANTS'; //import { MATRIX_STATUS } from '@/constants/rateMatrixConstants'; import { toast } from 'sonner'; - -const API_URLS = { - RATE_MATRIX: { - LIST: '/api/rate-matrices', - AUTHORIZE: (id: string) => `/api/rate-matrices/${id}/authorize`, - }, -}; import { Navigate } from 'react-router-dom'; export default function RateMatrixApprovalPage() { diff --git a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx index fc6855dc3..6f151f992 100644 --- a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx @@ -1,7 +1,7 @@ // pages/admin/rateMatrix/RateMatrixRegistration.tsx import React from 'react'; import { RateMatrixForm } from '@/components/baselineRatematrix/RateMatrixForm'; -import { useRateMatrixAuth } from '../../../auth/hooks/useAuth'; +import { useRateMatrixAuth } from '@/auth/useAuth'; import { Navigate } from 'react-router-dom'; // Local lightweight fallback for LoadingScreen to avoid import errors const LoadingScreen: React.FC<{ message?: string }> = ({ message = 'Loading...' }) => ( diff --git a/apps/edr-freight-web/backoffice/src/pages/cargoes/CargoesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/cargoes/CargoesPage.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/edr-freight-web/backoffice/src/pages/containers_management/ContainersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/containers_management/ContainersPage.tsx new file mode 100644 index 000000000..5c531a434 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/containers_management/ContainersPage.tsx @@ -0,0 +1,29 @@ +import { useContainers } from '@/hooks/useContainers'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; + +export default function ContainersPage() { + const { data: containers, isLoading } = useContainers(); + if (isLoading) return
Loading containers...
; + return ( + + All Containers + + + NumberTypeWagonStatus + + {containers?.map(c => ( + + {c.containerNumber} + {c.containerTypeId} + {c.wagonId || 'Unassigned'} + {c.status} + + ))} + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx new file mode 100644 index 000000000..500fc3c91 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx @@ -0,0 +1,33 @@ +import { useParams } from 'react-router-dom'; +import { useTrain } from '@/hooks/useTrains'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { AssignWagonDialog } from '@/components/AssignWagonDialog'; +import { WagonsTable } from '@/components/WagonsTable'; + +export default function TrainDetailPage() { + const { id } = useParams<{ id: string }>(); + const { data: train, isLoading } = useTrain(id!); + + if (isLoading) return ; + if (!train) return
Train not found
; + + return ( +
+ + {train.trainNumber || train.code} - {train.trainName || 'Unnamed'} + +
Status: {train.status}
+
Capacity: {train.capacityTons} tons
+
Origin Station: {train.originStationId || '-'}
+
Destination: {train.destinationStationId || '-'}
+
+
+
+

Wagons

+ +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx index a9d12eb6a..85b5a7bd2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx @@ -1,12 +1,78 @@ -import FeaturePlaceholder from "@/components/FeaturePlaceholder"; +import { useState } from 'react'; +import { useTrains, useDeleteTrain, useCreateTrain } from '@/hooks/useTrains'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/hooks/use-toast'; +import { Plus, Eye, Trash2 } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +const CreateTrainForm = ({ onSuccess }: { onSuccess: () => void }) => { + const [form, setForm] = useState({ code: '', capacityTons: 0, trainNumber: '', trainName: '' }); + const createTrain = useCreateTrain(); + const { toast } = useToast(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await createTrain.mutateAsync(form); + toast({ title: 'Train created', description: `${form.code} added.` }); + onSuccess(); + } catch { + toast({ title: 'Error', description: 'Failed to create train.', variant: 'destructive' }); + } + }; -const TrainsPage = () => { return ( - +
+
setForm({...form, code: e.target.value})} />
+
setForm({...form, capacityTons: parseFloat(e.target.value)})} />
+
setForm({...form, trainNumber: e.target.value})} />
+
setForm({...form, trainName: e.target.value})} />
+ +
); }; -export default TrainsPage; +export default function TrainsPage() { + const { data: trains, isLoading } = useTrains(); + const deleteTrain = useDeleteTrain(); + const [open, setOpen] = useState(false); + + if (isLoading) return
Loading trains...
; + + return ( + + + Trains + + + Create Train setOpen(false)} /> + + + + + NumberNameStatusCapacityActions + + {trains?.map(train => ( + + {train.trainNumber || train.code} + {train.trainName || '-'} + {train.status} + {train.capacityTons} t + + + + + + ))} + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/wagons/WagonsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/wagons/WagonsPage.tsx new file mode 100644 index 000000000..b5407f022 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/wagons/WagonsPage.tsx @@ -0,0 +1,29 @@ +import { useWagons } from '@/hooks/useWagons'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function WagonsPage() { + const { data: wagons, isLoading } = useWagons(); + if (isLoading) return
Loading wagons...
; + return ( + + All Wagons + + + NumberTypeTrainStatus + + {wagons?.map(w => ( + + {w.wagonNumber} + {w.wagonTypeId} + {w.trainId || 'Unassigned'} + {w.status} + + ))} + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/cargo.servcie.ts b/apps/edr-freight-web/backoffice/src/services/cargo.servcie.ts new file mode 100644 index 000000000..efd7eecdc --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/cargo.servcie.ts @@ -0,0 +1,26 @@ +import { apiClient } from '@/lib/axios'; + +export interface Cargo { + id: string; + cargoReference: string; + shipmentId: string; + containerId: string; + cargoTypeId?: string; + description?: string; + quantity: number; + weight: number; + volume?: number; + status: 'PENDING' | 'LOADED' | 'IN_TRANSIT' | 'DELIVERED' | 'UNLOADED'; + loadedAt?: string; + unloadedAt?: string; +} + +export const cargoService = { + getAll: () => apiClient.get('/cargoes'), + getByContainer: (containerId: string) => apiClient.get(`/cargoes?containerId=${containerId}`), + create: (data: any) => apiClient.post('/cargoes', data), + load: (cargoId: string, quantity: number, weight: number, volume?: number) => + apiClient.post(`/cargoes/${cargoId}/load`, { quantity, weight, volume }), + deliver: (cargoId: string) => apiClient.post(`/cargoes/${cargoId}/deliver`), + unload: (cargoId: string) => apiClient.post(`/cargoes/${cargoId}/unload`), +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/containerService.ts b/apps/edr-freight-web/backoffice/src/services/containerService.ts new file mode 100644 index 000000000..3d0e84bb8 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/containerService.ts @@ -0,0 +1,21 @@ +import { apiClient } from '@/lib/axios'; + +export interface Container { + id: string; + containerNumber: string; + containerTypeId: string; + wagonId: string | null; + position: number | null; + tareWeight: number; + maxGrossWeight: number; + sealNumber?: string; + status: string; +} + +export const containerService = { + getAll: () => apiClient.get('/containers'), + getByWagon: (wagonId: string) => apiClient.get(`/containers?wagonId=${wagonId}`), + assignToWagon: (containerId: string, wagonId: string, position?: number) => + apiClient.post(`/containers/${containerId}/assign-wagon`, { wagonId, position }), + unassign: (containerId: string) => apiClient.post(`/containers/${containerId}/unassign-wagon`), +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/trains.service.ts b/apps/edr-freight-web/backoffice/src/services/trains.service.ts new file mode 100644 index 000000000..57f0c5630 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/trains.service.ts @@ -0,0 +1,26 @@ +import { apiClient } from '@/lib/axios'; + +export interface Train { + id: string; + code: string; + capacityTons: number; + trainNumber?: string; + trainName?: string; + routeId?: string; + originStationId?: string; + destinationStationId?: string; + departureTime?: string; + arrivalTime?: string; + locomotiveNumber?: string; + status: string; + remarks?: string; +} + +export const trainService = { + getAll: () => apiClient.get('/trains'), + getById: (id: string) => apiClient.get(`/trains/${id}`), + create: (data: Partial) => apiClient.post('/trains', data), + update: (id: string, data: Partial) => apiClient.patch(`/trains/${id}`, data), + delete: (id: string) => apiClient.delete(`/trains/${id}`), + getDetails: (id: string) => apiClient.get(`/trains/${id}/details`), +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/wagon.service.ts b/apps/edr-freight-web/backoffice/src/services/wagon.service.ts new file mode 100644 index 000000000..27ee99bbd --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/wagon.service.ts @@ -0,0 +1,26 @@ +import { apiClient } from '@/lib/axios'; + +export interface Wagon { + id: string; + wagonNumber: string; + wagonTypeId: string; + trainId: string | null; + sequenceNumber: number | null; + tareWeight: number; + maxPayloadWeight: number; + status: string; + notes?: string; +} + +export const wagonService = { + getAll: () => apiClient.get('/wagons'), + getByTrain: (trainId: string) => apiClient.get(`/wagons?trainId=${trainId}`), + assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) => + apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }), + unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`), + reorder: (trainId: string, wagonIds: string[]) => + apiClient.post(`/trains/${trainId}/reorder-wagons`, { wagonIds }), + create: (data: Partial) => apiClient.post('/wagons', data), + update: (id: string, data: Partial) => apiClient.patch(`/wagons/${id}`, data), + +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/tsconfig.json b/apps/edr-freight-web/backoffice/tsconfig.json index 04f44ca2b..1ffef600d 100644 --- a/apps/edr-freight-web/backoffice/tsconfig.json +++ b/apps/edr-freight-web/backoffice/tsconfig.json @@ -2,18 +2,6 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" }, - { - "compilerOptions": { - "baseUrl": "src", - "paths": { - "@/*": ["*"], - - "@constants/*": ["constants/*"], - "@components/*": ["components/*"] - } - } -} - + { "path": "./tsconfig.node.json" } ] } diff --git a/apps/edr-freight-web/portal/vite.config.ts.timestamp-1780409607954-40aaca5c64a3.mjs b/apps/edr-freight-web/portal/vite.config.ts.timestamp-1780409607954-40aaca5c64a3.mjs new file mode 100644 index 000000000..3d7fe4d5b --- /dev/null +++ b/apps/edr-freight-web/portal/vite.config.ts.timestamp-1780409607954-40aaca5c64a3.mjs @@ -0,0 +1,24 @@ +// vite.config.ts +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0/node_modules/vite/dist/node/index.js"; +import react from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@vitejs/plugin-react/dist/index.js"; +import tailwindcss from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@tailwindcss+vite@4.3.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@tailwindcss/vite/dist/index.mjs"; +var __vite_injected_original_import_meta_url = "file:///C:/laragon/www/edr-platform/apps/edr-freight-web/portal/vite.config.ts"; +var __dirname = path.dirname(fileURLToPath(__vite_injected_original_import_meta_url)); +var vite_config_default = defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src") + } + }, + server: { + port: 5173, + host: "0.0.0.0" + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxsYXJhZ29uXFxcXHd3d1xcXFxlZHItcGxhdGZvcm1cXFxcYXBwc1xcXFxlZHItZnJlaWdodC13ZWJcXFxccG9ydGFsXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxsYXJhZ29uXFxcXHd3d1xcXFxlZHItcGxhdGZvcm1cXFxcYXBwc1xcXFxlZHItZnJlaWdodC13ZWJcXFxccG9ydGFsXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9sYXJhZ29uL3d3dy9lZHItcGxhdGZvcm0vYXBwcy9lZHItZnJlaWdodC13ZWIvcG9ydGFsL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xyXG5pbXBvcnQgeyBmaWxlVVJMVG9QYXRoIH0gZnJvbSBcIm5vZGU6dXJsXCI7XHJcblxyXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xyXG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCI7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIjtcclxuXHJcbmNvbnN0IF9fZGlybmFtZSA9IHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpO1xyXG5cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKSwgdGFpbHdpbmRjc3MoKV0sXHJcbiAgcmVzb2x2ZToge1xyXG4gICAgYWxpYXM6IHtcclxuICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXHJcbiAgICB9LFxyXG4gIH0sXHJcbiAgc2VydmVyOiB7XHJcbiAgICBwb3J0OiA1MTczLFxyXG4gICAgaG9zdDogXCIwLjAuMC4wXCIsXHJcbiAgfSxcclxufSk7XHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBcVcsT0FBTyxVQUFVO0FBQ3RYLFNBQVMscUJBQXFCO0FBRTlCLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sV0FBVztBQUNsQixPQUFPLGlCQUFpQjtBQUwyTSxJQUFNLDJDQUEyQztBQU9wUixJQUFNLFlBQVksS0FBSyxRQUFRLGNBQWMsd0NBQWUsQ0FBQztBQUU3RCxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFHLFlBQVksQ0FBQztBQUFBLEVBQ2hDLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLEtBQUssS0FBSyxRQUFRLFdBQVcsT0FBTztBQUFBLElBQ3RDO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/package.json b/package.json index 31a34c325..7b33dff30 100644 --- a/package.json +++ b/package.json @@ -17,20 +17,22 @@ "prepare": "husky" }, "devDependencies": { - "turbo": "^2.3.0", - "typescript": "^5.5.4", - "prettier": "^3.3.3", + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", "husky": "^9.1.6", "lint-staged": "^15.2.10", - "@commitlint/cli": "^19.5.0", - "@commitlint/config-conventional": "^19.5.0" + "prettier": "^3.3.3", + "turbo": "^2.3.0", + "typeorm": "0.3.30", + "typescript": "^5.5.4" }, "pnpm": { "overrides": { "date-fns": "^3.6.0", "pdfjs-dist": "^3.11.174", "react": "19.2.6", - "react-dom": "19.2.6" + "react-dom": "19.2.6", + "typeorm": "0.3.30" } }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db3a0db68..6cd75b663 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: pdfjs-dist: ^3.11.174 react: 19.2.6 react-dom: 19.2.6 + typeorm: 0.3.30 importers: @@ -32,6 +33,9 @@ importers: turbo: specifier: ^2.3.0 version: 2.9.14 + typeorm: + specifier: 0.3.30 + version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) typescript: specifier: ^5.5.4 version: 5.9.3 @@ -104,9 +108,6 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 - typeorm: - specifier: ^0.3.20 - version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) devDependencies: '@edr/eslint-config': specifier: workspace:* @@ -159,6 +160,9 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + typeorm: + specifier: 0.3.30 + version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) typescript: specifier: ^5.5.4 version: 5.9.3 @@ -411,7 +415,7 @@ importers: specifier: ^7.8.1 version: 7.8.2 typeorm: - specifier: ^0.3.20 + specifier: 0.3.30 version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) devDependencies: '@edr/eslint-config': @@ -616,7 +620,7 @@ importers: specifier: ^7.8.1 version: 7.8.2 typeorm: - specifier: ^0.3.20 + specifier: 0.3.30 version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) typescript: specifier: ^5.5.4 @@ -2043,7 +2047,7 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 reflect-metadata: ^0.1.13 || ^0.2.0 rxjs: ^7.2.0 - typeorm: ^0.3.0 || ^1.0.0-dev + typeorm: 0.3.30 '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} @@ -3639,7 +3643,7 @@ packages: '@tria-plc/iamapi-common': '*' reflect-metadata: ^0.2.0 rxjs: ^7.8.0 - typeorm: ^0.3.0 + typeorm: 0.3.30 '@tria-plc/iamapi-common@0.1.6': resolution: {integrity: sha512-qaCLZ1TgbcQ5XciRuA/aZjRCM/GmGYgcmsyDDIlhiMNw3FL3DCiR/QiFuQpERvV4FqgQNBJg66+S/jXO3I2jbw==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.1.6/e2a8f3357b650bb9477facae4aaa7044382acebf} @@ -3660,7 +3664,7 @@ packages: class-validator: ^0.14.1 reflect-metadata: ^0.2.0 rxjs: ^7.8.0 - typeorm: ^0.3.0 + typeorm: 0.3.30 '@tria-plc/iamui-common@1.1.1': resolution: {integrity: sha512-aj9fMxmB/3kwSnN1vl8NvmCrI/1quNjfg9Bb7fJR4HhWEE26foIZC2xeg1ERzQxBll6rMj/R0Zl0+Riv6OONTQ==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamui-common/1.1.1/98fa2e350b807e9d123cab7d75038a027005ef41} @@ -9645,7 +9649,7 @@ packages: hasBin: true peerDependencies: '@faker-js/faker': '>=8.4.1' - typeorm: ~0.3.0 + typeorm: 0.3.30 typeorm@0.3.30: resolution: {integrity: sha512-8T35PzjefOdqc2ZR9mwLQj0pUGp6lQhMbK2EvVMwJVJWlaoHm0v/Q6dThNOZkFchD+0yMg8gwjKM28ePiLSXSQ==}