Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx

265 lines
9.1 KiB
TypeScript

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,
History,
Route,
ShieldCheck,
Truck,
User,
} from "lucide-react";
import { driversService } from "@/services/drivers.service";
import { fleetHistoryService, type FleetHistoryEvent } from "@/services/fleet-history.service";
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 }) => (
<Group justify="space-between" wrap="nowrap">
<Text size="xs" c="dimmed" tt="uppercase">{label}</Text>
<Text size="sm" fw={500} ta="right">{value}</Text>
</Group>
);
const Loading = () => (
<Center py="xl"><Loader size="sm" /></Center>
);
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 (
<Container size="lg" py="md">
<Group gap="sm" mb="md" wrap="nowrap">
<ActionIcon variant="subtle" onClick={() => navigate("/dashboard/drivers")} aria-label="Back">
<ArrowLeft size={18} />
</ActionIcon>
<User size={22} />
<Stack gap={0}>
<Title order={4}>{name || "Driver"}</Title>
{driver && (
<Group gap="xs">
<Badge size="sm" variant="light">{driver.status}</Badge>
{driver.faydaVerified && (
<Badge size="sm" variant="light" color="green" leftSection={<ShieldCheck size={12} />}>
Fayda verified
</Badge>
)}
{licenseExpired && (
<Badge size="sm" variant="light" color="red">License expired</Badge>
)}
<Text size="xs" c="dimmed">{driver.licenseNumber}</Text>
</Group>
)}
</Stack>
</Group>
{isLoading ? (
<Loading />
) : !driver ? (
<Text c="dimmed">Driver not found.</Text>
) : (
<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview" leftSection={<User size={14} />}>Overview</Tabs.Tab>
<Tabs.Tab value="vehicles" leftSection={<Truck size={14} />}>Vehicles</Tabs.Tab>
<Tabs.Tab value="history" leftSection={<History size={14} />}>History</Tabs.Tab>
<Tabs.Tab value="trips" leftSection={<Route size={14} />}>Trips</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview" pt="lg">
<Card withBorder radius="md" padding="md">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
<InfoRow label="Name" value={name || "—"} />
<InfoRow label="Phone" value={driver.phoneNumber ?? "—"} />
<InfoRow label="Email" value={driver.email ?? "—"} />
<InfoRow label="License no." value={driver.licenseNumber ?? "—"} />
<InfoRow
label="License expiry"
value={
<Text size="sm" fw={500} c={licenseExpired ? "red" : undefined}>
{fmtDate(driver.licenseExpiryDate)}
</Text>
}
/>
<InfoRow label="Date of birth" value={fmtDate(driver.dateOfBirth)} />
<InfoRow label="Status" value={driver.status ?? "—"} />
<InfoRow label="Fayda verified" value={driver.faydaVerified ? "Yes" : "No"} />
<InfoRow
label="Authorized vehicles"
value={driver.vehicleTypesAuthorized?.join(", ") || "—"}
/>
<InfoRow label="Total trips" value={driver.totalTrips ?? "—"} />
<InfoRow label="Rating" value={driver.rating ?? "—"} />
<InfoRow label="Address" value={driver.address ?? "—"} />
<InfoRow label="Emergency contact" value={driver.emergencyContact ?? "—"} />
</SimpleGrid>
</Card>
</Tabs.Panel>
<Tabs.Panel value="vehicles" pt="lg">
<VehiclesTab driverId={id} />
</Tabs.Panel>
<Tabs.Panel value="history" pt="lg">
<HistoryTab driverId={id} />
</Tabs.Panel>
<Tabs.Panel value="trips" pt="lg">
<TripsTab driverId={id} />
</Tabs.Panel>
</Tabs>
)}
</Container>
);
};
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 <Loading />;
return (
<Stack gap={4}>
<Text size="sm" fw={600}>Vehicles driven ({rows.length})</Text>
{rows.length === 0 ? (
<Text size="sm" c="dimmed">No vehicle assignments recorded.</Text>
) : (
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr><Table.Th>Vehicle</Table.Th><Table.Th>Assigned</Table.Th></Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((r) => (
<Table.Tr key={r.id}>
<Table.Td>{r.plate}</Table.Td>
<Table.Td>{fmtDateTime(r.at)}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
);
};
const HistoryTab = ({ driverId }: { driverId: string }) => {
const { data = [], isLoading } = useDriverHistory(driverId);
if (isLoading) return <Loading />;
if (!data.length) return <Text c="dimmed">No activity recorded yet.</Text>;
return (
<Timeline active={data.length} bulletSize={18} lineWidth={2}>
{data.map((e) => (
<Timeline.Item key={e.id} title={<Text size="sm" fw={500}>{e.eventType.replaceAll("_", " ")}</Text>}>
{(meta(e, "vehiclePlate") || meta(e, "bookingRef") || e.label) && (
<Text size="xs" c="dimmed">
{[meta(e, "vehiclePlate"), meta(e, "bookingRef") && `Booking ${meta(e, "bookingRef")}`, e.label]
.filter(Boolean)
.join(" · ")}
</Text>
)}
<Text size="xs" c="dimmed" mt={2}>{fmtDateTime(e.createdAt)}</Text>
</Timeline.Item>
))}
</Timeline>
);
};
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 <Loading />;
return (
<Stack gap={4}>
<Text size="sm" fw={600}>Trips assigned ({trips.length})</Text>
{trips.length === 0 ? (
<Text size="sm" c="dimmed">No trips recorded.</Text>
) : (
<Table.ScrollContainer minWidth={640}>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Mile</Table.Th><Table.Th>Booking</Table.Th>
<Table.Th>Vehicle</Table.Th><Table.Th>Status</Table.Th><Table.Th>When</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{trips.map((t) => (
<Table.Tr key={t.id}>
<Table.Td><Badge size="sm" variant="light">{t.mile}</Badge></Table.Td>
<Table.Td>{t.booking}</Table.Td>
<Table.Td>{t.vehicle}</Table.Td>
<Table.Td>{t.status}</Table.Td>
<Table.Td>{fmtDateTime(t.at)}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</Stack>
);
};
export default DriverDetailPage;