mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
Merge pull request #320 from Tria-plc/freight/feature/vehicle_2
Freight/feature/vehicle 2
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDistanceColumnsToVehicles1821000000002 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.vehicles
|
||||
DROP COLUMN IF EXISTS estimated_distance_km,
|
||||
DROP COLUMN IF EXISTS actual_distance_km;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,12 @@ export class CreateVehicleDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
trailerPlateNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
estimatedDistanceKm?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
actualDistanceKm?: number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
|
||||
@@ -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 }) => (
|
||||
<InfoRow label="Pickup location" value={pickupLocation(record)} />
|
||||
<InfoRow label="Destination (origin yard)" value={destinationYardName(record)} />
|
||||
<InfoRow label="Cargo" value={cargoDesc(record)} />
|
||||
<InfoRow label="Price" value={formatPrice(priceAmount(record))} />
|
||||
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment)} />
|
||||
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment)} />
|
||||
<InfoRow label="Contact" value={contactPersonName(record)} />
|
||||
<InfoRow label="Phone" value={contactPhone(record)} />
|
||||
<InfoRow label="Requested date" value={requestedDate(record)} />
|
||||
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
|
||||
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
|
||||
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -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) ?? <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "estimatedKm",
|
||||
header: "Est. Distance (KM)",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.estimatedKm != null ? row.original.estimatedKm : <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "exactKm",
|
||||
header: "Actual Distance (KM)",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
|
||||
@@ -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 }) => (
|
||||
<Stack gap={2}>
|
||||
@@ -130,14 +128,17 @@ const BookingInfo = ({ record }: { record: LastMileRecord }) => (
|
||||
<SimpleGrid cols={2} spacing="sm">
|
||||
<InfoRow label="Customer" value={customerName(record)} />
|
||||
<InfoRow label="Service type" value={serviceTypeName(record)} />
|
||||
<InfoRow label="Origin yard" value={originYardName(record)} />
|
||||
<InfoRow label="Pickup (origin yard)" value={originYardName(record)} />
|
||||
<InfoRow label="Destination" value={deliveryLocation(record)} />
|
||||
<InfoRow label="Cargo" value={cargoDesc(record)} />
|
||||
<InfoRow label="Price" value={formatPrice(priceAmount(record))} />
|
||||
<InfoRow label="Advanced Payment" value={formatPrice(record.advancedPayment)} />
|
||||
<InfoRow label="Post Payment" value={formatPrice(record.remainingPayment)} />
|
||||
<InfoRow label="Contact" value={contactPersonName(record)} />
|
||||
<InfoRow label="Phone" value={contactPhone(record)} />
|
||||
<InfoRow label="Requested date" value={requestedDate(record)} />
|
||||
<InfoRow label="Assigned vehicle" value={vehicleLabel(record) ?? "—"} />
|
||||
<InfoRow label="Est. Distance (KM)" value={record.estimatedKm != null ? String(record.estimatedKm) : "—"} />
|
||||
<InfoRow label="Actual Distance (KM)" value={record.exactKm != null ? String(record.exactKm) : "—"} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -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) ?? <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "estimatedKm",
|
||||
header: "Est. Distance (KM)",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.estimatedKm != null ? row.original.estimatedKm : <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "exactKm",
|
||||
header: "Actual Distance (KM)",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.exactKm != null ? row.original.exactKm : <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
|
||||
@@ -60,7 +60,7 @@ export const firstMileService = {
|
||||
list: (pageSize = 1000) =>
|
||||
api.get<FirstMileListResponse>(`${FM.BASE}?pageSize=${pageSize}`),
|
||||
getById: (id: string) => api.get<FirstMileRecord>(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<FirstMileRecord>(FM.BY_ID(id), data),
|
||||
accept: (bookingReference: string) =>
|
||||
api.post<FirstMileRecord>(FM.ACCEPT(bookingReference)),
|
||||
|
||||
@@ -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<LastMileListResponse>(`${LM.BASE}?pageSize=${pageSize}`),
|
||||
getById: (id: string) => api.get<LastMileRecord>(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<LastMileRecord>(LM.BY_ID(id), data),
|
||||
accept: (bookingReference: string) =>
|
||||
api.post<LastMileRecord>(LM.ACCEPT(encodeURIComponent(bookingReference))),
|
||||
|
||||
Reference in New Issue
Block a user