Merge branch 'freight/feature/first_mile_invoice' of github.com:Tria-plc/edr-platform into freight/feature/first_mile_invoice

This commit is contained in:
yaschalew
2026-07-02 17:14:31 +03:00
13 changed files with 84 additions and 23 deletions

View File

@@ -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<void> {
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<void> {
// 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
`);
}
}

View File

@@ -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';
@@ -193,7 +193,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;
@@ -245,7 +245,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]);
@@ -373,7 +373,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)),

View File

@@ -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)),

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<void> {
await this.vehicleRepo.update(id, { status });
async setAvailability(id: string, availability: VehicleAvailability): Promise<void> {
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);
}
}
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 };

View File

@@ -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;
},
});

View File

@@ -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;
},
});

View File

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