diff --git a/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts b/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts new file mode 100644 index 000000000..4a59e33b1 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +/** + * Fix for fresh deployments: AddWarehouseInspection1750000000003 runs before + * the warehouse_inventory table exists, so it cannot add inspection_status. + */ +export class AddWarehouseInventoryInspectionStatusFix1791000000005 implements MigrationInterface { + private readonly table = 'freight.warehouse_inventory'; + + public async up(queryRunner: QueryRunner): Promise { + if ((await queryRunner.hasTable(this.table)) && !(await queryRunner.hasColumn(this.table, 'inspection_status'))) { + await queryRunner.addColumn( + this.table, + new TableColumn({ name: 'inspection_status', type: 'varchar', length: '20', isNullable: true }), + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + if ((await queryRunner.hasTable(this.table)) && (await queryRunner.hasColumn(this.table, 'inspection_status'))) { + await queryRunner.dropColumn(this.table, 'inspection_status'); + } + } +} diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.module.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.module.ts index e38fc4d75..6ed9b8ed5 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.module.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.module.ts @@ -15,6 +15,7 @@ import { TrainSchedulesModule } from '../train-schedules/train-schedules.module' import { WagonType } from '../wagon-types/entities/wagon-type.entity'; import { WagonTypesModule } from '../wagon-types/wagon-types.module'; import { Wagon } from '../wagons/entities/wagon.entity'; +import { WarehousesModule } from '../warehouses/warehouses.module'; import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity'; import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity'; import { TrainCheckpointEventsRepository } from './train-checkpoint-events.repository'; @@ -44,6 +45,7 @@ import { NotificationsModule } from '../notifications/notifications.module'; WagonTypesModule, TrainSetsModule, TrainSchedulesModule, + WarehousesModule, RuleEngineModule, ], controllers: [TrainSchedulingController], diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts index 6f22f7a83..f59597aaa 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts @@ -147,6 +147,10 @@ describe('TrainSchedulingService', () => { wagonAllocationBulkLoadsRepository as never, trainCheckpointEventsRepository as never, {} as never, // trainCompositionRemovalLogRepository + { + autoUnloadArrivedBookings: jest.fn(), + autoUnloadExportAtDjibouti: jest.fn(), + } as never, ); const defaultFleetWagons = [ 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 92aca8ac8..256e6bcdd 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 @@ -95,6 +95,8 @@ import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity'; import { TrainCheckpointEventsRepository } from './train-checkpoint-events.repository'; import { RecordCheckpointDto } from './dto/record-checkpoint.dto'; import { RouteMilestone } from '../routes/entities/route-milestone.entity'; +import { deriveTradeDirection } from '../../common/derive-trade-direction.util'; +import { WarehouseInventoryService } from '../warehouses/warehouse-inventory.service'; import { autoFillPlacements, findMissingContainerNumberIssues, @@ -167,6 +169,7 @@ export class TrainSchedulingService { private readonly wagonAllocationBulkLoadsRepository: WagonAllocationBulkLoadsRepository, private readonly trainCheckpointEventsRepository: TrainCheckpointEventsRepository, private readonly trainCompositionRemovalLogRepository: TrainCompositionRemovalLogRepository, + private readonly warehouseInventoryService: WarehouseInventoryService, private readonly configService?: ConfigService, ) {} @@ -602,6 +605,74 @@ export class TrainSchedulingService { return this.getTrainScheduleById(scheduleId); } + private async runWarehouseArrivalAutomation(scheduleId: string) { + const [schedule]: Array<{ + originCountry: string | null; + destinationCountry: string | null; + destinationCode: string | null; + destinationName: string | null; + }> = await this.dataSource.query( + `SELECT oy.country AS "originCountry", + dy.country AS "destinationCountry", + dy.code AS "destinationCode", + dy.name AS "destinationName" + FROM freight.train_schedules ts + LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id + LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id + WHERE ts.id = $1 AND ts.deleted_at IS NULL + LIMIT 1`, + [scheduleId], + ); + + if (!schedule) return { status: 'SKIPPED', reason: 'Train schedule not found' }; + + const direction = deriveTradeDirection( + { country: schedule.originCountry }, + { country: schedule.destinationCountry }, + ); + + try { + if (direction === 'IMPORT') { + return { + direction, + action: 'IMPORT_AUTO_UNLOAD', + status: 'COMPLETED', + result: await this.warehouseInventoryService.autoUnloadArrivedBookings( + scheduleId, + 'SYSTEM_TRAIN_ARRIVAL', + ), + }; + } + + if (direction === 'EXPORT' && this.isDjiboutiPortDestination(`${schedule.destinationCode ?? ''} ${schedule.destinationName ?? ''}`)) { + return { + direction, + action: 'EXPORT_DJIBOUTI_AUTO_UNLOAD', + status: 'COMPLETED', + result: await this.warehouseInventoryService.autoUnloadExportAtDjibouti( + scheduleId, + 'SYSTEM_TRAIN_ARRIVAL', + ), + }; + } + + return { direction, status: 'SKIPPED', reason: 'No warehouse arrival automation for this route' }; + } catch (error) { + return { + direction, + status: 'FAILED', + reason: error instanceof Error ? error.message : String(error), + }; + } + } + + private isDjiboutiPortDestination(value: string | null | undefined): boolean { + const normalized = (value ?? '').toUpperCase(); + return ['DJIBOUTI', 'DORALEH', 'DMP', 'DCT', 'NAGAD'].some((token) => + normalized.includes(token), + ); + } + async pinWagons(scheduleId: string, dto: PinWagonsDto) { const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId); if (!schedule) { @@ -653,7 +724,9 @@ export class TrainSchedulingService { } }); - return this.getTrainScheduleById(scheduleId); + const detail = await this.getTrainScheduleById(scheduleId); + const warehouseAutomation = await this.runWarehouseArrivalAutomation(scheduleId); + return Object.assign(detail, { warehouseAutomation }); } async finalizeSchedule(scheduleId: string) { diff --git a/apps/edr-freight-web/backoffice/src/auth/http.ts b/apps/edr-freight-web/backoffice/src/auth/http.ts index 5aa78e2d3..25712c742 100644 --- a/apps/edr-freight-web/backoffice/src/auth/http.ts +++ b/apps/edr-freight-web/backoffice/src/auth/http.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import { API_BASE_URL } from "@/constants/apiConfig"; +import { API_BASE_URL } from "@/pages/fleet/config/vehicles"; import { AUTH_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx index 625e32a3a..ffaf471b3 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx @@ -14,7 +14,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Loader2 } from 'lucide-react'; -import { API_BASE_URL } from '@/constants/apiConfig'; +import { API_BASE_URL } from "@/pages/fleet/config/vehicles"; interface Cargo { id: string; diff --git a/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts index 030b051a1..0a57a6f31 100644 --- a/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts +++ b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts @@ -1,3 +1,3 @@ export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; -// export const API_BASE_URL = 'http://localhost:3001'; +//export const API_BASE_URL = 'http://localhost:3001'; diff --git a/apps/edr-freight-web/backoffice/src/hooks/bookings/useBookings.ts b/apps/edr-freight-web/backoffice/src/hooks/bookings/useBookings.ts index 9b5f2b489..6ba2214e2 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/bookings/useBookings.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/bookings/useBookings.ts @@ -1,4 +1,5 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; import toast from "react-hot-toast"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; @@ -9,6 +10,16 @@ import { } from "@/services/bookings.service"; import { invalidateBookingDetail } from "@/utils/queryInvalidation"; +const parseApiError = (error: unknown, fallback: string) => { + if (isAxiosError(error)) { + const message = error.response?.data?.message; + if (Array.isArray(message)) return message.join(", "); + if (typeof message === "string") return message; + } + if (error instanceof Error && error.message) return error.message; + return fallback; +}; + export function useBookingList(filter?: BookingListFilter, enabled = true) { return useQuery({ queryKey: QUERY_KEYS.BOOKINGS.list(filter), @@ -85,7 +96,7 @@ export function useBookingMutations(bookingId: string) { requiredRole, }), onSuccess: (data) => onSuccess(data, "Approval step completed"), - onError: () => toast.error("Failed to approve step"), + onError: (error) => toast.error(parseApiError(error, "Failed to approve step")), }); const rejectStep = useMutation({ @@ -102,7 +113,7 @@ export function useBookingMutations(bookingId: string) { reason, }), onSuccess: (data) => onSuccess(data, "Booking rejected at approval step"), - onError: () => toast.error("Failed to reject step"), + onError: (error) => toast.error(parseApiError(error, "Failed to reject step")), }); const generateContract = useMutation({ diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx index 735f95c07..e685a3cf4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx @@ -42,19 +42,10 @@ import { useEffect, useMemo, useState } from "react"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; -import { api } from "@/auth/http"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; -import { URL_CONSTANTS } from "@/constants/URLS"; import { api as appApi } from "@/services/api"; import { bookingsService } from "@/services/bookings.service"; -import { unwrap } from "@/utils/endpoint"; - -interface CompanyOption { - id: string; - name?: string | null; - tin?: string | null; - email?: string | null; -} +import { customersService } from "@/services/customers.service"; type FreightType = "CONTAINER" | "BULK"; @@ -219,15 +210,12 @@ export default function NewBookingPage() { queryFn: () => bookingsService.getReferenceData() as Promise, }); - const { data: companies, isLoading: companiesLoading } = useQuery({ + const { data: companiesPage, isLoading: companiesLoading } = useQuery({ queryKey: ["companies", "list"], - queryFn: async () => { - const res = await api.get(URL_CONSTANTS.COMPANIES.BASE); - return unwrap(res.data) as CompanyOption[]; - }, + queryFn: () => customersService.list({ page: 1, pageSize: 1000 }), }); - const companyOptions = (companies ?? []).map((c) => ({ + const companyOptions = (companiesPage?.items ?? []).map((c) => ({ value: c.id, label: c.name || c.email || c.tin || c.id, })); diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts index fc646a8bc..2c9b0c132 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts @@ -92,3 +92,6 @@ export const vehiclesConfig: FleetResourceConfig = { }; export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS }; +// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; + + export const API_BASE_URL = 'http://localhost:3001'; diff --git a/apps/edr-freight-web/portal/src/constants/apiConfig.ts b/apps/edr-freight-web/portal/src/constants/apiConfig.ts index dce8aad63..7293dedd0 100644 --- a/apps/edr-freight-web/portal/src/constants/apiConfig.ts +++ b/apps/edr-freight-web/portal/src/constants/apiConfig.ts @@ -1,3 +1,2 @@ export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; // export const API_BASE_URL = 'http://localhost:3001'; -