From e0010695be5f81c894ea1f471fb2d8131c050cdc Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 09:41:52 +0000 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 c2c29f0735723e6d953abd07d28c8c320e2033b2 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:14:13 +0000 Subject: [PATCH 22/27] fix --- ...90000000000-SeparateVehicleAvailability.ts | 40 +++++++++++++++++++ .../modules/first-mile/first-mile.service.ts | 8 ++-- .../modules/last-mile/last-mile.service.ts | 8 ++-- .../vehicles/entities/vehicle.entity.ts | 10 ++++- .../modules/vehicles/vehicles.controller.ts | 2 + .../src/modules/vehicles/vehicles.service.ts | 13 ++++-- .../components/ContainerAllocationTable.tsx | 2 +- .../FirstMileContainerAllocationTable.tsx | 2 +- .../LastMileContainerAllocationTable.tsx | 2 +- .../src/pages/fleet/config/vehicles.ts | 10 +++-- .../src/pages/operations/FirstMilePage.tsx | 2 +- .../src/pages/operations/LastMilePage.tsx | 2 +- .../src/services/vehicles.service.ts | 6 ++- 13 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts diff --git a/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts b/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts new file mode 100644 index 000000000..c0015c3c6 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts @@ -0,0 +1,40 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Split the mixed vehicle status into two fields: + * - status: operational state (ACTIVE, MAINTENANCE, RETIRED, OUT_OF_SERVICE) + * - availability: assignment state (FREE, BUSY) + * + * Existing FREE/BUSY statuses are moved to availability and the status is + * normalized back to ACTIVE. + */ +export class SeparateVehicleAvailability1890000000000 implements MigrationInterface { + name = "SeparateVehicleAvailability1890000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + ADD COLUMN IF NOT EXISTS availability varchar DEFAULT 'FREE' + `); + await queryRunner.query(` + UPDATE freight.vehicles SET availability = 'BUSY' WHERE status = 'BUSY' + `); + await queryRunner.query(` + UPDATE freight.vehicles SET availability = 'FREE' WHERE availability IS NULL + `); + await queryRunner.query(` + UPDATE freight.vehicles SET status = 'ACTIVE' WHERE status IN ('FREE', 'BUSY') + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Fold availability back into status before dropping the column + await queryRunner.query(` + UPDATE freight.vehicles SET status = availability + WHERE status = 'ACTIVE' AND availability IN ('FREE', 'BUSY') + `); + await queryRunner.query(` + ALTER TABLE freight.vehicles DROP COLUMN IF EXISTS availability + `); + } +} 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 6fd1bc618..bffdefecf 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 @@ -7,7 +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 { VehicleAvailability } 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'; @@ -194,7 +194,7 @@ export class FirstMileService { }); if (dto.vehicleId) { - await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY); } return record; @@ -246,7 +246,7 @@ export class FirstMileService { // 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); + await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY); } if (existing.vehicleId) { await this.vehiclesService.releaseIfUnused([existing.vehicleId]); @@ -374,7 +374,7 @@ export class FirstMileService { const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); await Promise.all( - [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + [...vehicleIds].map((vehicleId) => this.vehiclesService.setAvailability(vehicleId, VehicleAvailability.BUSY)), ); await this.vehiclesService.releaseIfUnused( previousVehicleIds.filter((id) => !vehicleIds.has(id)), 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 61e732bba..dec6c47b4 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 @@ -5,7 +5,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 { VehicleAvailability } 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'; @@ -144,7 +144,7 @@ export class LastMileService { }); if (dto.vehicleId) { - await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY); } return record; @@ -184,7 +184,7 @@ export class LastMileService { // 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); + await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY); } if (existing.vehicleId) { await this.vehiclesService.releaseIfUnused([existing.vehicleId]); @@ -302,7 +302,7 @@ export class LastMileService { const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); await Promise.all( - [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + [...vehicleIds].map((vehicleId) => this.vehiclesService.setAvailability(vehicleId, VehicleAvailability.BUSY)), ); await this.vehiclesService.releaseIfUnused( previousVehicleIds.filter((id) => !vehicleIds.has(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 a94210e65..5427fac94 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,13 +20,16 @@ export enum FuelType { export enum VehicleStatus { ACTIVE = 'ACTIVE', - FREE = 'FREE', - BUSY = 'BUSY', MAINTENANCE = 'MAINTENANCE', RETIRED = 'RETIRED', OUT_OF_SERVICE = 'OUT_OF_SERVICE', } +export enum VehicleAvailability { + FREE = 'FREE', + BUSY = 'BUSY', +} + @Entity({ name: 'vehicles', schema: 'freight' }) export class Vehicle extends BaseEntity { @Column({ name: 'plate_number', unique: true, nullable: true }) @@ -56,6 +59,9 @@ export class Vehicle extends BaseEntity { @Column({ name: 'status', type: 'varchar', default: VehicleStatus.ACTIVE, nullable: true }) status?: VehicleStatus; + @Column({ name: 'availability', type: 'varchar', default: VehicleAvailability.FREE, nullable: true }) + availability?: VehicleAvailability; + @Column({ type: 'text', nullable: true }) description?: string | null; diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts index 24ff2d022..8e6d8a0a8 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts @@ -34,6 +34,7 @@ export class VehiclesController { findAll( @Query('search') search?: string, @Query('status') status?: string, + @Query('availability') availability?: string, @Query('page') page?: string, @Query('limit') limit?: string, @Query('sortBy') sortBy?: string, @@ -42,6 +43,7 @@ export class VehiclesController { return this.vehiclesService.findAll({ search, status: status as any, + availability: availability as any, page: page ? parseInt(page) : undefined, limit: limit ? parseInt(limit) : undefined, sortBy, 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 61765a88d..a27969e2b 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/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 { Vehicle, VehicleAvailability, 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'; @@ -39,6 +39,7 @@ export class VehiclesService { async findAll(query: { search?: string; status?: VehicleStatus | string; + availability?: VehicleAvailability | string; page?: number; limit?: number; sortBy?: string; @@ -57,6 +58,10 @@ export class VehiclesService { qb = qb.andWhere('v.status = :status', { status: query.status }); } + if (query.availability) { + qb = qb.andWhere('v.availability = :availability', { availability: query.availability }); + } + const sortBy = ['plateNumber', 'status', 'year', 'createdAt'].includes( query.sortBy ?? '', ) @@ -95,8 +100,8 @@ export class VehiclesService { return this.vehicleRepo.save(vehicle); } - async setStatus(id: string, status: VehicleStatus): Promise { - await this.vehicleRepo.update(id, { status }); + async setAvailability(id: string, availability: VehicleAvailability): Promise { + await this.vehicleRepo.update(id, { availability }); } /** @@ -131,7 +136,7 @@ export class VehiclesService { .getCount(), ]); if (fmRecords + lmRecords + fmAllocations + lmAllocations === 0) { - await this.setStatus(vehicleId, VehicleStatus.FREE); + await this.setAvailability(vehicleId, VehicleAvailability.FREE); } } } diff --git a/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx index ab1fd028d..8971d4ccb 100644 --- a/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ContainerAllocationTable.tsx @@ -43,7 +43,7 @@ export function ContainerAllocationTable({ const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ queryKey: ["vehicles", "free"], - queryFn: () => vehiclesService.getAll({ status: "FREE" }), + queryFn: () => vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }), }); const vehicleOptions = useMemo( diff --git a/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx index a1fc03539..78c5160ca 100644 --- a/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/FirstMileContainerAllocationTable.tsx @@ -43,7 +43,7 @@ export function FirstMileContainerAllocationTable({ const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ queryKey: ["vehicles", "free"], - queryFn: () => vehiclesService.getAll({ status: "FREE" }), + queryFn: () => vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }), }); const vehicleOptions = useMemo( diff --git a/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx b/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx index 1795aa793..cc619cb9a 100644 --- a/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/LastMileContainerAllocationTable.tsx @@ -43,7 +43,7 @@ export function LastMileContainerAllocationTable({ const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({ queryKey: ["vehicles", "free"], - queryFn: () => vehiclesService.getAll({ status: "FREE" }), + queryFn: () => vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }), }); const vehicleOptions = useMemo( 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 c6923d9fa..344eec6eb 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,13 +19,16 @@ 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" }, ]; +const VEHICLE_AVAILABILITY_OPTIONS = [ + { label: "Free", value: "FREE" }, + { label: "Busy", value: "BUSY" }, +]; + export const vehiclesConfig: FleetResourceConfig = { slug: "vehicles", label: "Vehicles", @@ -58,6 +61,7 @@ export const vehiclesConfig: FleetResourceConfig = { { id: "capacity", header: "Capacity (tons)", accessorKey: "capacity", format: "number", size: 100 }, { id: "locationId", header: "Location", accessorKey: "locationId", size: 140 }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 }, + { id: "availability", header: "Availability", accessorKey: "availability", format: "statusBadge", size: 100 }, ], formFields: [ { name: "code", label: "Code", type: "text" }, @@ -95,4 +99,4 @@ export const vehiclesConfig: FleetResourceConfig = { }, }; -export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS }; +export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS, VEHICLE_AVAILABILITY_OPTIONS }; 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 b956c2f31..4f401b3bc 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -357,7 +357,7 @@ const FirstMilePage = () => { const { data: vehiclesData } = useQuery({ queryKey: ["vehicles", "free"], queryFn: async () => { - const res = await vehiclesService.getAll({ status: "FREE" }); + const res = await vehiclesService.getAll({ status: "ACTIVE", availability: "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 cfb73a860..ae03aab75 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -341,7 +341,7 @@ const LastMilePage = () => { const { data: vehiclesData } = useQuery({ queryKey: ["vehicles", "free"], queryFn: async () => { - const res = await vehiclesService.getAll({ status: "FREE" }); + const res = await vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }); return res.data; }, }); 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 d5e9693d0..6de124df6 100644 --- a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts @@ -3,10 +3,12 @@ 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' | 'FREE' | 'BUSY' | 'MAINTENANCE' | 'RETIRED' | 'OUT_OF_SERVICE'; +export type VehicleStatus = 'ACTIVE' | 'MAINTENANCE' | 'RETIRED' | 'OUT_OF_SERVICE'; +export type VehicleAvailability = 'FREE' | 'BUSY'; export interface VehicleListFilters { status?: VehicleStatus; + availability?: VehicleAvailability; search?: string; page?: number; limit?: number; @@ -25,6 +27,7 @@ export interface Vehicle { fuelType: FuelType; capacity: number; status: VehicleStatus; + availability: VehicleAvailability; description?: string | null; code?: string | null; powerPlateNo?: string | null; @@ -43,6 +46,7 @@ export const vehiclesService = { getAll: (filters: VehicleListFilters = {}) => { const params = new URLSearchParams(); if (filters.status) params.set('status', filters.status); + if (filters.availability) params.set('availability', filters.availability); if (filters.search) params.set('search', filters.search); if (filters.page) params.set('page', filters.page.toString()); if (filters.limit) params.set('limit', filters.limit.toString()); From 7d85f5dc2ec589a9244920e30c93399ae39c25e4 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:18:53 +0000 Subject: [PATCH 23/27] fix --- .../src/modules/vehicles/dto/create-vehicle.dto.ts | 6 +++++- .../backoffice/src/pages/fleet/config/vehicles.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 5651a3906..9158ecca0 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 @@ -1,5 +1,5 @@ import { IsString, IsEnum, IsNumber, IsOptional, IsUUID } from 'class-validator'; -import { VehicleType, FuelType, VehicleStatus } from '../entities/vehicle.entity'; +import { VehicleType, FuelType, VehicleStatus, VehicleAvailability } from '../entities/vehicle.entity'; export class CreateVehicleDto { @IsString() @@ -26,6 +26,10 @@ export class CreateVehicleDto { @IsEnum(VehicleStatus) status!: VehicleStatus; + @IsOptional() + @IsEnum(VehicleAvailability) + availability?: VehicleAvailability; + @IsOptional() @IsString() description?: 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 344eec6eb..a8c8e90dd 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 @@ -78,6 +78,7 @@ export const vehiclesConfig: FleetResourceConfig = { { 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 }, + { name: "availability", label: "Availability", type: "select", required: true, options: VEHICLE_AVAILABILITY_OPTIONS }, { name: "description", label: "Description", type: "textarea" }, ], emptyValues: { @@ -95,6 +96,7 @@ export const vehiclesConfig: FleetResourceConfig = { estimatedDistanceKm: "", actualDistanceKm: "", status: "ACTIVE", + availability: "FREE", description: "", }, }; From e5fed081fccd45f26fa62c2edbd34f6c95062331 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:27:09 +0000 Subject: [PATCH 24/27] fix --- .../src/modules/bookings/bookings.module.ts | 2 ++ .../src/modules/bookings/bookings.service.ts | 23 +++++++++++++++++++ .../src/modules/vehicles/vehicles.service.ts | 6 +++-- .../src/pages/bookings/BookingDetailPage.tsx | 1 + .../src/pages/operations/FirstMilePage.tsx | 3 +++ .../src/pages/operations/LastMilePage.tsx | 3 +++ 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index 4cd5d10df..72dbab183 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -39,6 +39,7 @@ import { ContractRendererService } from "../../contracts/contract-renderer.servi import { ContractTemplateResolver } from "../../contracts/contract-template.resolver"; import { ContractViewModelBuilder } from "../../contracts/contract-view-model.builder"; import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module"; +import { VehiclesModule } from "../vehicles/vehicles.module"; @Module({ imports: [ @@ -58,6 +59,7 @@ import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.modu forwardRef(() => TrainSchedulingModule), FilesModule, MinioModule, + VehiclesModule, CompaniesModule, // CustomersModule, RuleEngineModule, diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index a4636cbf7..99d69c25f 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -31,6 +31,8 @@ import { ServiceType } from '../rule-engine/entities/service-type.entity'; import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity'; import { BookingsRepository } from './bookings.repository'; import { ConsolidationService } from './consolidation.service'; +import { VehiclesService } from '../vehicles/vehicles.service'; +import { VehicleAvailability } from '../vehicles/entities/vehicle.entity'; import { assertFreightShape } from './booking-freight.util'; import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto'; import { mapStatusCountsToTabs } from './booking-list-tabs.config'; @@ -81,6 +83,7 @@ export class BookingsService { private readonly ruleEngineService: RuleEngineService, private readonly containerTypesService: ContainerTypesService, private readonly consolidationService: ConsolidationService, + private readonly vehiclesService: VehiclesService, ) {} /** Resolve trade direction from yard countries; reject client mismatch. */ @@ -1348,6 +1351,16 @@ export class BookingsService { throw new NotFoundException(`Booking ${bookingId} not found`); } + const previousAllocations = await this.dataSource.manager.find(BookingContainerAllocation, { + where: { + bookingId, + 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(BookingContainerAllocation, { @@ -1364,6 +1377,16 @@ export class BookingsService { } }); + const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); + await Promise.all( + [...vehicleIds].map((vehicleId) => + this.vehiclesService.setAvailability(vehicleId, VehicleAvailability.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 a27969e2b..5da3b448d 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -8,6 +8,7 @@ import { FirstMile, FirstMileStatus } from '../first-mile/entities/first-mile.en 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'; +import { BookingContainerAllocation } from '../bookings/entities/booking-container-allocation.entity'; @Injectable() export class VehiclesService { @@ -113,7 +114,7 @@ export class VehiclesService { 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([ + const [fmRecords, lmRecords, fmAllocations, lmAllocations, bookingAllocations] = await Promise.all([ manager.count(FirstMile, { where: { vehicleId, status: Not('RECEIVED_TO_PORT') }, }), @@ -134,8 +135,9 @@ export class VehiclesService { .andWhere('lm.status != :done', { done: 'DELIVERED' }) .andWhere('lm.deletedAt IS NULL') .getCount(), + manager.count(BookingContainerAllocation, { where: { vehicleId } }), ]); - if (fmRecords + lmRecords + fmAllocations + lmAllocations === 0) { + if (fmRecords + lmRecords + fmAllocations + lmAllocations + bookingAllocations === 0) { await this.setAvailability(vehicleId, VehicleAvailability.FREE); } } diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx index bf01c4818..d49e24f95 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx @@ -33,6 +33,7 @@ const BookingDetailPage = () => { onSuccess: () => { toast.success("Containers allocated"); qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? "") }); + qc.invalidateQueries({ queryKey: ["vehicles"] }); }, onError: () => { toast.error("Failed to allocate 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 4f401b3bc..78b15525b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -400,6 +400,7 @@ const FirstMilePage = () => { firstMileService.update(id, data), onSuccess: () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); }, onError: () => { toast({ title: "Update failed", variant: "destructive" }); @@ -444,6 +445,7 @@ const FirstMilePage = () => { }, onSuccess: () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); toast({ title: "Booking accepted", description: "First-mile leg created successfully." }); closeAccept(); }, @@ -460,6 +462,7 @@ const FirstMilePage = () => { onSuccess: () => { toast({ title: "Containers allocated" }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.byId(containerAllocationFirstMileId ?? "") }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); setContainerAllocationOpen(false); setContainerAllocationFirstMileId(null); }, 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 ae03aab75..9284c4dae 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -377,6 +377,7 @@ const LastMilePage = () => { lastMileService.update(id, data), onSuccess: () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); }, onError: () => { toast({ title: "Update failed", variant: "destructive" }); @@ -415,6 +416,7 @@ const LastMilePage = () => { onSuccess: () => { toast({ title: "Containers allocated", variant: "default" }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.byId(activeId ?? "") }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); closeAllocation(); }, onError: () => { @@ -453,6 +455,7 @@ const LastMilePage = () => { }, onSuccess: (created) => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT }); + void qc.invalidateQueries({ queryKey: ["vehicles"] }); toast({ title: "Last-mile leg created", description: `${created.length} ${created.length === 1 ? "delivery" : "deliveries"} accepted successfully.`, From 2424e96e74619ac05eba6f46b5408c31f3ae99fe Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:34:22 +0000 Subject: [PATCH 25/27] fix --- .../src/modules/drivers/drivers.service.ts | 10 ++++------ .../src/modules/vehicles/vehicles.service.ts | 2 +- .../backoffice/src/pages/fleet/FleetResourcePage.tsx | 9 +++++++-- .../backoffice/src/pages/fleet/config/vehicles.ts | 6 ++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.service.ts b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts index d5176d14b..2464a26ac 100644 --- a/apps/edr-freight-api/src/modules/drivers/drivers.service.ts +++ b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts @@ -48,12 +48,10 @@ export class DriversService { const qb = this.driverRepo.createQueryBuilder('d'); if (query.search) { - const searchTerm = `%${query.search}%`; - qb.where('d.firstName ILIKE :search', { search: searchTerm }) - .orWhere('d.lastName ILIKE :search', { search: searchTerm }) - .orWhere('d.email ILIKE :search', { search: searchTerm }) - .orWhere('d.licenseNumber ILIKE :search', { search: searchTerm }) - .orWhere('d.phoneNumber ILIKE :search', { search: searchTerm }); + qb.where( + '(d.firstName ILIKE :search OR d.lastName ILIKE :search OR d.email ILIKE :search OR d.licenseNumber ILIKE :search OR d.phoneNumber ILIKE :search)', + { search: `%${query.search}%` }, + ); } if (query.status) { 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 5da3b448d..5e151f1ea 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -50,7 +50,7 @@ export class VehiclesService { if (query.search) { qb = qb.where( - 'v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search', + '(v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search OR v.model ILIKE :search)', { search: `%${query.search}%` }, ); } 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 5310a6f12..13be9ef71 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx @@ -46,17 +46,22 @@ const FleetResourcePage = () => { const { viewMode, setViewMode } = useFleetViewMode(slug); const serverListFilters = useMemo((): FleetListFilters | undefined => { - if (slug !== "wagons" && slug !== "locomotives") return undefined; + const serverFilteredSlugs: FleetResourceSlug[] = ["wagons", "locomotives", "vehicles", "drivers"]; + if (!serverFilteredSlugs.includes(slug)) return undefined; const filters: FleetListFilters = {}; const status = listFilterValues.status; const currentYardId = listFilterValues.currentYardId; + const availability = listFilterValues.availability; if (status && status !== "ALL") { (filters as { status?: string }).status = status; } if (currentYardId && currentYardId !== "ALL") { filters.currentYardId = currentYardId; } - if (slug === "wagons" && search.trim()) { + if (availability && availability !== "ALL") { + (filters as { availability?: string }).availability = availability; + } + if (slug !== "locomotives" && search.trim()) { filters.search = search.trim(); } return filters; 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 a8c8e90dd..de6eb09c7 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,12 @@ export const vehiclesConfig: FleetResourceConfig = { allLabel: "All statuses", options: VEHICLE_STATUS_OPTIONS, }, + { + key: "availability", + label: "Availability", + allLabel: "All availability", + options: VEHICLE_AVAILABILITY_OPTIONS, + }, ], searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"], columns: [ From b7da02496883d18f2d31d71040e013ab5c590f15 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:37:38 +0000 Subject: [PATCH 26/27] fix --- .../1890000000001-AddVehicleCodeAndPlates.ts | 28 +++++++++++++++++++ .../vehicles/entities/vehicle.entity.ts | 9 ++++++ .../src/modules/vehicles/vehicles.service.ts | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts diff --git a/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts b/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts new file mode 100644 index 000000000..6f9faa1f8 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Add code, power_plate_no and trailer_plate_no columns to vehicles. + * These fields existed in the DTO and UI form but had no entity columns, + * so submitted values were silently dropped. + */ +export class AddVehicleCodeAndPlates1890000000001 implements MigrationInterface { + name = "AddVehicleCodeAndPlates1890000000001"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + ADD COLUMN IF NOT EXISTS code varchar, + ADD COLUMN IF NOT EXISTS power_plate_no varchar, + ADD COLUMN IF NOT EXISTS trailer_plate_no varchar + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + DROP COLUMN IF EXISTS code, + DROP COLUMN IF EXISTS power_plate_no, + DROP COLUMN IF EXISTS trailer_plate_no + `); + } +} 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 5427fac94..416cddee6 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 @@ -32,9 +32,18 @@ export enum VehicleAvailability { @Entity({ name: 'vehicles', schema: 'freight' }) export class Vehicle extends BaseEntity { + @Column({ nullable: true }) + code?: string; + @Column({ name: 'plate_number', unique: true, nullable: true }) plateNumber?: string; + @Column({ name: 'power_plate_no', nullable: true }) + powerPlateNo?: string; + + @Column({ name: 'trailer_plate_no', nullable: true }) + trailerPlateNo?: string; + @Column({ name: 'registration_number', unique: true, nullable: true }) registrationNumber?: string; 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 5e151f1ea..69568e483 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -50,7 +50,7 @@ export class VehiclesService { if (query.search) { qb = qb.where( - '(v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search OR v.model ILIKE :search)', + '(v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search OR v.model ILIKE :search OR v.code ILIKE :search OR v.trailerPlateNo ILIKE :search)', { search: `%${query.search}%` }, ); } From eb5d8a7411d76c03e9787366eeee85017959d027 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 14:42:04 +0000 Subject: [PATCH 27/27] fix --- .../src/modules/last-mile/last-mile.service.ts | 14 ++++++++++++-- .../src/pages/operations/LastMilePage.tsx | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) 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 dec6c47b4..54d992bd1 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,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { DataSource, FindOptionsWhere, In } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; @@ -165,6 +165,13 @@ export class LastMileService { async update(id: string, dto: UpdateLastMileDto): Promise { const existing = await this.findById(id); + if ( + existing.status === 'DELIVERED' && + (dto.vehicleId !== undefined || dto.exactKm !== undefined) + ) { + throw new BadRequestException('Delivered records cannot be reassigned or have distance changed'); + } + const dtoAny = dto as any; const updated = await this.lastMileRepository.update(id, { ...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}), @@ -261,7 +268,10 @@ export class LastMileService { } async remove(id: string): Promise { - await this.findById(id); + const existing = await this.findById(id); + if (existing.status === 'DELIVERED') { + throw new BadRequestException('Delivered records cannot be deleted'); + } await this.lastMileRepository.softDelete(id); } 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 9284c4dae..eb6d88a92 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -833,6 +833,7 @@ const LastMilePage = () => { const nextStatus = NEXT_STATUS[row.original.status]; const canPrint = row.original.status !== "PAYMENT_PENDING"; const isPaid = (row.original as any).paid; + const delivered = row.original.status === "DELIVERED"; return ( @@ -852,14 +853,14 @@ const LastMilePage = () => { } - disabled={assigned} + disabled={assigned || delivered} onClick={() => openAssign(row.original.id)} > Assign } - disabled={!assigned} + disabled={!assigned || delivered} onClick={() => openAssign(row.original.id)} > Reassign @@ -873,6 +874,7 @@ const LastMilePage = () => { } + disabled={delivered} onClick={() => openDistance(row.original.id)} > Add distance @@ -891,6 +893,7 @@ const LastMilePage = () => { } color="red" + disabled={delivered} onClick={() => { if (confirm(`Delete last-mile record ${bookingRef(row.original)}?`)) { deleteMutation.mutate(row.original.id);