From 18926c32cba8d2c44974699d00591285c0c2cead Mon Sep 17 00:00:00 2001 From: natib21 Date: Fri, 3 Jul 2026 23:24:41 +0000 Subject: [PATCH] enhance phase countdown and booking window descriptions for clarity --- apps/edr-freight-web/backoffice/src/App.tsx | 18 + .../components/fleet/FleetRecordActions.tsx | 19 + .../src/pages/fleet/DriverDetailPage.tsx | 264 ++++++++++ .../src/pages/fleet/VehicleDetailPage.tsx | 453 ++++++++++++++++++ .../src/pages/fleet/config/drivers.ts | 1 + .../src/pages/fleet/config/vehicles.ts | 1 + 6 files changed, 756 insertions(+) create mode 100644 apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 4c74f6993..70b080d6d 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -83,6 +83,8 @@ import UsersPage from "./pages/dashboard/user-management/UsersPage"; import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage"; import FleetResourcePage from "./pages/fleet/FleetResourcePage"; +import VehicleDetailPage from "./pages/fleet/VehicleDetailPage"; +import DriverDetailPage from "./pages/fleet/DriverDetailPage"; import RoutesPage from "./pages/fleet/RoutesPage"; import FuelPurchasePage from "./pages/fleet/FuelPurchasePage"; import FuelStatsPage from "./pages/fleet/FuelStatsPage"; @@ -926,6 +928,14 @@ const App = () => { } /> + + + + } + /> { } /> + + + + } + /> void; onAssignDriver?: (record: FleetRecord) => void; onHistory?: (record: FleetRecord) => void; + onViewDetail?: (record: FleetRecord) => void; layout?: "row" | "compact"; } @@ -22,11 +23,13 @@ const FleetRecordActions = ({ onRemove, onAssignDriver, onHistory, + onViewDetail, layout = "row", }: FleetRecordActionsProps) => { const navigate = useNavigate(); const removeLabel = config.removeActionLabel ?? "Delete"; const showDetail = Boolean(config.detailPath && "id" in record); + const showViewDetail = Boolean(onViewDetail); const isVehicle = config.slug === "vehicles"; const showHistory = Boolean(onHistory) && @@ -62,6 +65,14 @@ const FleetRecordActions = ({ > Edit + {showViewDetail ? ( + onViewDetail?.(record)} + leftSection={} + > + View detail + + ) : null} {showHistory ? ( onHistory?.(record)} @@ -114,6 +125,14 @@ const FleetRecordActions = ({ > Edit + {showViewDetail ? ( + onViewDetail?.(record)} + leftSection={} + > + View detail + + ) : null} {showDetail ? ( { + if (!iso) return "—"; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString(); +}; +const fmtDateTime = (iso?: string | null) => { + if (!iso) return "—"; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); +}; +const meta = (e: FleetHistoryEvent, k: string) => { + const v = e.metadata?.[k]; + return typeof v === "string" && v ? v : null; +}; + +const InfoRow = ({ label, value }: { label: string; value: React.ReactNode }) => ( + + {label} + {value} + +); +const Loading = () => ( +
+); + +const DriverDetailPage = () => { + const { id = "" } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const { data: driver, isLoading } = useQuery({ + queryKey: ["driver", id], + queryFn: () => driversService.getById(id).then((r) => r.data), + enabled: Boolean(id), + }); + + const name = driver ? `${driver.firstName ?? ""} ${driver.lastName ?? ""}`.trim() : ""; + const licenseExpired = + driver?.licenseExpiryDate && new Date(driver.licenseExpiryDate) < new Date(); + + return ( + + + navigate("/dashboard/drivers")} aria-label="Back"> + + + + + {name || "Driver"} + {driver && ( + + {driver.status} + {driver.faydaVerified && ( + }> + Fayda verified + + )} + {licenseExpired && ( + License expired + )} + {driver.licenseNumber} + + )} + + + + {isLoading ? ( + + ) : !driver ? ( + Driver not found. + ) : ( + + + }>Overview + }>Vehicles + }>History + }>Trips + + + + + + + + + + + {fmtDate(driver.licenseExpiryDate)} + + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +const useDriverHistory = (driverId: string) => + useQuery({ + queryKey: ["driver-history", driverId], + queryFn: () => fleetHistoryService.driver(driverId), + }); + +const VehiclesTab = ({ driverId }: { driverId: string }) => { + const { data = [], isLoading } = useDriverHistory(driverId); + const rows = data + .filter((e) => e.eventType === "DRIVER_ASSIGNED") + .map((e) => ({ id: e.id, plate: meta(e, "vehiclePlate") || e.vehicleId || "Vehicle", at: e.createdAt })); + if (isLoading) return ; + return ( + + Vehicles driven ({rows.length}) + {rows.length === 0 ? ( + No vehicle assignments recorded. + ) : ( + + + VehicleAssigned + + + {rows.map((r) => ( + + {r.plate} + {fmtDateTime(r.at)} + + ))} + +
+ )} +
+ ); +}; + +const HistoryTab = ({ driverId }: { driverId: string }) => { + const { data = [], isLoading } = useDriverHistory(driverId); + if (isLoading) return ; + if (!data.length) return No activity recorded yet.; + return ( + + {data.map((e) => ( + {e.eventType.replaceAll("_", " ")}}> + {(meta(e, "vehiclePlate") || meta(e, "bookingRef") || e.label) && ( + + {[meta(e, "vehiclePlate"), meta(e, "bookingRef") && `Booking ${meta(e, "bookingRef")}`, e.label] + .filter(Boolean) + .join(" · ")} + + )} + {fmtDateTime(e.createdAt)} + + ))} + + ); +}; + +const TripsTab = ({ driverId }: { driverId: string }) => { + const { data = [], isLoading } = useDriverHistory(driverId); + const trips = useMemo( + () => + data + .filter((e) => e.eventType === "MILE_VEHICLE_ASSIGNED") + .map((e) => ({ + id: e.id, + mile: meta(e, "mile") === "LAST" ? "Last-mile" : "First-mile", + booking: meta(e, "bookingRef") ?? "—", + vehicle: meta(e, "vehiclePlate") ?? "—", + status: e.label ?? "—", + at: e.createdAt, + })), + [data], + ); + if (isLoading) return ; + return ( + + Trips assigned ({trips.length}) + {trips.length === 0 ? ( + No trips recorded. + ) : ( + + + + + MileBooking + VehicleStatusWhen + + + + {trips.map((t) => ( + + {t.mile} + {t.booking} + {t.vehicle} + {t.status} + {fmtDateTime(t.at)} + + ))} + +
+
+ )} +
+ ); +}; + +export default DriverDetailPage; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx new file mode 100644 index 000000000..76665f385 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/VehicleDetailPage.tsx @@ -0,0 +1,453 @@ +import { useMemo } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { + ActionIcon, + Badge, + Card, + Center, + Container, + Group, + Loader, + SimpleGrid, + Stack, + Table, + Tabs, + Text, + Timeline, + Title, +} from "@mantine/core"; +import { + ArrowLeft, + Fuel, + History, + Route, + Truck, + User, + Wrench, +} from "lucide-react"; + +import { api } from "@/auth/http"; +import { vehiclesService } from "@/services/vehicles.service"; +import { driversService } from "@/services/drivers.service"; +import { fleetHistoryService } from "@/services/fleet-history.service"; + +interface MaintenanceCost { + id: string; + incurredDate: string; + costAmount: number; + costType: string; + description?: string | null; + serviceProvider?: string | null; + invoiceNumber?: string | null; +} +interface FuelPurchase { + id: string; + purchaseDate: string; + liters: number; + costPerLiter: number; + totalCost: number; + fuelStation?: string | null; + odometerReading?: number | null; +} +interface MileRecord { + id: string; + status: string; + exactKm?: number | null; + estimatedKm?: number | null; + remainingPayment?: number | null; + advancedPayment?: number | null; + booking?: { reference?: string } | null; + bookingId: string; +} + +const fmtDate = (iso?: string | null) => { + if (!iso) return "—"; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString(); +}; +const fmtDateTime = (iso?: string | null) => { + if (!iso) return "—"; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); +}; +const money = (n?: number | null) => + n == null ? "—" : `ETB ${Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 })}`; + +const InfoRow = ({ label, value }: { label: string; value: React.ReactNode }) => ( + + {label} + {value} + +); + +const Loading = () => ( +
+); + +const VehicleDetailPage = () => { + const { id = "" } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const { data: vehicle, isLoading } = useQuery({ + queryKey: ["vehicle", id], + queryFn: () => vehiclesService.getById(id).then((r) => r.data), + enabled: Boolean(id), + }); + + const plate = vehicle + ? [vehicle.code, vehicle.plateNumber].filter(Boolean).join(" · ") + : ""; + + return ( + + + navigate("/dashboard/vehicles")} aria-label="Back"> + + + + + {plate || "Vehicle"} + {vehicle && ( + + {vehicle.status} + + {vehicle.availability} + + {vehicle.vehicleType && {vehicle.vehicleType}} + + )} + + + + {isLoading ? ( + + ) : !vehicle ? ( + Vehicle not found. + ) : ( + + + }>Overview + }>Driver + }>History + }>Maintenance + }>Fuel + }>First/Last mile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +const DriverTab = ({ + vehicleId, + driverId, + fallbackName, +}: { + vehicleId: string; + driverId?: string | null; + fallbackName?: string | null; +}) => { + const { data: driver } = useQuery({ + queryKey: ["driver", driverId], + queryFn: () => driversService.getById(driverId!).then((r) => r.data), + enabled: Boolean(driverId), + }); + // All drivers that have driven this vehicle, from the assignment history. + const { data: history = [], isLoading } = useQuery({ + queryKey: ["vehicle-history", vehicleId], + queryFn: () => fleetHistoryService.vehicle(vehicleId), + }); + const drivers = history + .filter((e) => e.eventType === "DRIVER_ASSIGNED") + .map((e) => ({ + id: e.id, + driverId: e.driverId, + name: (typeof e.metadata?.driverName === "string" && e.metadata.driverName) || e.label || "Driver", + at: e.createdAt, + })); + + return ( + + {driverId && driver ? ( + + Current driver + + + + + + + + + + ) : ( + {fallbackName ? `Assigned: ${fallbackName}` : "No driver currently assigned."} + )} + + + Driver history ({drivers.length}) + {isLoading ? ( + + ) : drivers.length === 0 ? ( + No driver assignments recorded. + ) : ( + + + + DriverAssigned + + + + {drivers.map((d) => ( + + {d.name} + {fmtDateTime(d.at)} + + ))} + +
+ )} +
+
+ ); +}; + +const HistoryTab = ({ vehicleId }: { vehicleId: string }) => { + const { data = [], isLoading } = useQuery({ + queryKey: ["vehicle-history", vehicleId], + queryFn: () => fleetHistoryService.vehicle(vehicleId), + }); + if (isLoading) return ; + if (!data.length) return No activity recorded yet.; + return ( + + {data.map((e) => ( + {e.eventType.replaceAll("_", " ")}}> + {(e.label || e.fromValue || e.toValue) && ( + + {[e.label, e.fromValue && e.toValue ? `${e.fromValue} → ${e.toValue}` : e.toValue] + .filter(Boolean) + .join(" · ")} + + )} + {fmtDateTime(e.createdAt)} + + ))} + + ); +}; + +const MaintenanceTab = ({ vehicleId }: { vehicleId: string }) => { + const { data = [], isLoading } = useQuery({ + queryKey: ["vehicle-maintenance", vehicleId], + queryFn: () => api.get(`/maintenance/history/${vehicleId}`).then((r) => r.data), + }); + const total = useMemo(() => data.reduce((s, m) => s + (Number(m.costAmount) || 0), 0), [data]); + if (isLoading) return ; + return ( + + + Last 12 months + Total: {money(total)} + + {data.length === 0 ? ( + No maintenance records. + ) : ( + + + + + DateTypeAmount + DescriptionProvider + + + + {data.map((m) => ( + + {fmtDate(m.incurredDate)} + {m.costType} + {money(m.costAmount)} + {m.description ?? "—"} + {m.serviceProvider ?? "—"} + + ))} + +
+
+ )} +
+ ); +}; + +const FuelTab = ({ vehicleId }: { vehicleId: string }) => { + const { data = [], isLoading } = useQuery({ + queryKey: ["vehicle-fuel", vehicleId], + queryFn: () => { + const end = new Date(); + const start = new Date(); + start.setMonth(start.getMonth() - 12); + const qs = `startDate=${start.toISOString()}&endDate=${end.toISOString()}`; + return api.get(`/fuel/purchases/${vehicleId}?${qs}`).then((r) => r.data); + }, + }); + const totals = useMemo( + () => ({ + liters: data.reduce((s, f) => s + (Number(f.liters) || 0), 0), + cost: data.reduce((s, f) => s + (Number(f.totalCost) || 0), 0), + }), + [data], + ); + if (isLoading) return ; + return ( + + + + Total litres + {totals.liters.toLocaleString(undefined, { maximumFractionDigits: 1 })} L + + + Total fuel cost + {money(totals.cost)} + + + Purchases + {data.length} + + + {data.length === 0 ? ( + No fuel purchases in the last 12 months. + ) : ( + + + + + DateLitresCost/L + TotalOdometerStation + + + + {data.map((f) => ( + + {fmtDate(f.purchaseDate)} + {f.liters} L + {money(f.costPerLiter)} + {money(f.totalCost)} + {f.odometerReading ?? "—"} + {f.fuelStation ?? "—"} + + ))} + +
+
+ )} +
+ ); +}; + +const mileTotal = (rows: MileRecord[]) => + rows.reduce((s, r) => s + (Number(r.remainingPayment) || 0), 0); + +const MileTable = ({ title, rows }: { title: string; rows: MileRecord[] }) => ( + + + {title} ({rows.length}) + Total: {money(mileTotal(rows))} + + {rows.length === 0 ? ( + None. + ) : ( + + + + BookingStatus + Distance (km)Cost + + + + {rows.map((r) => ( + + {r.booking?.reference ?? r.bookingId} + {r.status} + {r.exactKm ?? r.estimatedKm ?? "—"} + {money(r.remainingPayment)} + + ))} + +
+ )} +
+); + +const MileTab = ({ vehicleId }: { vehicleId: string }) => { + const first = useQuery({ + queryKey: ["vehicle-first-mile", vehicleId], + queryFn: () => + api.get<{ data: MileRecord[] }>(`/first-mile?vehicleId=${vehicleId}&pageSize=1000`).then((r) => r.data.data ?? []), + }); + const last = useQuery({ + queryKey: ["vehicle-last-mile", vehicleId], + queryFn: () => + api.get<{ data: MileRecord[] }>(`/last-mile?vehicleId=${vehicleId}&pageSize=1000`).then((r) => r.data.data ?? []), + }); + if (first.isLoading || last.isLoading) return ; + const firstRows = first.data ?? []; + const lastRows = last.data ?? []; + const grandTotal = mileTotal(firstRows) + mileTotal(lastRows); + return ( + + + + Total first + last mile revenue for this vehicle + {money(grandTotal)} + + + + + + ); +}; + +export default VehicleDetailPage; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts index f240bda09..fdfdd367d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts @@ -19,6 +19,7 @@ export const driversConfig: FleetResourceConfig = { label: "Drivers", subtitle: "Manage driver records and licenses", basePath: "/dashboard/drivers", + detailPath: "/dashboard/drivers/:id", addLabel: "Add Driver", entityLabel: "Driver", searchPlaceholder: "Search drivers…", 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 de6eb09c7..5e76584ed 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 @@ -34,6 +34,7 @@ export const vehiclesConfig: FleetResourceConfig = { label: "Vehicles", subtitle: "Manage vehicle master data for fleet operations", basePath: "/dashboard/vehicles", + detailPath: "/dashboard/vehicles/:id", addLabel: "Add Vehicle", entityLabel: "Vehicle", searchPlaceholder: "Search vehicles…",