From dc42838a43d9f982af0e29acfcfca5f81bda30d9 Mon Sep 17 00:00:00 2001 From: hagiye Date: Fri, 5 Jun 2026 21:07:04 +0300 Subject: [PATCH] Trains management CRUD issues solved --- apps/edr-freight-api/package.json | 1 + apps/edr-freight-api/src/app.module.ts | 1 - .../1750000000000-AddCompanyContactColumns.ts | 21 + .../1750100000000-CreateFleetCrudTables.ts | 127 ++ .../src/modules/cargoes/cargoes.controller.ts | 7 +- .../src/modules/cargoes/cargoes.service.ts | 36 +- .../companies/entities/company.entity.ts | 15 + .../containers.controller.ts | 7 +- .../containers.service.ts | 33 +- .../modules/customers/customers.controller.ts | 8 +- .../modules/customers/customers.service.ts | 32 +- .../customers/dto/response-customer.dto.ts | 4 +- .../customers/entities/customer.entity.ts | 6 +- .../get-eligible-container-bookings.dto.ts | 5 +- .../train-scheduling.service.ts | 8 +- .../src/modules/trains/trains.controller.ts | 20 +- .../src/modules/trains/trains.service.ts | 28 +- .../src/modules/wagons/wagons.controller.ts | 7 +- .../src/modules/wagons/wagons.service.ts | 33 +- .../src/scripts/create-freight-schema.js | 25 + .../src/scripts/create-freight-schema.ts | 27 + .../src/scripts/run-migrations.ts | 21 + .../src/seed/demo-bookings.seeder.ts | 49 +- apps/edr-freight-web/backoffice/src/App.tsx | 33 +- .../components/cargoes/CargoFormDialog.tsx | 238 +++ .../ContainerFormDialog.tsx | 263 +++ .../wagons/WagonFormDialogEnhanced.tsx | 263 +++ .../src/components/wagons/WagonsTable.tsx | 6 +- .../backoffice/src/constants/URLS.ts | 1 + .../backoffice/src/hooks/useCargoes.ts | 30 +- .../backoffice/src/hooks/useContainers.ts | 30 +- .../backoffice/src/hooks/useTrains.ts | 6 +- .../backoffice/src/hooks/useWagons.ts | 21 +- .../src/pages/cargoes/CargoesPageEnhanced.tsx | 314 +++ .../src/pages/fleet/FleetCrudPages.tsx | 464 +++++ .../src/pages/trains/TrainsPage.tsx | 23 +- .../backoffice/src/services/cargoService.ts | 5 +- .../src/services/containerService.ts | 6 +- .../src/services/trainScheduling.service.ts | 9 +- .../backoffice/src/services/wagon.service.ts | 9 +- pnpm-lock.yaml | 1780 +---------------- 41 files changed, 2192 insertions(+), 1830 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts create mode 100644 apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts create mode 100644 apps/edr-freight-api/src/scripts/create-freight-schema.js create mode 100644 apps/edr-freight-api/src/scripts/create-freight-schema.ts create mode 100644 apps/edr-freight-api/src/scripts/run-migrations.ts create mode 100644 apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/container_management/ContainerFormDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/wagons/WagonFormDialogEnhanced.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/cargoes/CargoesPageEnhanced.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx diff --git a/apps/edr-freight-api/package.json b/apps/edr-freight-api/package.json index e086598b8..50ff2f1d4 100644 --- a/apps/edr-freight-api/package.json +++ b/apps/edr-freight-api/package.json @@ -53,6 +53,7 @@ "@types/multer": "^2.1.0", "@types/node": "^20.14.0", "@types/supertest": "^6.0.2", + "@types/pg": "^8.6.7", "jest": "^29.7.0", "supertest": "^7.0.0", "ts-jest": "^29.2.5", diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 75826364c..4c57af97a 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -75,7 +75,6 @@ import { CargoesModule } from './modules/cargoes/cargoes.module'; BookingsModule, FilesModule, ConsignmentsModule, - TrainsModule, LocomotivesModule, WagonTypesModule, TrainSetsModule, diff --git a/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts b/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts new file mode 100644 index 000000000..56d41edf9 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCompanyContactColumns1750000000000 implements MigrationInterface { + name = 'AddCompanyContactColumns1750000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_name VARCHAR(100);`); + await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_phone VARCHAR(20);`); + await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_name VARCHAR(100);`); + await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_email VARCHAR(150);`); + await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_phone VARCHAR(20);`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_phone;`); + await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_email;`); + await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_name;`); + await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_phone;`); + await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_name;`); + } +} diff --git a/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts b/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts new file mode 100644 index 000000000..1763a9db0 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts @@ -0,0 +1,127 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateFleetCrudTables1750100000000 implements MigrationInterface { + name = 'CreateFleetCrudTables1750100000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS freight.wagons ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + wagon_number VARCHAR NOT NULL UNIQUE, + wagon_type_id UUID NOT NULL, + train_id UUID, + sequence_number INT, + tare_weight NUMERIC(10, 2) NOT NULL, + max_payload_weight NUMERIC(10, 2) NOT NULL, + status VARCHAR NOT NULL DEFAULT 'AVAILABLE', + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ + ); + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS freight.containers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + container_number VARCHAR NOT NULL UNIQUE, + container_type_id UUID NOT NULL, + wagon_id UUID, + position INT, + tare_weight NUMERIC(10, 2) NOT NULL, + max_gross_weight NUMERIC(10, 2) NOT NULL, + seal_number VARCHAR, + status VARCHAR NOT NULL DEFAULT 'AVAILABLE', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ + ); + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS freight.cargoes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + cargo_reference VARCHAR NOT NULL UNIQUE, + shipment_id UUID NOT NULL, + container_id UUID NOT NULL, + cargo_type_id UUID, + description TEXT, + quantity NUMERIC(12, 3) NOT NULL, + weight NUMERIC(10, 2) NOT NULL, + volume NUMERIC(10, 2), + status VARCHAR NOT NULL DEFAULT 'PENDING', + loaded_at TIMESTAMP, + unloaded_at TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ + ); + `); + + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_wagons_train_id" ON freight.wagons (train_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_containers_wagon_id" ON freight.containers (wagon_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_cargoes_container_id" ON freight.cargoes (container_id)`); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.wagons + ADD CONSTRAINT "FK_wagons_train_id" + FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE SET NULL; + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.wagons + ADD CONSTRAINT "FK_wagons_wagon_type_id" + FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.containers + ADD CONSTRAINT "FK_containers_wagon_id" + FOREIGN KEY (wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL; + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.containers + ADD CONSTRAINT "FK_containers_container_type_id" + FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.cargoes + ADD CONSTRAINT "FK_cargoes_container_id" + FOREIGN KEY (container_id) REFERENCES freight.containers(id) ON DELETE RESTRICT; + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + + await queryRunner.query(` + DO $$ BEGIN + ALTER TABLE freight.cargoes + ADD CONSTRAINT "FK_cargoes_cargo_type_id" + FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS freight.cargoes CASCADE`); + await queryRunner.query(`DROP TABLE IF EXISTS freight.containers CASCADE`); + await queryRunner.query(`DROP TABLE IF EXISTS freight.wagons CASCADE`); + } +} diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts index 5d86a579b..b0babb06f 100644 --- a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts @@ -7,6 +7,7 @@ import { ParseUUIDPipe, Patch, Post, + Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CreateCargoDto } from './dto/create-cargo.dto'; @@ -28,8 +29,8 @@ export class CargoesController { @Get() @ApiOperation({ summary: 'List all cargoes' }) - findAll() { - return this.cargoesService.findAll(); + findAll(@Query() query: Record) { + return this.cargoesService.findAll(query); } @Get(':id') @@ -67,4 +68,4 @@ export class CargoesController { 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.service.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts index c1a6e213e..0ddd507cb 100644 --- a/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsOrder, FindOptionsWhere, ILike, Repository } from 'typeorm'; import { CreateCargoDto } from './dto/create-cargo.dto'; import { UpdateCargoDto } from './dto/update-cargo.dto'; import { LoadCargoDto } from './dto/load-cargo.dto'; @@ -22,8 +22,36 @@ export class CargoesService { return this.cargoRepo.save(cargo); } - async findAll(): Promise { - return this.cargoRepo.find({ order: { cargoReference: 'ASC' } }); + async findAll(query: Record = {}): Promise { + const where: FindOptionsWhere[] | FindOptionsWhere = []; + const search = query.search?.trim(); + const status = query.status?.trim(); + const containerId = query.containerId?.trim(); + + if (search) { + where.push({ + cargoReference: ILike(`%${search}%`), + ...(status ? { status } : {}), + ...(containerId ? { containerId } : {}), + }); + where.push({ + description: ILike(`%${search}%`), + ...(status ? { status } : {}), + ...(containerId ? { containerId } : {}), + }); + } + + const sortBy = ['cargoReference', 'quantity', 'weight', 'volume', 'status'].includes(query.sortBy ?? '') + ? (query.sortBy as keyof Cargo) + : 'cargoReference'; + const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + + return this.cargoRepo.find({ + where: search ? where : { ...(status ? { status } : {}), ...(containerId ? { containerId } : {}) }, + order: { [sortBy]: sortOrder } as FindOptionsOrder, + skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined, + take: query.limit ? Number(query.limit) : undefined, + }); } async findById(id: string): Promise { @@ -101,4 +129,4 @@ export class CargoesService { return this.cargoRepo.save(cargo); } -} \ No newline at end of file +} diff --git a/apps/edr-freight-api/src/modules/companies/entities/company.entity.ts b/apps/edr-freight-api/src/modules/companies/entities/company.entity.ts index ad7407df1..070854eb8 100644 --- a/apps/edr-freight-api/src/modules/companies/entities/company.entity.ts +++ b/apps/edr-freight-api/src/modules/companies/entities/company.entity.ts @@ -53,6 +53,21 @@ export class Company extends BaseEntity { @Column({ name: 'email', type: 'varchar', length: 150, nullable: true }) email?: string | null; + @Column({ name: 'contact_person_name', type: 'varchar', length: 100, nullable: true }) + contactPersonName?: string | null; + + @Column({ name: 'contact_person_phone', type: 'varchar', length: 20, nullable: true }) + contactPersonPhone?: string | null; + + @Column({ name: 'general_manager_name', type: 'varchar', length: 100, nullable: true }) + generalManagerName?: string | null; + + @Column({ name: 'general_manager_email', type: 'varchar', length: 150, nullable: true }) + generalManagerEmail?: string | null; + + @Column({ name: 'general_manager_phone', type: 'varchar', length: 20, nullable: true }) + generalManagerPhone?: string | null; + @Column({ name: 'website', type: 'varchar', length: 200, nullable: true }) website?: string | null; 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 index 78759d9b4..efffb075e 100644 --- a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts +++ b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts @@ -7,6 +7,7 @@ import { ParseUUIDPipe, Patch, Post, + Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CreateContainerDto } from './dto/create-container.dto'; @@ -27,8 +28,8 @@ export class ContainersController { @Get() @ApiOperation({ summary: 'List all containers' }) - findAll() { - return this.containersService.findAll(); + findAll(@Query() query: Record) { + return this.containersService.findAll(query); } @Get(':id') @@ -60,4 +61,4 @@ export class ContainersController { 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.service.ts b/apps/edr-freight-api/src/modules/container-management/containers.service.ts index 39f2d8274..1624e2d17 100644 --- a/apps/edr-freight-api/src/modules/container-management/containers.service.ts +++ b/apps/edr-freight-api/src/modules/container-management/containers.service.ts @@ -1,7 +1,7 @@ // 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 { FindOptionsOrder, FindOptionsWhere, ILike, 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'; @@ -24,8 +24,31 @@ export class ContainersService { return this.containerRepo.save(container); } - async findAll(): Promise { - return this.containerRepo.find({ order: { containerNumber: 'ASC' } }); + async findAll(query: Record = {}): Promise { + const where: FindOptionsWhere[] | FindOptionsWhere = []; + const search = query.search?.trim(); + const status = query.status?.trim(); + const wagonId = query.wagonId?.trim(); + + if (search) { + where.push({ + containerNumber: ILike(`%${search}%`), + ...(status ? { status } : {}), + ...(wagonId ? { wagonId } : {}), + }); + } + + const sortBy = ['containerNumber', 'tareWeight', 'maxGrossWeight', 'status', 'position'].includes(query.sortBy ?? '') + ? (query.sortBy as keyof Container) + : 'containerNumber'; + const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + + return this.containerRepo.find({ + where: search ? where : { ...(status ? { status } : {}), ...(wagonId ? { wagonId } : {}) }, + order: { [sortBy]: sortOrder } as FindOptionsOrder, + skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined, + take: query.limit ? Number(query.limit) : undefined, + }); } async findById(id: string): Promise { @@ -37,8 +60,6 @@ export class ContainersService { 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); } @@ -82,4 +103,4 @@ export class ContainersService { container.status = 'AVAILABLE'; return this.containerRepo.save(container); } -} \ No newline at end of file +} diff --git a/apps/edr-freight-api/src/modules/customers/customers.controller.ts b/apps/edr-freight-api/src/modules/customers/customers.controller.ts index 03b5a5549..404e4d27b 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.controller.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.controller.ts @@ -61,10 +61,10 @@ export class CustomersController { return this.customersService.findById(id); } - @Get("user/:userId") - findByUserId(@Param("userId", ParseUUIDPipe) userId: string): Promise { - return this.customersService.findByUserId(userId); - } + // @Get("user/:userId") + // findByUserId(@Param("userId", ParseUUIDPipe) userId: string): Promise { + // return this.customersService.findByUserId(userId); + // } @Patch(":id") @ApiOperation({ summary: "Update a customer" }) diff --git a/apps/edr-freight-api/src/modules/customers/customers.service.ts b/apps/edr-freight-api/src/modules/customers/customers.service.ts index 7a5238439..e3d1f3a82 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.service.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.service.ts @@ -52,15 +52,15 @@ export class CustomersService { return customer; } - async findByUserId(userId: string): Promise { - const customer = await this.customersRepository.findByUserId(userId); + // async findByUserId(userId: string): Promise { + // const customer = await this.customersRepository.findByUserId(userId); - if (!customer) { - throw new NotFoundException(`Customer with ID ${userId} not found`); - } + // if (!customer) { + // throw new NotFoundException(`Customer with ID ${userId} not found`); + // } - return customer; - } + // return customer; + //} /** Get customer by email */ async findByEmail(email: string): Promise { @@ -100,16 +100,16 @@ export class CustomersService { throw new BadRequestException("VAT number must be exactly 10 digits"); } - // Check email conflict - if (dto.email) { - const existing = await this.customersRepository.findByEmail(dto.email); + // // Check email conflict + // if (dto.email) { + // const existing = await this.customersRepository.findByEmail(dto.email); - if (existing && existing.userId !== id) { - throw new ConflictException( - `Customer with email "${dto.email}" already exists`, - ); - } - } + // // if (existing && existing.userId !== id) { + // // throw new ConflictException( + // // `Customer with email "${dto.email}" already exists`, + // // ); + // // } + // } const updated = await this.customersRepository.update(id, dto); diff --git a/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts index 98e350b96..3d2a086d4 100644 --- a/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts @@ -2,7 +2,7 @@ import { Customer } from '../entities/customer.entity'; export class ResponseCustomerDto { - UserId: string; + //UserId: string; firstName: string; lastName: string; email: string; @@ -30,7 +30,7 @@ export class ResponseCustomerDto { updatedAt: Date; constructor(customer: Customer) { - this.UserId = customer.userId; + //this.UserId = customer.userId; this.firstName = customer.firstName; this.lastName = customer.lastName; this.email = customer.email; diff --git a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts index fd2defac2..abccbaef8 100644 --- a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts +++ b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts @@ -3,12 +3,12 @@ import { Column, Entity, Index } from 'typeorm'; @Entity({ schema: 'freight', name: 'customers' }) @Index(['email']) -@Index(['userId']) +//@Index(['userId']) @Index(['tinNumber']) @Index(['fanNumber']) export class Customer extends BaseEntity { - @Column({ name: 'user_id', type: 'uuid' }) - userId!: string; + //@Column({ name: 'user_id', type: 'uuid' }) + //userId!: string; @Column({ name: 'first_name', type: 'varchar', length: 100 }) firstName!: string; diff --git a/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts b/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts index 8712b2a1d..24735808d 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts @@ -1,5 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDateString, IsOptional, IsUUID } from 'class-validator'; +import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator'; + +import { BOOKING_STATUSES } from '../../bookings/entities/booking.entity'; export class GetEligibleContainerBookingsDto { @ApiPropertyOptional({ format: 'uuid' }) @@ -19,5 +21,6 @@ export class GetEligibleContainerBookingsDto { @ApiPropertyOptional() @IsOptional() + @IsIn(BOOKING_STATUSES) status?: string; } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 34c4a7843..1d86bd1a5 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -118,11 +118,9 @@ export class TrainSchedulingService { ); } - if (query.status) { - queryBuilder.andWhere("booking.status = :status", { - status: query.status, - }); - } + queryBuilder.andWhere("booking.status = :status", { + status: query.status ?? "PAID", + }); const bookings = await queryBuilder .orderBy("booking.scheduled_date", "ASC") diff --git a/apps/edr-freight-api/src/modules/trains/trains.controller.ts b/apps/edr-freight-api/src/modules/trains/trains.controller.ts index 86ce6856b..c58fc086e 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.controller.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.controller.ts @@ -1,14 +1,18 @@ import { Body, Controller, + Delete, Get, Param, ParseUUIDPipe, + Patch, Post, + Query, } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { CreateTrainDto } from "./dto/create-train.dto"; +import { UpdateTrainDto } from "./dto/update-train.dto"; import { TrainsService } from "./trains.service"; @ApiTags("trains") @@ -24,8 +28,8 @@ export class TrainsController { @Get() @ApiOperation({ summary: "List all trains" }) - findAll() { - return this.trainsService.findAll(); + findAll(@Query() query: Record) { + return this.trainsService.findAll(query); } @Get(":id") @@ -33,4 +37,16 @@ export class TrainsController { findOne(@Param("id", ParseUUIDPipe) id: string) { return this.trainsService.findById(id); } + + @Patch(":id") + @ApiOperation({ summary: "Update a train" }) + update(@Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateTrainDto) { + return this.trainsService.update(id, dto); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a train" }) + remove(@Param("id", ParseUUIDPipe) id: string) { + return this.trainsService.remove(id); + } } 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 12aa65afe..9cf37760e 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.service.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsOrder, FindOptionsWhere, ILike, Repository } from 'typeorm'; import { CreateTrainDto } from './dto/create-train.dto'; import { UpdateTrainDto } from './dto/update-train.dto'; import { Train } from './entities/train.entity'; @@ -17,8 +17,28 @@ export class TrainsService { return this.trainRepo.save(train); } - findAll(): Promise { - return this.trainRepo.find({ order: { code: 'ASC' } }); + findAll(query: Record = {}): Promise { + const where: FindOptionsWhere[] | FindOptionsWhere = []; + const search = query.search?.trim(); + const status = query.status?.trim(); + + if (search) { + where.push({ code: ILike(`%${search}%`), ...(status ? { status: status as Train['status'] } : {}) }); + where.push({ trainNumber: ILike(`%${search}%`), ...(status ? { status: status as Train['status'] } : {}) }); + where.push({ trainName: ILike(`%${search}%`), ...(status ? { status: status as Train['status'] } : {}) }); + } + + const sortBy = ['code', 'trainNumber', 'trainName', 'capacityTons', 'status'].includes(query.sortBy ?? '') + ? (query.sortBy as keyof Train) + : 'code'; + const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + + return this.trainRepo.find({ + where: search ? where : status ? { status: status as Train['status'] } : {}, + order: { [sortBy]: sortOrder } as FindOptionsOrder, + skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined, + take: query.limit ? Number(query.limit) : undefined, + }); } async findById(id: string): Promise { @@ -38,4 +58,4 @@ export class TrainsService { 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/wagons.controller.ts b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts index 339948eec..fd2305f93 100644 --- a/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts +++ b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts @@ -7,6 +7,7 @@ import { ParseUUIDPipe, Patch, Post, + Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CreateWagonDto } from './dto/create-wagon.dto'; @@ -28,8 +29,8 @@ export class WagonsController { @Get() @ApiOperation({ summary: 'List all wagons' }) - findAll() { - return this.wagonsService.findAll(); + findAll(@Query() query: Record) { + return this.wagonsService.findAll(query); } @Get(':id') @@ -73,4 +74,4 @@ export class TrainWagonsReorderController { 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.service.ts b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts index 70909cbd8..bc0b52a69 100644 --- a/apps/edr-freight-api/src/modules/wagons/wagons.service.ts +++ b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; +import { Repository, DataSource, FindOptionsOrder, FindOptionsWhere, ILike } 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'; @@ -26,8 +26,31 @@ export class WagonsService { return this.wagonRepo.save(wagon); } - async findAll(): Promise { - return this.wagonRepo.find({ order: { wagonNumber: 'ASC' } }); + async findAll(query: Record = {}): Promise { + const where: FindOptionsWhere[] | FindOptionsWhere = []; + const search = query.search?.trim(); + const status = query.status?.trim(); + const trainId = query.trainId?.trim(); + + if (search) { + where.push({ + wagonNumber: ILike(`%${search}%`), + ...(status ? { status } : {}), + ...(trainId ? { trainId } : {}), + }); + } + + const sortBy = ['wagonNumber', 'tareWeight', 'maxPayloadWeight', 'status', 'sequenceNumber'].includes(query.sortBy ?? '') + ? (query.sortBy as keyof Wagon) + : 'wagonNumber'; + const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + + return this.wagonRepo.find({ + where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}) }, + order: { [sortBy]: sortOrder } as FindOptionsOrder, + skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined, + take: query.limit ? Number(query.limit) : undefined, + }); } async findById(id: string): Promise { @@ -39,8 +62,6 @@ export class WagonsService { 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); } @@ -98,4 +119,4 @@ export class WagonsService { await queryRunner.release(); } } -} \ No newline at end of file +} diff --git a/apps/edr-freight-api/src/scripts/create-freight-schema.js b/apps/edr-freight-api/src/scripts/create-freight-schema.js new file mode 100644 index 000000000..60d98616c --- /dev/null +++ b/apps/edr-freight-api/src/scripts/create-freight-schema.js @@ -0,0 +1,25 @@ +const { Client } = require('pg'); + +(async function createSchema(){ + const client = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: '', + database: 'edr_freight', + }); + + try { + console.log('Connecting to Postgres...'); + await client.connect(); + console.log('Creating schema freight if not exists...'); + await client.query('CREATE SCHEMA IF NOT EXISTS freight'); + console.log('Schema ensured.'); + await client.end(); + process.exit(0); + } catch (err) { + console.error('Failed to create schema:', err); + try { await client.end(); } catch {} + process.exit(1); + } +})(); diff --git a/apps/edr-freight-api/src/scripts/create-freight-schema.ts b/apps/edr-freight-api/src/scripts/create-freight-schema.ts new file mode 100644 index 000000000..5f66d2e76 --- /dev/null +++ b/apps/edr-freight-api/src/scripts/create-freight-schema.ts @@ -0,0 +1,27 @@ +import { Client } from 'pg'; + +async function createSchema() { + const client = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: '', + database: 'edr_freight', + }); + + try { + console.log('Connecting to Postgres...'); + await client.connect(); + console.log('Creating schema freight if not exists...'); + await client.query('CREATE SCHEMA IF NOT EXISTS freight'); + console.log('Schema ensured.'); + await client.end(); + process.exit(0); + } catch (err) { + console.error('Failed to create schema:', err); + try { await client.end(); } catch {} + process.exit(1); + } +} + +createSchema(); diff --git a/apps/edr-freight-api/src/scripts/run-migrations.ts b/apps/edr-freight-api/src/scripts/run-migrations.ts new file mode 100644 index 000000000..b5cb23078 --- /dev/null +++ b/apps/edr-freight-api/src/scripts/run-migrations.ts @@ -0,0 +1,21 @@ +import { AppDataSource } from '../data-source'; + +async function runMigrations() { + try { + console.log('Initializing datasource...'); + await AppDataSource.initialize(); + console.log('Datasource initialized. Running migrations...'); + const migrations = await AppDataSource.runMigrations(); + console.log(`Applied ${migrations.length} migrations.`); + await AppDataSource.destroy(); + process.exit(0); + } catch (err) { + console.error('Migration run failed:', err); + try { + await AppDataSource.destroy(); + } catch {} + process.exit(1); + } +} + +runMigrations(); diff --git a/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts b/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts index e215dbd9b..c009d21f8 100644 --- a/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts +++ b/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts @@ -4,7 +4,11 @@ import { DataSource } from "typeorm"; import { BookingContainer } from "../modules/bookings/entities/booking-container.entity"; import { Booking } from "../modules/bookings/entities/booking.entity"; -import { Customer } from "../modules/customers/entities/customer.entity"; +import { + Company, + CompanyStatus, + CompanyType, +} from "../modules/companies/entities/company.entity"; import { Locomotive } from "../modules/locomotives/entities/locomotive.entity"; import { ServiceType } from "../modules/rule-engine/entities/service-type.entity"; import { Yard } from "../modules/rule-engine/entities/yard.entity"; @@ -14,7 +18,8 @@ import { ContainerType } from "../modules/rule-engine/entities/container-type.en const SEED_FLAG = "SEED_DEMO_BOOKINGS"; const SERVICE_TYPE_CODE = "RAIL_CONTAINER"; -const CUSTOMER_EMAIL = "train-scheduling-demo@edr.local"; +const COMPANY_EMAIL = "train-scheduling-demo@edr.local"; +const COMPANY_TIN = "1234567890"; const YARDS = [ { code: "DJIBOUTI", label: "Djibouti", country: "Djibouti", displayOrder: 1 }, @@ -183,39 +188,35 @@ export class DemoBookingsSeeder { { conflictPaths: { code: true } }, ); - await manager.getRepository(Customer).upsert( + await manager.getRepository(Company).upsert( { - userId: "00000000-0000-0000-0000-000000000111", - firstName: "Train", - lastName: "Scheduling", - email: CUSTOMER_EMAIL, - phone: "251900000001", - companyName: "Train Scheduling Demo Customer", - companyEmail: CUSTOMER_EMAIL, - companyPhone: "251900000001", - companyLocation: "Addis Ababa", - companyAddress: "Demo Address", - customerType: "DEMO", - status: "ACTIVE", - contactPersonName: "Train Scheduling", - contactPersonPhone: "251900000001", - tinNumber: "1234567890", + name: "Train Scheduling Demo Customer", + type: CompanyType.Customer, + status: CompanyStatus.Active, + tin: COMPANY_TIN, vatNumber: "1234567890", fanNumber: "1234567890123456", + country: "Ethiopia", + address: "Demo Address", + phone: "251900000001", + email: COMPANY_EMAIL, + website: null, + contactPersonName: "Train Scheduling", + contactPersonPhone: "251900000001", generalManagerName: "Demo Manager", - generalManagerEmail: CUSTOMER_EMAIL, + generalManagerEmail: COMPANY_EMAIL, generalManagerPhone: "251900000001", }, - { conflictPaths: { email: true } }, + { conflictPaths: { tin: true } }, ); - const [serviceType, customer, yards, containerTypes] = await Promise.all([ + const [serviceType, company, yards, containerTypes] = await Promise.all([ manager .getRepository(ServiceType) .findOneByOrFail({ code: SERVICE_TYPE_CODE }), manager - .getRepository(Customer) - .findOneByOrFail({ email: CUSTOMER_EMAIL }), + .getRepository(Company) + .findOneByOrFail({ tin: COMPANY_TIN }), manager.getRepository(Yard).find(), manager.getRepository(ContainerType).find(), ]); @@ -247,7 +248,7 @@ export class DemoBookingsSeeder { await manager.getRepository(Booking).upsert( { reference: demoBooking.reference, - companyId: customer.id, + companyId: company.id, status: "APPROVED", scheduledDate: new Date(demoBooking.scheduledDate), totalAmount: 0, diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index ea824762e..6abe397dc 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -24,7 +24,7 @@ import BookingRequestsPage from "./pages/bookings/BookingRequestsPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import OverviewPage from "./pages/dashboard/OverviewPage"; -import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; +//import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage"; import PositionTypesPage from "./pages/dashboard/user-management/PositionTypesPage"; import RolesPage from "./pages/dashboard/user-management/RolesPage"; @@ -34,13 +34,15 @@ import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect"; import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage"; -// import TrainsPage from "./pages/trains/TrainsPage"; -import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; -//import TrainsPage from "./pages/trains/TrainsPage"; -import TrainDetailPage from "./pages/trains/TrainDetailPage"; -import WagonsPage from "./pages/wagons/WagonsPage"; -import ContainersPage from "./pages/containers_management/ContainersPage"; -import CargoesPage from "./pages/cargoes/CargoesPage"; +import TrainsPage from "./pages/trains/TrainsPage"; +import { + CargoesCrudPage, + ContainersCrudPage, + TrainMasterDataPage, + WagonsCrudPage, +} from "./pages/fleet/FleetCrudPages"; +import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; +import TrainDetailPage from "./pages/trains/TrainDetailPage"; const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ { @@ -239,14 +241,13 @@ const App = () => { } - /> - {/* } /> - - } /> */} - } /> - } /> - } /> - } /> + /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx new file mode 100644 index 000000000..6d1cfb545 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx @@ -0,0 +1,238 @@ +import { useState, useEffect } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import axios from 'axios'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Loader2 } from 'lucide-react'; + +interface Cargo { + id: string; + cargoReference: string; + description: string; + quantity: number; + weight: number; + status: 'PENDING' | 'LOADED' | 'IN_TRANSIT' | 'DELIVERED' | 'CANCELLED'; + remarks?: string; +} + +interface CargoFormDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + cargo?: Cargo | null; + onSuccess?: () => void; +} + +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; + +export default function CargoFormDialog({ + open, + onOpenChange, + cargo, + onSuccess, +}: CargoFormDialogProps) { + const queryClient = useQueryClient(); + const [formData, setFormData] = useState>({ + cargoReference: '', + description: '', + quantity: 0, + weight: 0, + status: 'PENDING', + remarks: '', + }); + + useEffect(() => { + if (cargo) { + setFormData(cargo); + } else { + setFormData({ + cargoReference: '', + description: '', + quantity: 0, + weight: 0, + status: 'PENDING', + remarks: '', + }); + } + }, [cargo, open]); + + const createMutation = useMutation({ + mutationFn: (data: Partial) => + axios.post(`${API_BASE_URL}/api/cargoes`, data), + onSuccess: () => { + toast.success('Cargo created successfully'); + onOpenChange(false); + queryClient.invalidateQueries({ queryKey: ['cargoes'] }); + onSuccess?.(); + }, + onError: (error) => { + const message = axios.isAxiosError(error) + ? error.response?.data?.message || 'Failed to create cargo' + : 'Failed to create cargo'; + toast.error(message); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (data: Partial) => + axios.patch(`${API_BASE_URL}/api/cargoes/${cargo?.id}`, data), + onSuccess: () => { + toast.success('Cargo updated successfully'); + onOpenChange(false); + queryClient.invalidateQueries({ queryKey: ['cargoes'] }); + onSuccess?.(); + }, + onError: (error) => { + const message = axios.isAxiosError(error) + ? error.response?.data?.message || 'Failed to update cargo' + : 'Failed to update cargo'; + toast.error(message); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.cargoReference || !formData.description) { + toast.error('Please fill in all required fields'); + return; + } + if (cargo?.id) { + updateMutation.mutate(formData); + } else { + createMutation.mutate(formData); + } + }; + + const isLoading = createMutation.isPending || updateMutation.isPending; + + return ( + + + + + {cargo ? 'Edit Cargo' : 'Create New Cargo'} + + +
+
+
+ + + setFormData({ ...formData, cargoReference: e.target.value }) + } + placeholder="e.g., CRG001" + required + /> +
+
+ + +
+
+ +
+ +