Merge branch 'dev' of github.com:Tria-plc/edr-platform into Interchange

This commit is contained in:
hagiye
2026-06-27 21:05:20 +03:00
9 changed files with 126 additions and 27 deletions

View File

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

View File

@@ -225,6 +225,16 @@ export class FirstMileService {
return updated;
}
async updateStatus(id: string, status: FirstMileStatus): Promise<FirstMile> {
const updated = await this.firstMileRepository.update(id, { status });
if (!updated) {
throw new NotFoundException(`First-mile record ${id} not found`);
}
return updated;
}
private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise<void> {
try {
const vehicle = await this.vehiclesService.findById(vehicleId);

View File

@@ -49,4 +49,12 @@ export class CreateVehicleDto {
@IsOptional()
@IsString()
trailerPlateNo?: string;
@IsOptional()
@IsNumber()
estimatedDistanceKm?: number;
@IsOptional()
@IsNumber()
actualDistanceKm?: number;
}

View File

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

View File

@@ -59,6 +59,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: [
@@ -72,6 +74,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" },
],
@@ -86,6 +90,8 @@ export const vehiclesConfig: FleetResourceConfig = {
year: new Date().getFullYear(),
fuelType: "DIESEL",
capacity: 0,
estimatedDistanceKm: "",
actualDistanceKm: "",
status: "ACTIVE",
description: "",
},

View File

@@ -67,7 +67,7 @@ type StatusFilter = "ALL" | FirstMileApiStatus | AssignmentStatus;
const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [
{ value: "ALL", label: "All" },
...FIRST_MILE_STATUSES.map((s) => ({ value: s as StatusFilter, label: STATUS_META[s].label })),
...FIRST_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" },
];
@@ -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",

View File

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

View File

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

View File

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