diff --git a/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts b/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts new file mode 100644 index 000000000..98fa8b228 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDistanceColumnsToVehicles1821000000002 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + ADD COLUMN IF NOT EXISTS estimated_distance_km NUMERIC, + ADD COLUMN IF NOT EXISTS actual_distance_km NUMERIC; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + DROP COLUMN IF EXISTS estimated_distance_km, + DROP COLUMN IF EXISTS actual_distance_km; + `); + } +} 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 55e12047b..b5f9abcb8 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 @@ -49,4 +49,12 @@ export class CreateVehicleDto { @IsOptional() @IsString() trailerPlateNo?: string; + + @IsOptional() + @IsNumber() + estimatedDistanceKm?: number; + + @IsOptional() + @IsNumber() + actualDistanceKm?: number; } 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 005118b0a..fef078ef8 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 @@ -62,4 +62,10 @@ export class Vehicle extends BaseEntity { @Column({ name: 'assigned_driver_name', nullable: true }) assignedDriverName?: string; + + @Column({ name: 'estimated_distance_km', type: 'numeric', nullable: true }) + estimatedDistanceKm?: number; + + @Column({ name: 'actual_distance_km', type: 'numeric', nullable: true }) + actualDistanceKm?: number; } 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 9daa036ae..b91e24cb0 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 @@ -60,6 +60,8 @@ export const vehiclesConfig: FleetResourceConfig = { { 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: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 }, ], formFields: [ @@ -73,6 +75,8 @@ 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: "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: "description", label: "Description", type: "textarea" }, ], @@ -87,6 +91,8 @@ export const vehiclesConfig: FleetResourceConfig = { year: new Date().getFullYear(), fuelType: "DIESEL", capacity: 0, + estimatedDistanceKm: "", + actualDistanceKm: "", status: "ACTIVE", description: "", }, 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 34e23e97e..1e748cc51 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -93,8 +93,6 @@ const cargoDesc = (r: FirstMileRecord) => { if (r.booking?.cargoTotalWeightVgm) parts.push(`${r.booking.cargoTotalWeightVgm} t`); return parts.join(" · ") || "—"; }; -const priceAmount = (r: FirstMileRecord) => - r.booking?.totalAmount ?? r.advancedPayment; // First-mile destination is the origin yard (pickup → origin yard) const destinationYardName = (r: FirstMileRecord) => r.booking?.originYard?.label ?? "—"; @@ -142,11 +140,14 @@ const BookingInfo = ({ record }: { record: FirstMileRecord }) => ( - + + + + @@ -158,10 +159,13 @@ const tripSlipRows = (record: FirstMileRecord): [string, string][] => [ ["Pickup location", pickupLocation(record)], ["Destination yard", destinationYardName(record)], ["Cargo", cargoDesc(record)], - ["Price", formatPrice(priceAmount(record))], + ["Advanced Payment", formatPrice(record.advancedPayment)], + ["Post Payment", formatPrice(record.remainingPayment)], ["Vehicle", vehicleLabel(record) ?? "Unassigned"], ["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`], ["Requested date", requestedDate(record)], + ["Est. Distance (KM)", record.estimatedKm != null ? String(record.estimatedKm) : "—"], + ["Actual Distance (KM)", record.exactKm != null ? String(record.exactKm) : "—"], ["Status", STATUS_META[record.status].label], ]; @@ -628,10 +632,16 @@ const FirstMilePage = () => { cell: ({ row }) => cargoDesc(row.original), }, { - id: "price", - header: "Price", + id: "advancedPayment", + header: "Advanced Payment", meta: { headerClassName, cellClassName }, - cell: ({ row }) => formatPrice(priceAmount(row.original)), + cell: ({ row }) => formatPrice(row.original.advancedPayment), + }, + { + id: "postPayment", + header: "Post Payment", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => formatPrice(row.original.remainingPayment), }, { id: "vehicle", @@ -639,6 +649,18 @@ const FirstMilePage = () => { meta: { headerClassName, cellClassName }, cell: ({ row }) => vehicleLabel(row.original) ?? , }, + { + id: "estimatedKm", + header: "Est. Distance (KM)", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => row.original.estimatedKm != null ? row.original.estimatedKm : , + }, + { + id: "exactKm", + header: "Actual Distance (KM)", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : , + }, { id: "status", header: "Status", 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 bb121ddfc..5535809a2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -66,7 +66,7 @@ type StatusFilter = "ALL" | LastMileApiStatus | AssignmentStatus; const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ { value: "ALL", label: "All" }, - ...LAST_MILE_STATUSES.map((s) => ({ value: s as StatusFilter, label: STATUS_META[s].label })), + ...LAST_MILE_STATUSES.filter((s) => s !== "PAYMENT_PENDING").map((s) => ({ value: s as StatusFilter, label: STATUS_META[s].label })), { value: "ASSIGNED", label: "Assigned" }, { value: "UNASSIGNED", label: "Unassigned" }, ]; @@ -87,14 +87,12 @@ const bookingRef = (r: LastMileRecord) => r.booking?.reference ?? r.bookingId; const customerName = (r: LastMileRecord) => r.booking?.company?.name ?? "—"; const deliveryLocation = (r: LastMileRecord) => r.booking?.lastMileDeliveryAddress ?? "—"; const cargoDesc = (r: LastMileRecord) => { - const parts = [r.booking?.cargoType?.name ?? r.booking?.cargoFreeText].filter(Boolean); + const parts = [r.booking?.cargoType?.cargoTypeName ?? r.booking?.cargoType?.label ?? r.booking?.cargoType?.name ?? r.booking?.cargoFreeText].filter(Boolean); if (r.booking?.cargoTotalWeightVgm) parts.push(`${r.booking.cargoTotalWeightVgm} t`); return parts.join(" · ") || "—"; }; -const priceAmount = (r: LastMileRecord) => - r.booking?.totalAmount ?? r.advancedPayment; const originYardName = (r: LastMileRecord) => - r.booking?.originYard?.name ?? "—"; + r.booking?.originYard?.label ?? r.booking?.originYard?.name ?? "—"; const contactPersonName = (r: LastMileRecord) => r.booking?.company?.contactPersonName ?? "—"; const contactPhone = (r: LastMileRecord) => @@ -104,7 +102,7 @@ const requestedDate = (r: LastMileRecord) => { return d ? new Date(d).toISOString().slice(0, 10) : "—"; }; const serviceTypeName = (r: LastMileRecord) => - r.booking?.serviceType?.name ?? "—"; + r.booking?.serviceType?.label ?? r.booking?.serviceType?.name ?? "—"; const InfoRow = ({ label, value }: { label: string; value: string }) => ( @@ -130,14 +128,17 @@ const BookingInfo = ({ record }: { record: LastMileRecord }) => ( - + - + + + + @@ -146,13 +147,16 @@ const BookingInfo = ({ record }: { record: LastMileRecord }) => ( const tripSlipRows = (record: LastMileRecord): [string, string][] => [ ["Customer", customerName(record)], ["Service", serviceTypeName(record)], - ["Origin yard", originYardName(record)], + ["Pickup (origin yard)", originYardName(record)], ["Destination", deliveryLocation(record)], ["Cargo", cargoDesc(record)], - ["Price", formatPrice(priceAmount(record))], + ["Advanced Payment", formatPrice(record.advancedPayment)], + ["Post Payment", formatPrice(record.remainingPayment)], ["Vehicle", vehicleLabel(record) ?? "Unassigned"], ["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`], ["Requested date", requestedDate(record)], + ["Est. Distance (KM)", record.estimatedKm != null ? String(record.estimatedKm) : "—"], + ["Actual Distance (KM)", record.exactKm != null ? String(record.exactKm) : "—"], ["Status", STATUS_META[record.status].label], ]; @@ -587,6 +591,12 @@ const LastMilePage = () => { meta: { headerClassName, cellClassName }, cell: ({ row }) => customerName(row.original), }, + { + id: "pickup", + header: "Pickup", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => originYardName(row.original), + }, { id: "destination", header: "Destination", @@ -600,10 +610,16 @@ const LastMilePage = () => { cell: ({ row }) => cargoDesc(row.original), }, { - id: "price", - header: "Price", + id: "advancedPayment", + header: "Advanced Payment", meta: { headerClassName, cellClassName }, - cell: ({ row }) => formatPrice(priceAmount(row.original)), + cell: ({ row }) => formatPrice(row.original.advancedPayment), + }, + { + id: "postPayment", + header: "Post Payment", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => formatPrice(row.original.remainingPayment), }, { id: "vehicle", @@ -611,6 +627,18 @@ const LastMilePage = () => { meta: { headerClassName, cellClassName }, cell: ({ row }) => vehicleLabel(row.original) ?? , }, + { + id: "estimatedKm", + header: "Est. Distance (KM)", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => row.original.estimatedKm != null ? row.original.estimatedKm : , + }, + { + id: "exactKm", + header: "Actual Distance (KM)", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : , + }, { id: "status", header: "Status", 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 23f5261c8..b182a16d0 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,7 +60,7 @@ 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 }) => + update: (id: string, data: { status?: FirstMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null }) => api.patch(FM.BY_ID(id), data), accept: (bookingReference: string) => api.post(FM.ACCEPT(bookingReference)), 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 779fd96aa..cb9e61b52 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 @@ -18,10 +18,10 @@ export interface LastMileBooking { totalAmount: number; scheduledDate?: string | null; company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null; - serviceType?: { id: string; name?: string } | null; - originYard?: { id: string; name?: string } | null; - destinationYard?: { id: string; name?: string } | null; - cargoType?: { id: string; name?: string } | null; + serviceType?: { id: string; name?: string; label?: string } | null; + originYard?: { id: string; name?: string; label?: string } | null; + destinationYard?: { id: string; name?: string; label?: string } | null; + cargoType?: { id: string; name?: string; label?: string; cargoTypeName?: string } | null; } export interface LastMileVehicle { @@ -60,7 +60,7 @@ 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 }) => + update: (id: string, data: { status?: LastMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null }) => api.patch(LM.BY_ID(id), data), accept: (bookingReference: string) => api.post(LM.ACCEPT(encodeURIComponent(bookingReference))),