From e0010695be5f81c894ea1f471fb2d8131c050cdc Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 09:41:52 +0000 Subject: [PATCH 01/30] fix --- .../src/pages/operations/FirstMilePage.tsx | 73 ++++++++++--------- .../src/pages/operations/LastMilePage.tsx | 65 +++++++++-------- 2 files changed, 72 insertions(+), 66 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 35618fa3b..01e5ded57 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -122,42 +122,45 @@ const InfoRow = ({ label, value }: { label: string; value: string }) => ( ); -const BookingInfo = ({ record }: { record: FirstMileRecord }) => ( - - - - {bookingRef(record)} - - - {STATUS_META[record.status].label} - - - {isAssigned(record) ? "Assigned" : "Unassigned"} - +const BookingInfo = ({ record }: { record: FirstMileRecord }) => { + const hasPickupAddress = record.booking?.firstMilePickupAddress != null; + return ( + + + + {bookingRef(record)} + + + {STATUS_META[record.status].label} + + + {isAssigned(record) ? "Assigned" : "Unassigned"} + + - - - - - - - - - - - - - - - - - - -); + + + + {hasPickupAddress && } + + + + + + + + + + + + + + ); +}; const tripSlipRows = (record: FirstMileRecord): [string, string][] => [ ["Customer", customerName(record)], diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 70d9e9105..e85fbfd2d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -117,38 +117,41 @@ const InfoRow = ({ label, value }: { label: string; value: string }) => ( ); -const BookingInfo = ({ record }: { record: LastMileRecord }) => ( - - - - {bookingRef(record)} - - - {STATUS_META[record.status].label} - - - {isAssigned(record) ? "Assigned" : "Unassigned"} - +const BookingInfo = ({ record }: { record: LastMileRecord }) => { + const hasDeliveryAddress = record.booking?.lastMileDeliveryAddress != null; + return ( + + + + {bookingRef(record)} + + + {STATUS_META[record.status].label} + + + {isAssigned(record) ? "Assigned" : "Unassigned"} + + - - - - - - - - - - - - - - - - - - -); + + + + + {hasDeliveryAddress && } + + + + + + + + + + + + + ); +}; const tripSlipRows = (record: LastMileRecord): [string, string][] => [ ["Customer", customerName(record)], From 10e442deb1a8a0a627fa2e97c6c79cdecf2b067b Mon Sep 17 00:00:00 2001 From: yaschalew Date: Thu, 2 Jul 2026 13:18:49 +0300 Subject: [PATCH 02/30] fix --- .../src/modules/first-mile/first-mile.controller.ts | 6 ++++++ .../src/modules/first-mile/first-mile.service.ts | 11 ++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.controller.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.controller.ts index 680bb5d09..d5f53ae0c 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.controller.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.controller.ts @@ -65,6 +65,12 @@ export class FirstMileController { return this.firstMileService.findById(id); } + @Get('acceptitem/:id') + @ApiOperation({ summary: 'Get a first-mile accep by ID' }) + acceptItem(@Param('id', ParseUUIDPipe) id: string) { + return this.firstMileService.acceptBooking(id); + } + @Post('accept/:reference') @TrainSchedulingManage() @ApiOperation({ summary: 'Accept a paid booking and create a first-mile leg' }) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index ae0ada831..a7a4f9100 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -43,7 +43,7 @@ export class FirstMileService { private readonly vehiclesService: VehiclesService, private readonly driversService: DriversService, private readonly smsClient: SmsClientService, - ) {} + ) { } /** * Look up a booking by its human-readable reference and confirm it has been @@ -95,7 +95,7 @@ export class FirstMileService { if (booking.paymentStatus !== 'PAID') { throw new BadRequestException(`Booking ${label} is not paid`); } - + console.log("-------------------", booking) if (!this.bookingRequestsFirstMile(booking)) { throw new BadRequestException(`Booking ${label} does not require a first mile`); } @@ -212,11 +212,8 @@ export class FirstMileService { }): boolean { // Export bookings always need a first mile (pickup → origin yard); the // pickup address is captured at assignment time, not required upfront. - return Boolean( - booking.tradeDirection === 'EXPORT' || - booking.firstMilePickupAddress?.trim() || - booking.serviceType?.includesFirstMile, - ); + return Boolean(booking.firstMilePickupAddress?.trim() || + booking.serviceType?.includesFirstMile); } async update(id: string, dto: UpdateFirstMileDto): Promise { From 1c88bd8d686b4b97cd5d26d39f0aa3a7bcef3adb Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 10:27:01 +0000 Subject: [PATCH 03/30] fix --- .../backoffice/src/pages/operations/FirstMilePage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 01e5ded57..9f6e82d03 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -469,9 +469,14 @@ const FirstMilePage = () => { const firstMileEligiblePaidBookings = useMemo( () => paidBookings.filter( - (booking) => - booking.tradeDirection === "EXPORT" && - !existingFirstMileBookingIds.has(booking.id), + (booking) => { + if (existingFirstMileBookingIds.has(booking.id)) return false; + return ( + booking.tradeDirection === "EXPORT" || + (booking.firstMilePickupAddress?.trim() ?? false) || + booking.serviceType?.includesFirstMile + ); + }, ), [existingFirstMileBookingIds, paidBookings], ); From 4519a1d75d4b7dc09646cec9be45404429a7d863 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 10:33:32 +0000 Subject: [PATCH 04/30] fix --- .../src/modules/first-mile/first-mile.service.ts | 9 +++++---- .../backoffice/src/pages/operations/FirstMilePage.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index a7a4f9100..bc8801067 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -210,10 +210,11 @@ export class FirstMileService { firstMilePickupAddress?: string | null; serviceType?: { includesFirstMile?: boolean | null } | null; }): boolean { - // Export bookings always need a first mile (pickup → origin yard); the - // pickup address is captured at assignment time, not required upfront. - return Boolean(booking.firstMilePickupAddress?.trim() || - booking.serviceType?.includesFirstMile); + return Boolean( + booking.tradeDirection === 'EXPORT' && + (booking.firstMilePickupAddress?.trim() || + booking.serviceType?.includesFirstMile), + ); } async update(id: string, dto: UpdateFirstMileDto): Promise { diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 9f6e82d03..93e3e51b0 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -471,11 +471,11 @@ const FirstMilePage = () => { paidBookings.filter( (booking) => { if (existingFirstMileBookingIds.has(booking.id)) return false; - return ( - booking.tradeDirection === "EXPORT" || - (booking.firstMilePickupAddress?.trim() ?? false) || - booking.serviceType?.includesFirstMile - ); + if (booking.paymentStatus !== "PAID") return false; + if (booking.tradeDirection !== "EXPORT") return false; + const hasPickupAddress = booking.firstMilePickupAddress?.trim() ?? false; + const includesFirstMile = booking.serviceType?.includesFirstMile ?? false; + return hasPickupAddress || includesFirstMile; }, ), [existingFirstMileBookingIds, paidBookings], From f4f98575f118216219c4dfbcda4eaf0dbab052f9 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 10:48:31 +0000 Subject: [PATCH 05/30] fix error --- .../backoffice/src/pages/operations/LastMilePage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index e85fbfd2d..9d54326e1 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -354,6 +354,10 @@ const LastMilePage = () => { }); const records = listData?.data ?? []; + const existingLastMileBookingIds = useMemo( + () => new Set(records.map((record) => record.bookingId)), + [records], + ); const vehicleOptions = useMemo( () => @@ -414,15 +418,16 @@ const LastMilePage = () => { const arrivalQueue = arrivalQueueData ?? []; const filteredArrivalQueue = useMemo(() => { + let filtered = arrivalQueue.filter((item) => !existingLastMileBookingIds.has(item.bookingId)); const term = arrivalSearch.trim().toLowerCase(); - if (!term) return arrivalQueue; - return arrivalQueue.filter((item) => + if (!term) return filtered; + return filtered.filter((item) => [item.bookingReference, item.customer, item.cargo, item.warehouse, item.yard] .join(" ") .toLowerCase() .includes(term), ); - }, [arrivalQueue, arrivalSearch]); + }, [arrivalQueue, arrivalSearch, existingLastMileBookingIds]); const acceptMutation = useMutation({ mutationFn: async ({ items, vehicleId }: { items: ArrivalQueueItem[]; vehicleId: string | null }) => { From e59a8f980244fa15f90944ffb548657f11a7dd15 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 10:54:43 +0000 Subject: [PATCH 06/30] fix delete api --- .../src/pages/operations/FirstMilePage.tsx | 29 +++++++++++++++++++ .../src/pages/operations/LastMilePage.tsx | 29 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 93e3e51b0..198fe50a3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -7,6 +7,7 @@ import { Printer, RefreshCw, Ruler, + Trash, Truck, } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -420,6 +421,17 @@ const FirstMilePage = () => { }, }); + const deleteMutation = useMutation({ + mutationFn: (id: string) => firstMileService.remove(id), + onSuccess: () => { + void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() }); + toast({ title: "Record deleted", description: "First-mile record removed successfully." }); + }, + onError: () => { + toast({ title: "Delete failed", variant: "destructive" }); + }, + }); + const acceptMutation = useMutation({ mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => { const res = await firstMileService.accept(reference); @@ -837,6 +849,7 @@ const FirstMilePage = () => { const assigned = isAssigned(row.original); const nextStatus = NEXT_STATUS[row.original.status]; const canPrint = row.original.status !== "PAYMENT_PENDING"; + const isPaid = (row.original as any).paid; return ( @@ -889,6 +902,22 @@ const FirstMilePage = () => { Print trip slip )} + {!isPaid && ( + <> + + } + color="red" + onClick={() => { + if (confirm(`Delete first-mile record ${bookingRef(row.original)}?`)) { + deleteMutation.mutate(row.original.id); + } + }} + > + Delete + + + )} diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 9d54326e1..eb00c0904 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -6,6 +6,7 @@ import { Printer, RefreshCw, Ruler, + Trash, Truck, } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -397,6 +398,17 @@ const LastMilePage = () => { }, }); + const deleteMutation = useMutation({ + mutationFn: (id: string) => lastMileService.remove(id), + onSuccess: () => { + void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.list() }); + toast({ title: "Record deleted", description: "Last-mile record removed successfully." }); + }, + onError: () => { + toast({ title: "Delete failed", variant: "destructive" }); + }, + }); + const allocateMutation = useMutation({ mutationFn: (data: Array<{ containerId: string; vehicleId: string }>) => api.post(`/last-mile/${activeId}/allocate-containers`, data), @@ -817,6 +829,7 @@ const LastMilePage = () => { const assigned = isAssigned(row.original); const nextStatus = NEXT_STATUS[row.original.status]; const canPrint = row.original.status !== "PAYMENT_PENDING"; + const isPaid = (row.original as any).paid; return ( @@ -869,6 +882,22 @@ const LastMilePage = () => { Print trip slip )} + {!isPaid && ( + <> + + } + color="red" + onClick={() => { + if (confirm(`Delete last-mile record ${bookingRef(row.original)}?`)) { + deleteMutation.mutate(row.original.id); + } + }} + > + Delete + + + )} From bab329e8e5f14bf20ce6771169f4be4708ea26b1 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 10:57:35 +0000 Subject: [PATCH 07/30] fix --- .../backoffice/src/services/first-mile.service.ts | 4 +++- .../backoffice/src/services/last-mile.service.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts index b182a16d0..1c81f5dc5 100644 --- a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts @@ -60,8 +60,10 @@ export const firstMileService = { list: (pageSize = 1000) => api.get(`${FM.BASE}?pageSize=${pageSize}`), getById: (id: string) => api.get(FM.BY_ID(id)), - update: (id: string, data: { status?: FirstMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null }) => + update: (id: string, data: { status?: FirstMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null; paid?: boolean }) => api.patch(FM.BY_ID(id), data), accept: (bookingReference: string) => api.post(FM.ACCEPT(bookingReference)), + remove: (id: string) => + api.delete(FM.BY_ID(id)), }; diff --git a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts index cb9e61b52..158056e46 100644 --- a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts @@ -60,8 +60,10 @@ export const lastMileService = { list: (pageSize = 1000) => api.get(`${LM.BASE}?pageSize=${pageSize}`), getById: (id: string) => api.get(LM.BY_ID(id)), - update: (id: string, data: { status?: LastMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null }) => + update: (id: string, data: { status?: LastMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null; paid?: boolean }) => api.patch(LM.BY_ID(id), data), accept: (bookingReference: string) => api.post(LM.ACCEPT(encodeURIComponent(bookingReference))), + remove: (id: string) => + api.delete(LM.BY_ID(id)), }; From ff7e20d2ee3a1cf25c038ae60d5d829104fbae69 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 11:22:35 +0000 Subject: [PATCH 08/30] fix --- .../1870000000000-AddLocationToVehicles.ts | 22 +++++++++++++++++++ .../vehicles/entities/vehicle.entity.ts | 3 +++ .../src/pages/fleet/config/vehicles.ts | 2 ++ .../src/services/vehicles.service.ts | 3 ++- 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts diff --git a/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts b/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts new file mode 100644 index 000000000..3434631a2 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Add location_id column to vehicles table to track vehicle base location. + */ +export class AddLocationToVehicles1870000000000 implements MigrationInterface { + name = "AddLocationToVehicles1870000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + ADD COLUMN IF NOT EXISTS location_id uuid; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + DROP COLUMN IF EXISTS location_id; + `); + } +} diff --git a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts index fef078ef8..1e4121802 100644 --- a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts +++ b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts @@ -68,4 +68,7 @@ export class Vehicle extends BaseEntity { @Column({ name: 'actual_distance_km', type: 'numeric', nullable: true }) actualDistanceKm?: number; + + @Column({ name: 'location_id', type: 'uuid', nullable: true }) + locationId?: string; } 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 92da9b9dc..648c0579d 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 @@ -74,6 +74,7 @@ export const vehiclesConfig: FleetResourceConfig = { { name: "year", label: "Year", type: "number", required: true }, { name: "fuelType", label: "Fuel Type", type: "select", required: true, options: FUEL_TYPE_OPTIONS }, { name: "capacity", label: "Capacity", type: "number", required: true }, + { name: "locationId", label: "Location", type: "select", dataSource: "yards" }, { name: "estimatedDistanceKm", label: "Estimated Distance (KM)", type: "number" }, { name: "actualDistanceKm", label: "Actual Distance (KM)", type: "number" }, { name: "status", label: "Status", type: "select", required: true, options: VEHICLE_STATUS_OPTIONS }, @@ -90,6 +91,7 @@ export const vehiclesConfig: FleetResourceConfig = { year: new Date().getFullYear(), fuelType: "DIESEL", capacity: 0, + locationId: null, estimatedDistanceKm: "", actualDistanceKm: "", status: "ACTIVE", diff --git a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts index b9d5129e3..3cb2ad358 100644 --- a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts @@ -29,6 +29,7 @@ export interface Vehicle { code?: string | null; powerPlateNo?: string | null; trailerPlateNo?: string | null; + locationId?: string | null; createdAt: string; updatedAt: string; } @@ -55,7 +56,7 @@ export const vehiclesService = { getById: (id: string) => apiClient.get(URL_CONSTANTS.VEHICLES.BY_ID(id)), create: (data: Partial) => apiClient.post(URL_CONSTANTS.VEHICLES.BASE, data), - update: (id: string, data: Partial) => + update: (id: string, data: Partial) => apiClient.patch(URL_CONSTANTS.VEHICLES.BY_ID(id), data), delete: (id: string) => apiClient.delete(URL_CONSTANTS.VEHICLES.BY_ID(id)), }; From adde5962895699d5ef23e986adb83873c88a04b7 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 11:32:16 +0000 Subject: [PATCH 09/30] fix --- .../backoffice/src/pages/fleet/config/vehicles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 648c0579d..6ac04e7aa 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 @@ -74,7 +74,7 @@ export const vehiclesConfig: FleetResourceConfig = { { name: "year", label: "Year", type: "number", required: true }, { name: "fuelType", label: "Fuel Type", type: "select", required: true, options: FUEL_TYPE_OPTIONS }, { name: "capacity", label: "Capacity", type: "number", required: true }, - { name: "locationId", label: "Location", type: "select", dataSource: "yards" }, + { name: "locationId", label: "Location", type: "select", dynamicOptions: "yards" }, { name: "estimatedDistanceKm", label: "Estimated Distance (KM)", type: "number" }, { name: "actualDistanceKm", label: "Actual Distance (KM)", type: "number" }, { name: "status", label: "Status", type: "select", required: true, options: VEHICLE_STATUS_OPTIONS }, From a0f4de05f859113cb26699b7d4ca7fb8d06afaa0 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 12:37:02 +0000 Subject: [PATCH 10/30] log full error --- .../src/modules/vehicles/dto/create-vehicle.dto.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts index b5f9abcb8..5651a3906 100644 --- a/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts +++ b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts @@ -57,4 +57,8 @@ export class CreateVehicleDto { @IsOptional() @IsNumber() actualDistanceKm?: number; + + @IsOptional() + @IsUUID() + locationId?: string; } From 657f3bd2ab962b1b0ffb8210e641dcf474889fe6 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 12:39:47 +0000 Subject: [PATCH 11/30] fix --- .../src/pages/fleet/config/vehicles.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) 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 6ac04e7aa..6f65224bc 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 @@ -49,18 +49,11 @@ export const vehiclesConfig: FleetResourceConfig = { columns: [ { id: "code", header: "Code", accessorKey: "code", format: "code", size: 110 }, { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", format: "code", size: 130 }, - { id: "powerPlateNo", header: "Power Plate No", accessorKey: "powerPlateNo", format: "code", size: 140 }, - { id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", format: "code", size: 140 }, - { id: "registrationNumber", header: "Registration", accessorKey: "registrationNumber", format: "code", size: 140 }, - { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 140 }, - { id: "model", header: "Model", accessorKey: "model", format: "code", size: 120 }, - { id: "vehicleType", header: "Type", accessorKey: "vehicleType", format: "code", size: 100 }, - { id: "year", header: "Year", accessorKey: "year", format: "number", size: 80 }, - { id: "fuelType", header: "Fuel Type", accessorKey: "fuelType", format: "code", size: 110 }, - { id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 130 }, - { id: "assignedDriverName", header: "Assigned Driver", accessorKey: "assignedDriverName", format: "code", size: 140 }, - { id: "estimatedDistanceKm", header: "Est. Distance (KM)", accessorKey: "estimatedDistanceKm", format: "number", size: 150 }, - { id: "actualDistanceKm", header: "Actual Distance (KM)", accessorKey: "actualDistanceKm", format: "number", size: 150 }, + { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 120 }, + { id: "model", header: "Model", accessorKey: "model", format: "code", size: 100 }, + { id: "vehicleType", header: "Type", accessorKey: "vehicleType", format: "code", size: 80 }, + { id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 100 }, + { id: "locationId", header: "Location", accessorKey: "locationId", format: "code", size: 130 }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 }, ], formFields: [ From 58b3805d0bd4d60eb3f6e362adf2d92910a354eb Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 12:43:03 +0000 Subject: [PATCH 12/30] fix --- .../1880000000000-AddVehicleStatuses.ts | 22 +++++++++++++++++++ .../vehicles/entities/vehicle.entity.ts | 2 ++ .../src/components/fleet/fleetFormat.tsx | 2 ++ .../src/pages/fleet/FleetResourcePage.tsx | 1 + .../src/pages/fleet/config/vehicles.ts | 14 +++++++----- .../src/services/vehicles.service.ts | 2 +- 6 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts diff --git a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts new file mode 100644 index 000000000..29da135c3 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Add FREE and BUSY statuses to vehicle status enum. + */ +export class AddVehicleStatuses1880000000000 implements MigrationInterface { + name = "AddVehicleStatuses1880000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'FREE' BEFORE 'MAINTENANCE'; + `); + await queryRunner.query(` + ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'BUSY' AFTER 'FREE'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Note: Postgres cannot drop individual enum values, so the down migration is a no-op + // The enum values FREE and BUSY will remain but will be unused after downgrade + } +} diff --git a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts index 1e4121802..a94210e65 100644 --- a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts +++ b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts @@ -20,6 +20,8 @@ export enum FuelType { export enum VehicleStatus { ACTIVE = 'ACTIVE', + FREE = 'FREE', + BUSY = 'BUSY', MAINTENANCE = 'MAINTENANCE', RETIRED = 'RETIRED', OUT_OF_SERVICE = 'OUT_OF_SERVICE', diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/fleetFormat.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/fleetFormat.tsx index 5c997af9f..709f288ce 100644 --- a/apps/edr-freight-web/backoffice/src/components/fleet/fleetFormat.tsx +++ b/apps/edr-freight-web/backoffice/src/components/fleet/fleetFormat.tsx @@ -25,6 +25,8 @@ export const formatFleetCell = ( const getStatusColor = (st: string): string => { const s = st.toUpperCase(); if (s === "ACTIVE" || s === "AVAILABLE") return "green"; + if (s === "FREE") return "teal"; + if (s === "BUSY") return "blue"; if (s === "INACTIVE") return "gray"; if (s === "SUSPENDED" || s === "OUT_OF_SERVICE") return "red"; if (s === "MAINTENANCE" || s === "ON_LEAVE") return "orange"; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx index 615e706f5..5310a6f12 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx @@ -189,6 +189,7 @@ const FleetResourcePage = () => { registerFleetOptionLabels("wagonId", dynamicOptions.wagons); registerFleetOptionLabels("containerId", dynamicOptions.containers); registerFleetOptionLabels("currentYardId", dynamicOptions.yards); + registerFleetOptionLabels("locationId", dynamicOptions.yards); }, [dynamicOptions]); const formFields = useMemo((): FleetFormFieldDef[] => { 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 6f65224bc..3643e390f 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 @@ -19,6 +19,8 @@ const FUEL_TYPE_OPTIONS = [ const VEHICLE_STATUS_OPTIONS = [ { label: "Active", value: "ACTIVE" }, + { label: "Free", value: "FREE" }, + { label: "Busy", value: "BUSY" }, { label: "Maintenance", value: "MAINTENANCE" }, { label: "Retired", value: "RETIRED" }, { label: "Out of service", value: "OUT_OF_SERVICE" }, @@ -47,13 +49,13 @@ export const vehiclesConfig: FleetResourceConfig = { ], searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"], columns: [ - { id: "code", header: "Code", accessorKey: "code", format: "code", size: 110 }, - { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", format: "code", size: 130 }, - { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 120 }, - { id: "model", header: "Model", accessorKey: "model", format: "code", size: 100 }, - { id: "vehicleType", header: "Type", accessorKey: "vehicleType", format: "code", size: 80 }, + { id: "code", header: "Code", accessorKey: "code", size: 90 }, + { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", size: 120 }, + { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", size: 120 }, + { id: "model", header: "Model", accessorKey: "model", size: 100 }, + { id: "vehicleType", header: "Type", accessorKey: "vehicleType", size: 75 }, { id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 100 }, - { id: "locationId", header: "Location", accessorKey: "locationId", format: "code", size: 130 }, + { id: "locationId", header: "Location", accessorKey: "locationId", size: 140 }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 }, ], formFields: [ diff --git a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts index 3cb2ad358..d5e9693d0 100644 --- a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts @@ -3,7 +3,7 @@ import { URL_CONSTANTS } from '@/constants/URLS'; export type VehicleType = 'TRUCK' | 'VAN' | 'CAR' | 'BUS' | 'TRAILER' | 'TANKER' | 'FLATBED'; export type FuelType = 'PETROL' | 'DIESEL' | 'ELECTRIC' | 'HYBRID'; -export type VehicleStatus = 'ACTIVE' | 'MAINTENANCE' | 'RETIRED' | 'OUT_OF_SERVICE'; +export type VehicleStatus = 'ACTIVE' | 'FREE' | 'BUSY' | 'MAINTENANCE' | 'RETIRED' | 'OUT_OF_SERVICE'; export interface VehicleListFilters { status?: VehicleStatus; From 43a32f192fe4cb9c70d53066246637747866a313 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 12:47:10 +0000 Subject: [PATCH 13/30] fix --- .../backoffice/src/pages/fleet/config/vehicles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3643e390f..1f1ae1412 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 @@ -49,8 +49,8 @@ export const vehiclesConfig: FleetResourceConfig = { ], searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"], columns: [ - { id: "code", header: "Code", accessorKey: "code", size: 90 }, { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", size: 120 }, + { id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", size: 130 }, { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", size: 120 }, { id: "model", header: "Model", accessorKey: "model", size: 100 }, { id: "vehicleType", header: "Type", accessorKey: "vehicleType", size: 75 }, From c8b7d76073526ce1b65048ed8652b3100fd49b89 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 12:50:49 +0000 Subject: [PATCH 14/30] fix --- .../backoffice/src/pages/fleet/config/vehicles.ts | 1 + 1 file changed, 1 insertion(+) 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 1f1ae1412..c6923d9fa 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 @@ -49,6 +49,7 @@ export const vehiclesConfig: FleetResourceConfig = { ], searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"], columns: [ + { id: "code", header: "Code", accessorKey: "code", size: 90 }, { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", size: 120 }, { id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", size: 130 }, { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", size: 120 }, From 64e346324b81c0380061c73d004d27cbe393e0cd Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:11:12 +0000 Subject: [PATCH 15/30] fix issue --- .../src/migrations/1880000000000-AddVehicleStatuses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts index 29da135c3..ce9af151b 100644 --- a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts +++ b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts @@ -15,7 +15,7 @@ export class AddVehicleStatuses1880000000000 implements MigrationInterface { `); } - public async down(queryRunner: QueryRunner): Promise { + public async down(_queryRunner: QueryRunner): Promise { // Note: Postgres cannot drop individual enum values, so the down migration is a no-op // The enum values FREE and BUSY will remain but will be unused after downgrade } From e429a2cebff69900473b6bd96f0e74e472fc59e7 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:15:44 +0000 Subject: [PATCH 16/30] fix --- ...8427600000-AddServiceTypesAndCargoTypes.ts | 33 +++++++++++-------- .../1880000000000-AddVehicleStatuses.ts | 15 ++++++--- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts index 65052bad4..c3698aed6 100644 --- a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts +++ b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts @@ -93,20 +93,25 @@ export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInter ); // Create indexes for service_types - await queryRunner.createIndex( - "freight.service_types", - new TableIndex({ - name: "IDX_SERVICE_TYPES_IS_ACTIVE", - columnNames: ["is_active"], - }), - ); - await queryRunner.createIndex( - "freight.service_types", - new TableIndex({ - name: "IDX_SERVICE_TYPES_DISPLAY_ORDER", - columnNames: ["display_order"], - }), - ); + const table = await queryRunner.getTable("freight.service_types"); + if (table && !(await queryRunner.hasIndex("freight.service_types", "IDX_SERVICE_TYPES_IS_ACTIVE"))) { + await queryRunner.createIndex( + "freight.service_types", + new TableIndex({ + name: "IDX_SERVICE_TYPES_IS_ACTIVE", + columnNames: ["is_active"], + }), + ); + } + if (table && !(await queryRunner.hasIndex("freight.service_types", "IDX_SERVICE_TYPES_DISPLAY_ORDER"))) { + await queryRunner.createIndex( + "freight.service_types", + new TableIndex({ + name: "IDX_SERVICE_TYPES_DISPLAY_ORDER", + columnNames: ["display_order"], + }), + ); + } // Create cargo_types table if (!(await queryRunner.hasTable("freight.cargo_types"))) await queryRunner.createTable( diff --git a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts index ce9af151b..484a2686b 100644 --- a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts +++ b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts @@ -7,11 +7,18 @@ export class AddVehicleStatuses1880000000000 implements MigrationInterface { name = "AddVehicleStatuses1880000000000"; public async up(queryRunner: QueryRunner): Promise { + // Create enum type if it doesn't exist await queryRunner.query(` - ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'FREE' BEFORE 'MAINTENANCE'; - `); - await queryRunner.query(` - ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'BUSY' AFTER 'FREE'; + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vehicles_status_enum' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'freight')) THEN + CREATE TYPE freight.vehicles_status_enum AS ENUM ('ACTIVE', 'FREE', 'BUSY', 'MAINTENANCE', 'RETIRED', 'OUT_OF_SERVICE'); + ELSE + -- Add values if enum already exists but doesn't have them + ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'FREE' BEFORE 'MAINTENANCE'; + ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'BUSY' AFTER 'FREE'; + END IF; + END $$; `); } From 1869790eab76c84ff3ccfa3b660ca02c0e633b1c Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:18:34 +0000 Subject: [PATCH 17/30] fix --- .../migrations/1748427600000-AddServiceTypesAndCargoTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts index c3698aed6..08ce634eb 100644 --- a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts +++ b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts @@ -94,7 +94,7 @@ export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInter // Create indexes for service_types const table = await queryRunner.getTable("freight.service_types"); - if (table && !(await queryRunner.hasIndex("freight.service_types", "IDX_SERVICE_TYPES_IS_ACTIVE"))) { + if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_IS_ACTIVE")) { await queryRunner.createIndex( "freight.service_types", new TableIndex({ @@ -103,7 +103,7 @@ export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInter }), ); } - if (table && !(await queryRunner.hasIndex("freight.service_types", "IDX_SERVICE_TYPES_DISPLAY_ORDER"))) { + if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_DISPLAY_ORDER")) { await queryRunner.createIndex( "freight.service_types", new TableIndex({ From d8f1ed8899a5e15063ae94ad860c0fce89041adc Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:44:19 +0000 Subject: [PATCH 18/30] fix issue --- .../backoffice/src/components/ContainerAllocationTable.tsx | 6 +++--- .../src/components/FirstMileContainerAllocationTable.tsx | 6 +++--- .../src/components/LastMileContainerAllocationTable.tsx | 6 +++--- .../backoffice/src/pages/operations/FirstMilePage.tsx | 4 ++-- .../backoffice/src/pages/operations/LastMilePage.tsx | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx index 950cfd476..ab1fd028d 100644 --- a/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx @@ -42,8 +42,8 @@ export function ContainerAllocationTable({ ); const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ - queryKey: ["vehicles", "active"], - queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }), + queryKey: ["vehicles", "free"], + queryFn: () => vehiclesService.getAll({ status: "FREE" }), }); const vehicleOptions = useMemo( @@ -99,7 +99,7 @@ export function ContainerAllocationTable({ {vehicles.length === 0 && ( } color="yellow"> - No active vehicles available. Add vehicles before allocating containers. + No free vehicles available. Free up or add vehicles before allocating containers. )} diff --git a/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx index 85bba1dc4..a1fc03539 100644 --- a/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx @@ -42,8 +42,8 @@ export function FirstMileContainerAllocationTable({ ); const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ - queryKey: ["vehicles", "active"], - queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }), + queryKey: ["vehicles", "free"], + queryFn: () => vehiclesService.getAll({ status: "FREE" }), }); const vehicleOptions = useMemo( @@ -99,7 +99,7 @@ export function FirstMileContainerAllocationTable({ {vehicles.length === 0 && ( } color="yellow"> - No active vehicles available. Add vehicles before allocating containers. + No free vehicles available. Free up or add vehicles before allocating containers. )} diff --git a/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx index d11d99a4a..1795aa793 100644 --- a/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx @@ -42,8 +42,8 @@ export function LastMileContainerAllocationTable({ ); const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ - queryKey: ["vehicles", "active"], - queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }), + queryKey: ["vehicles", "free"], + queryFn: () => vehiclesService.getAll({ status: "FREE" }), }); const vehicleOptions = useMemo( @@ -99,7 +99,7 @@ export function LastMileContainerAllocationTable({ {vehicles.length === 0 && ( } color="yellow"> - No active vehicles available. Add vehicles before allocating containers. + No free vehicles available. Free up or add vehicles before allocating containers. )} diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 198fe50a3..34bf68c21 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -355,9 +355,9 @@ const FirstMilePage = () => { }); const { data: vehiclesData } = useQuery({ - queryKey: ["vehicles", "list"], + queryKey: ["vehicles", "free"], queryFn: async () => { - const res = await vehiclesService.getAll({ status: "ACTIVE" }); + const res = await vehiclesService.getAll({ status: "FREE" }); return res.data; }, }); diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index eb00c0904..ba02483a7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -339,9 +339,9 @@ const LastMilePage = () => { }); const { data: vehiclesData } = useQuery({ - queryKey: ["vehicles", "list"], + queryKey: ["vehicles", "free"], queryFn: async () => { - const res = await vehiclesService.getAll({ status: "ACTIVE" }); + const res = await vehiclesService.getAll({ status: "FREE" }); return res.data; }, }); From 7f6c9c63134beccb3395fffce623cf776ba9611d Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:55:08 +0000 Subject: [PATCH 19/30] fix --- .../modules/first-mile/first-mile.service.ts | 66 ++++++++++++++++++- .../modules/last-mile/last-mile.service.ts | 61 ++++++++++++++++- .../src/modules/vehicles/vehicles.service.ts | 47 ++++++++++++- 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index bc8801067..6fd1bc618 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { FindOptionsWhere } from 'typeorm'; +import { FindOptionsWhere, In } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; @@ -7,6 +7,7 @@ import { BookingsRepository } from '../bookings/bookings.repository'; import { DriversService } from '../drivers/drivers.service'; import { SmsClientService } from '../notifications/sms-client.service'; import { VehiclesService } from '../vehicles/vehicles.service'; +import { VehicleStatus } from '../vehicles/entities/vehicle.entity'; import { CreateFirstMileDto } from './dto/create-first-mile.dto'; import { UpdateFirstMileDto } from './dto/update-first-mile.dto'; import { FirstMile, FirstMileStatus } from './entities/first-mile.entity'; @@ -181,7 +182,7 @@ export class FirstMileService { return existing; } - return this.firstMileRepository.create({ + const record = await this.firstMileRepository.create({ bookingId: dto.bookingId, status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, @@ -191,6 +192,12 @@ export class FirstMileService { vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, }); + + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + + return record; } private async findByBookingId(bookingId: string): Promise { @@ -236,24 +243,61 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${id} not found`); } + // Keep vehicle statuses in sync: new vehicle goes BUSY, replaced one goes back to FREE + if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) { + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + if (existing.vehicleId) { + await this.vehiclesService.releaseIfUnused([existing.vehicleId]); + } + } + // Notify assigned driver on every explicit vehicle assignment or reassignment if (dto.vehicleId) { void this.notifyDriverAssignment(dto.vehicleId, existing); } + // Trip finished — release the vehicles it was holding + if (dto.status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') { + await this.releaseVehicles(updated); + } + return updated; } async updateStatus(id: string, status: FirstMileStatus): Promise { + const existing = await this.findById(id); const updated = await this.firstMileRepository.update(id, { status }); if (!updated) { throw new NotFoundException(`First-mile record ${id} not found`); } + if (status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') { + await this.releaseVehicles(updated); + } + return updated; } + /** + * Free every vehicle held by this record (direct assignment + container + * allocations), unless still in use by another active trip. + */ + private async releaseVehicles(record: FirstMile): Promise { + const recordAllocations = await this.dataSource.manager.find(FirstMileContainerAllocation, { + where: { firstMileId: record.id }, + }); + const vehicleIds = recordAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + if (record.vehicleId) { + vehicleIds.push(record.vehicleId); + } + await this.vehiclesService.releaseIfUnused(vehicleIds); + } + private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise { try { const vehicle = await this.vehiclesService.findById(vehicleId); @@ -302,6 +346,16 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${firstMileId} not found`); } + const previousAllocations = await this.dataSource.manager.find(FirstMileContainerAllocation, { + where: { + firstMileId, + containerId: In(allocations.map((a) => a.containerId)), + }, + }); + const previousVehicleIds = previousAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + await this.dataSource.transaction(async (manager) => { for (const allocation of allocations) { await manager.delete(FirstMileContainerAllocation, { @@ -318,6 +372,14 @@ export class FirstMileService { } }); + const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); + await Promise.all( + [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + ); + await this.vehiclesService.releaseIfUnused( + previousVehicleIds.filter((id) => !vehicleIds.has(id)), + ); + return { success: true, allocated: allocations.length, diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index 732b1a618..61e732bba 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -1,10 +1,11 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { DataSource, FindOptionsWhere } from 'typeorm'; +import { DataSource, FindOptionsWhere, In } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; import { DriversService } from '../drivers/drivers.service'; import { SmsClientService } from '../notifications/sms-client.service'; import { VehiclesService } from '../vehicles/vehicles.service'; +import { VehicleStatus } from '../vehicles/entities/vehicle.entity'; import { CreateLastMileDto } from './dto/create-last-mile.dto'; import { UpdateLastMileDto } from './dto/update-last-mile.dto'; import { LastMile, LastMileStatus } from './entities/last-mile.entity'; @@ -131,7 +132,7 @@ export class LastMileService { } async create(dto: CreateLastMileDto): Promise { - return this.lastMileRepository.create({ + const record = await this.lastMileRepository.create({ bookingId: dto.bookingId, status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, @@ -141,6 +142,12 @@ export class LastMileService { vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, }); + + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + + return record; } @OnEvent("lastmile.invoice.paid") @@ -174,14 +181,46 @@ export class LastMileService { throw new NotFoundException(`Last-mile record ${id} not found`); } + // Keep vehicle statuses in sync: new vehicle goes BUSY, replaced one goes back to FREE + if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) { + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + if (existing.vehicleId) { + await this.vehiclesService.releaseIfUnused([existing.vehicleId]); + } + } + // Notify assigned driver on every explicit vehicle assignment or reassignment if (dto.vehicleId) { void this.notifyDriverAssignment(dto.vehicleId, existing); } + // Trip finished — release the vehicles it was holding + if (dto.status === 'DELIVERED' && existing.status !== 'DELIVERED') { + await this.releaseVehicles(updated); + } + return updated; } + /** + * Free every vehicle held by this record (direct assignment + container + * allocations), unless still in use by another active trip. + */ + private async releaseVehicles(record: LastMile): Promise { + const recordAllocations = await this.dataSource.manager.find(LastMileContainerAllocation, { + where: { lastMileId: record.id }, + }); + const vehicleIds = recordAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + if (record.vehicleId) { + vehicleIds.push(record.vehicleId); + } + await this.vehiclesService.releaseIfUnused(vehicleIds); + } + private async notifyDriverAssignment(vehicleId: string, record: LastMile): Promise { try { const vehicle = await this.vehiclesService.findById(vehicleId); @@ -235,6 +274,16 @@ export class LastMileService { throw new NotFoundException(`Last-mile record ${lastMileId} not found`); } + const previousAllocations = await this.dataSource.manager.find(LastMileContainerAllocation, { + where: { + lastMileId, + containerId: In(allocations.map((a) => a.containerId)), + }, + }); + const previousVehicleIds = previousAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + await this.dataSource.transaction(async (manager) => { for (const allocation of allocations) { await manager.delete(LastMileContainerAllocation, { @@ -251,6 +300,14 @@ export class LastMileService { } }); + const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); + await Promise.all( + [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + ); + await this.vehiclesService.releaseIfUnused( + previousVehicleIds.filter((id) => !vehicleIds.has(id)), + ); + return { success: true, allocated: allocations.length, diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts index 2bc882591..61765a88d 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -1,9 +1,13 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Not, Repository } from 'typeorm'; import { CreateVehicleDto } from './dto/create-vehicle.dto'; import { UpdateVehicleDto } from './dto/update-vehicle.dto'; import { Vehicle, VehicleStatus } from './entities/vehicle.entity'; +import { FirstMile, FirstMileStatus } from '../first-mile/entities/first-mile.entity'; +import { FirstMileContainerAllocation } from '../first-mile/entities/first-mile-container-allocation.entity'; +import { LastMile, LastMileStatus } from '../last-mile/entities/last-mile.entity'; +import { LastMileContainerAllocation } from '../last-mile/entities/last-mile-container-allocation.entity'; @Injectable() export class VehiclesService { @@ -91,6 +95,47 @@ export class VehiclesService { return this.vehicleRepo.save(vehicle); } + async setStatus(id: string, status: VehicleStatus): Promise { + await this.vehicleRepo.update(id, { status }); + } + + /** + * Set vehicles back to FREE, but only when no active (non-completed) + * first/last-mile record or container allocation still references them. + * First-mile trips ending in RECEIVED_TO_PORT and last-mile trips ending + * in DELIVERED no longer hold the vehicle. + */ + async releaseIfUnused(vehicleIds: string[]): Promise { + const manager = this.vehicleRepo.manager; + for (const vehicleId of [...new Set(vehicleIds)]) { + const [fmRecords, lmRecords, fmAllocations, lmAllocations] = await Promise.all([ + manager.count(FirstMile, { + where: { vehicleId, status: Not('RECEIVED_TO_PORT') }, + }), + manager.count(LastMile, { + where: { vehicleId, status: Not('DELIVERED') }, + }), + manager + .createQueryBuilder(FirstMileContainerAllocation, 'alloc') + .innerJoin(FirstMile, 'fm', 'fm.id = alloc.firstMileId') + .where('alloc.vehicleId = :vehicleId', { vehicleId }) + .andWhere('fm.status != :done', { done: 'RECEIVED_TO_PORT' }) + .andWhere('fm.deletedAt IS NULL') + .getCount(), + manager + .createQueryBuilder(LastMileContainerAllocation, 'alloc') + .innerJoin(LastMile, 'lm', 'lm.id = alloc.lastMileId') + .where('alloc.vehicleId = :vehicleId', { vehicleId }) + .andWhere('lm.status != :done', { done: 'DELIVERED' }) + .andWhere('lm.deletedAt IS NULL') + .getCount(), + ]); + if (fmRecords + lmRecords + fmAllocations + lmAllocations === 0) { + await this.setStatus(vehicleId, VehicleStatus.FREE); + } + } + } + async remove(id: string): Promise { await this.findById(id); await this.vehicleRepo.softDelete(id); From fb8dcb02177ab7981b9abd130928d447a26bf7ce Mon Sep 17 00:00:00 2001 From: yaschalew Date: Thu, 2 Jul 2026 16:55:51 +0300 Subject: [PATCH 20/30] fix --- .../edr-freight-api/src/modules/first-mile/first-mile.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index bc8801067..46f272880 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -95,7 +95,6 @@ export class FirstMileService { if (booking.paymentStatus !== 'PAID') { throw new BadRequestException(`Booking ${label} is not paid`); } - console.log("-------------------", booking) if (!this.bookingRequestsFirstMile(booking)) { throw new BadRequestException(`Booking ${label} does not require a first mile`); } From 34b87bbcee8e36688347bb037a05517a10dd9d4f Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:06:45 +0000 Subject: [PATCH 21/30] fix --- .../src/pages/operations/FirstMilePage.tsx | 16 ++++++++++++++-- .../src/pages/operations/LastMilePage.tsx | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 34bf68c21..b956c2f31 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -1037,11 +1037,17 @@ const FirstMilePage = () => { diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index ba02483a7..cfb73a860 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -1095,12 +1095,18 @@ const LastMilePage = () => { From 635d674bbed8fb4a6ce08ef6917126e05adb95b3 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 2 Jul 2026 14:11:34 +0000 Subject: [PATCH 22/30] fix: merge marker in invoice detial page --- .../src/pages/billing/InvoiceDetailPage.tsx | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx index 888a09c1a..5dca05b15 100644 --- a/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx @@ -10,7 +10,6 @@ import { Group, Loader, Paper, - Select, SimpleGrid, Stack, Table, @@ -24,7 +23,6 @@ import { ExternalLink, Receipt, } from "lucide-react"; -import { useState } from "react"; import toast from "react-hot-toast"; import { api } from "@/services/api"; @@ -67,7 +65,9 @@ function MetaItem({ label, value }: { label: string; value: string }) { export default function InvoiceDetailPage() { const { id = "" } = useParams(); const navigate = useNavigate(); - const [paymentMethod, setPaymentMethod] = useState<"TELEBIRR" | "WAAFI">("TELEBIRR"); + const [paymentMethod, setPaymentMethod] = useState<"TELEBIRR" | "WAAFI">( + "TELEBIRR", + ); const { data: invoice, @@ -130,11 +130,6 @@ export default function InvoiceDetailPage() { const amountDue = Number(invoice.balanceAmount ?? invoice.totalAmount); const handlePay = () => { -<<<<<<< HEAD - const returnUrl = `${window.location.origin}/payment/success`; - const failureUrl = `${window.location.origin}/payment/failure`; - payMutation.mutate({ id, payload: { method: paymentMethod, returnUrl, failureUrl } }); -======= setPayModalOpen(true); }; @@ -184,7 +179,6 @@ export default function InvoiceDetailPage() { } toast.error("This invoice's source isn't linked to a booking."); } ->>>>>>> 03740ee719f22f9617379a652b26f13b6870f671 }; return ( @@ -214,20 +208,6 @@ export default function InvoiceDetailPage() { -<<<<<<< HEAD - {payable && ( - -