import { useMemo, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ActionIcon, Badge, Button, Card, Center, Container, Group, Loader, SimpleGrid, Stack, Table, Tabs, Text, Timeline, Title, } from "@mantine/core"; import { SmartFileInput } from "@edr/ui-common"; import type { IFileUploadSetting } from "@edr/types/freight"; import { ArrowLeft, Download, Eye, FileText, History, Route, ShieldCheck, Trash2, Upload, Truck, User, } from "lucide-react"; import { driversService } from "@/services/drivers.service"; import { vehiclesService } from "@/services/vehicles.service"; import { fleetHistoryService, type FleetHistoryEvent } from "@/services/fleet-history.service"; import { fileUploadSettingsService } from "@/services/fileUploadSettings.service"; import { downloadBookingFile, fetchViewableFile, } from "@/services/files.service"; import { useToast } from "@/hooks/use-toast"; 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 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 fmtSize = (bytes: number) => { if (!bytes) return "—"; const kb = bytes / 1024; return kb < 1024 ? `${kb.toFixed(0)} KB` : `${(kb / 1024).toFixed(1)} MB`; }; /** Upload-area setting code configured on the File Settings page. */ const DRIVER_DOCS_CODE = "driver_docs"; // Field key the FALLBACK setting is keyed on (used only when the "driver_docs" // upload area hasn't been configured in File Settings yet). const DRIVER_DOCS_KEY = "driver_docs"; /** Fallback single-field setting so the dropzone still works before an admin * configures the "driver_docs" area in File Settings. */ const DRIVER_DOCS_FALLBACK: IFileUploadSetting = { id: "driver-docs-setting", createdAt: "", updatedAt: "", deletedAt: null, code: "driver_docs", label: "Driver documents", description: null, entity: "other", fields: [ { id: "driver-docs-field", createdAt: "", updatedAt: "", deletedAt: null, settingId: "driver-docs-setting", fileKey: DRIVER_DOCS_KEY, fileLabel: "Upload driver document(s)", helpText: "License, national ID, contracts, training certificates, etc.", isRequired: false, isMultiple: true, maxFiles: 20, allowedExtensions: ["pdf", "png", "jpg", "jpeg", "doc", "docx"], maxSizeMb: 10, order: 1, }, ], }; /** Driver documents upload + view area (files stored under code "driver_docs"). */ const DriverDocuments = ({ driverId }: { driverId: string }) => { const { toast } = useToast(); const qc = useQueryClient(); // Files selected per configured field key (SmartFileInput is multi-field). const [selectedMap, setSelectedMap] = useState>({}); const selectedFiles = Object.values(selectedMap).flatMap((v) => Array.isArray(v) ? v : v ? [v] : [], ); // Upload-area configuration from the File Settings page (code "driver_docs"). // Falls back to a default field until an admin configures it there. const { data: setting } = useQuery({ queryKey: ["file-upload-setting", DRIVER_DOCS_CODE], queryFn: () => fileUploadSettingsService.getByCode(DRIVER_DOCS_CODE), retry: false, }); const activeSetting = setting ?? DRIVER_DOCS_FALLBACK; const { data: docs = [], isLoading } = useQuery({ queryKey: ["driver", driverId, "documents"], queryFn: () => driversService.listDocuments(driverId).then((r) => r.data ?? []), enabled: Boolean(driverId), }); const uploadMutation = useMutation({ mutationFn: (files: File[]) => driversService.uploadDocuments(driverId, files), onSuccess: () => { toast({ title: "Documents uploaded" }); void qc.invalidateQueries({ queryKey: ["driver", driverId, "documents"] }); }, onError: (err: unknown) => { const description = (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Upload failed"; toast({ title: "Upload failed", description, variant: "destructive" }); }, }); const removeMutation = useMutation({ mutationFn: (fileId: string) => driversService.removeDocument(driverId, fileId), onSuccess: () => { toast({ title: "Document deleted" }); void qc.invalidateQueries({ queryKey: ["driver", driverId, "documents"] }); }, onError: () => toast({ title: "Delete failed", variant: "destructive" }), }); return ( {activeSetting.label ?? "Upload documents"} Uploaded documents ({docs.length}) {isLoading ? ( ) : docs.length === 0 ? ( No documents uploaded yet. ) : ( Name Size Uploaded Actions {docs.map((doc) => ( {doc.name} {fmtSize(doc.size)} {fmtDate(doc.createdAt)} void fetchViewableFile(doc.id, doc.name).then((f) => window.open(f.url, "_blank"), ) } > void downloadBookingFile(doc.id, doc.name)} > removeMutation.mutate(doc.id)} > ))}
)}
); }; 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 }>Documents {fmtDate(driver.licenseExpiryDate)} } /> )} ); }; const useDriverHistory = (driverId: string) => useQuery({ queryKey: ["driver-history", driverId], queryFn: () => fleetHistoryService.driver(driverId), }); /** id → "code · plate" map so events without a stored plate still show a name. */ const useVehicleMap = () => { const { data } = useQuery({ queryKey: ["vehicles-all"], queryFn: () => vehiclesService.getAll({}).then((r) => r.data), }); return useMemo(() => { const m = new Map(); for (const v of data ?? []) { m.set(v.id, [v.code, v.plateNumber].filter(Boolean).join(" · ") || v.id); } return m; }, [data]); }; const VehiclesTab = ({ driverId }: { driverId: string }) => { const { data = [], isLoading } = useDriverHistory(driverId); const vmap = useVehicleMap(); const rows = data .filter((e) => e.eventType === "DRIVER_ASSIGNED") .map((e) => ({ id: e.id, plate: meta(e, "vehiclePlate") || (e.vehicleId ? vmap.get(e.vehicleId) : null) || "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 vmap = useVehicleMap(); 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") || (e.vehicleId ? vmap.get(e.vehicleId) : null) || "—", status: e.label ?? "—", at: e.createdAt, })), [data, vmap], ); 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;