From c3e92dbe701b56d870530ae5af55e83011bd535e Mon Sep 17 00:00:00 2001 From: hagiye Date: Fri, 5 Jun 2026 14:13:35 +0300 Subject: [PATCH 1/3] resolve conflict --- .../train-scheduling.service.ts | 32 -------- .../src/seed/demo-bookings.seeder.ts | 22 ------ apps/edr-freight-web/backoffice/package.json | 1 + .../backoffice/src/components/ui/card.tsx | 1 + .../src/pages/trains/TrainsPage.tsx | 79 +------------------ .../backoffice/src/services/trains.service.ts | 16 ++-- pnpm-lock.yaml | 33 ++++++++ 7 files changed, 44 insertions(+), 140 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/ui/card.tsx 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 68fae00ea..34c4a7843 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 @@ -82,17 +82,6 @@ export class TrainSchedulingService { async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) { const bookingRepository = this.dataSource.getRepository(Booking); const queryBuilder = bookingRepository -<<<<<<< HEAD - .createQueryBuilder('booking') - .leftJoinAndSelect('booking.company', 'company') - .leftJoinAndSelect('booking.originYard', 'originYard') - .leftJoinAndSelect('booking.destinationYard', 'destinationYard') - .leftJoinAndSelect('booking.bookingContainers', 'bookingContainer') - .leftJoinAndSelect('bookingContainer.containerType', 'containerType') - .leftJoin(TrainScheduleBooking, 'scheduleBooking', 'scheduleBooking.booking_id = booking.id') - .where('booking.freightType = :freightType', { freightType: 'CONTAINER' }) - .andWhere('scheduleBooking.id IS NULL'); -======= .createQueryBuilder("booking") .leftJoinAndSelect("booking.customer", "customer") .leftJoinAndSelect("booking.originYard", "originYard") @@ -106,7 +95,6 @@ export class TrainSchedulingService { ) .where("booking.freightType = :freightType", { freightType: "CONTAINER" }) .andWhere("scheduleBooking.id IS NULL"); ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 if (query.originStationId) { queryBuilder.andWhere("booking.originYardId = :originStationId", { @@ -144,13 +132,6 @@ export class TrainSchedulingService { const items: EligibleBookingItem[] = bookings.map((booking) => ({ id: booking.id, reference: booking.reference, -<<<<<<< HEAD - customer: booking.company?.name ?? booking.company?.email ?? 'Unknown customer', - containerType: booking.bookingContainers - ?.map((container) => container.containerType?.label ?? container.containerType?.code ?? 'Container') - .join(', ') ?? 'Container', - quantity: booking.bookingContainers?.reduce((sum, container) => sum + Number(container.quantity ?? 0), 0) ?? 0, -======= customer: booking.company?.name ?? booking.company?.email ?? "Unknown customer", containerType: @@ -167,7 +148,6 @@ export class TrainSchedulingService { (sum, container) => sum + Number(container.quantity ?? 0), 0, ) ?? 0, ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 weightTons: this.roundTons(booking.cargoTotalWeightVgm), origin: booking.originYard?.label ?? @@ -674,17 +654,6 @@ export class TrainSchedulingService { } async getContainerTrainScheduleById(id: string) { -<<<<<<< HEAD - const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({ - where: { id }, - relations: { - trainSet: { locomotive: true, wagons: { wagonType: true, allocations: { booking: true } } }, - originStation: true, - destinationStation: true, - scheduleBookings: { booking: { company: true, originYard: true, destinationYard: true } }, - }, - }); -======= const schedule = await this.dataSource .getRepository(TrainSchedule) .findOne({ @@ -701,7 +670,6 @@ export class TrainSchedulingService { }, }, }); ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 if (!schedule) { throw new NotFoundException(`Train schedule ${id} not found`); 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 da835dabd..e215dbd9b 100644 --- a/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts +++ b/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts @@ -2,16 +2,6 @@ import { Injectable, Logger } from "@nestjs/common"; import { randomUUID } from "crypto"; import { DataSource } from "typeorm"; -<<<<<<< HEAD -import { BookingContainer } from '../modules/bookings/entities/booking-container.entity'; -import { Booking } from '../modules/bookings/entities/booking.entity'; -import { Company } 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'; -import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity'; -import { ContainerType } from '../modules/rule-engine/entities/container-type.entity'; -======= 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"; @@ -20,7 +10,6 @@ import { ServiceType } from "../modules/rule-engine/entities/service-type.entity import { Yard } from "../modules/rule-engine/entities/yard.entity"; import { WagonType } from "../modules/wagon-types/entities/wagon-type.entity"; import { ContainerType } from "../modules/rule-engine/entities/container-type.entity"; ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 const SEED_FLAG = "SEED_DEMO_BOOKINGS"; @@ -220,11 +209,6 @@ export class DemoBookingsSeeder { { conflictPaths: { email: true } }, ); -<<<<<<< HEAD - const [serviceType, company, yards, containerTypes] = await Promise.all([ - manager.getRepository(ServiceType).findOneByOrFail({ code: SERVICE_TYPE_CODE }), - manager.getRepository(Customer).findOneByOrFail({ email: CUSTOMER_EMAIL }), -======= const [serviceType, customer, yards, containerTypes] = await Promise.all([ manager .getRepository(ServiceType) @@ -232,7 +216,6 @@ export class DemoBookingsSeeder { manager .getRepository(Customer) .findOneByOrFail({ email: CUSTOMER_EMAIL }), ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 manager.getRepository(Yard).find(), manager.getRepository(ContainerType).find(), ]); @@ -264,13 +247,8 @@ export class DemoBookingsSeeder { await manager.getRepository(Booking).upsert( { reference: demoBooking.reference, -<<<<<<< HEAD - companyId: company.id, - status: 'APPROVED', -======= companyId: customer.id, status: "APPROVED", ->>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8 scheduledDate: new Date(demoBooking.scheduledDate), totalAmount: 0, paymentStatus: "PENDING", diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index 7499e38a2..9d5843372 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -16,6 +16,7 @@ "@edr/ui-common": "workspace:*", "@tanstack/react-query": "^5.100.11", "@tria-plc/iamui-common": "1.1.1", + "@hello-pangea/dnd": "^18.0.1", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/apps/edr-freight-web/backoffice/src/components/ui/card.tsx b/apps/edr-freight-web/backoffice/src/components/ui/card.tsx new file mode 100644 index 000000000..524e7794f --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/ui/card.tsx @@ -0,0 +1 @@ +export { Card, CardHeader, CardTitle, CardDescription, CardFooter, CardAction, CardContent } from '@edr/ui-common'; 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 ef6dba506..9794e86f3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx @@ -1,42 +1,3 @@ -<<<<<<< HEAD -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' }); - } - }; - - 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})} />
- -
-======= import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; @@ -794,45 +755,7 @@ const TrainsPage = () => { ->>>>>>> 523d7e58422f1bde8024c2dd237092a7cf6aa190 ); }; -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 +export default TrainsPage; \ 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 index 57f0c5630..aaea7b097 100644 --- a/apps/edr-freight-web/backoffice/src/services/trains.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/trains.service.ts @@ -1,5 +1,5 @@ -import { apiClient } from '@/lib/axios'; - +//import { apiClient } from '@/lib/axios'; +import { api as client } from "@/auth/http"; export interface Train { id: string; code: string; @@ -17,10 +17,10 @@ export interface Train { } 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`), + getAll: () => client.get('/trains'), + getById: (id: string) => client.get(`/trains/${id}`), + create: (data: Partial) => client.post('/trains', data), + update: (id: string, data: Partial) => client.patch(`/trains/${id}`, data), + delete: (id: string) => client.delete(`/trains/${id}`), + getDetails: (id: string) => client.get(`/trains/${id}/details`), }; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d01697188..8f68078f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: '@edr/ui-common': specifier: workspace:* version: link:../../../packages/ui-common + '@hello-pangea/dnd': + specifier: ^18.0.1 + version: 18.0.1(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-query': specifier: ^5.100.11 version: 5.100.11(react@19.2.6) @@ -1377,6 +1380,12 @@ packages: '@gilbarbara/deep-equal@0.3.1': resolution: {integrity: sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==} + '@hello-pangea/dnd@18.0.1': + resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==} + peerDependencies: + react: 19.2.6 + react-dom: 19.2.6 + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -5331,6 +5340,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + css-line-break@2.1.0: resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} @@ -8537,6 +8549,9 @@ packages: '@types/react-dom': optional: true + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} @@ -11264,6 +11279,18 @@ snapshots: '@gilbarbara/deep-equal@0.3.1': {} + '@hello-pangea/dnd@18.0.1(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + css-box-model: 1.2.1 + raf-schd: 4.0.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-redux: 9.3.0(@types/react@18.3.29)(react@19.2.6)(redux@5.0.1) + redux: 5.0.1 + transitivePeerDependencies: + - '@types/react' + '@hono/node-server@1.19.14(hono@4.12.22)': dependencies: hono: 4.12.22 @@ -16070,6 +16097,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + css-line-break@2.1.0: dependencies: utrie: 1.0.2 @@ -19735,6 +19766,8 @@ snapshots: '@types/react': 18.3.29 '@types/react-dom': 18.3.7(@types/react@18.3.29) + raf-schd@4.0.3: {} + raf@3.4.1: dependencies: performance-now: 2.1.0 From dc42838a43d9f982af0e29acfcfca5f81bda30d9 Mon Sep 17 00:00:00 2001 From: hagiye Date: Fri, 5 Jun 2026 21:07:04 +0300 Subject: [PATCH 2/3] 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 + /> +
+
+ + +
+
+ +
+ +