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());