mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
208 lines
5.8 KiB
TypeScript
208 lines
5.8 KiB
TypeScript
import { Center, Loader, Modal, Text, Timeline } from "@mantine/core";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
Activity,
|
|
CircleDot,
|
|
Route,
|
|
Truck,
|
|
UserCheck,
|
|
UserMinus,
|
|
UserPlus,
|
|
} from "lucide-react";
|
|
|
|
import {
|
|
fleetHistoryService,
|
|
type FleetHistoryEvent,
|
|
} from "@/services/fleet-history.service";
|
|
import type { FleetRecord } from "@/services/fleet/fleet.service";
|
|
|
|
export interface FleetHistoryModalProps {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
entity: "driver" | "vehicle";
|
|
record: FleetRecord | null;
|
|
}
|
|
|
|
const asObj = (r: FleetRecord | null) => (r ?? {}) as Record<string, unknown>;
|
|
|
|
const titleFor = (entity: "driver" | "vehicle", record: FleetRecord | null) => {
|
|
const r = asObj(record);
|
|
if (entity === "vehicle") {
|
|
return `Vehicle history — ${r.plateNumber ?? r.code ?? ""}`.trim();
|
|
}
|
|
return `Driver history — ${[r.firstName, r.lastName]
|
|
.filter(Boolean)
|
|
.join(" ")}`.trim();
|
|
};
|
|
|
|
const mileLabel = (e: FleetHistoryEvent) =>
|
|
e.metadata?.mile === "LAST" ? "Last-mile" : "First-mile";
|
|
|
|
const arrow = (from?: string | null, to?: string | null) =>
|
|
`${from ?? "—"} → ${to ?? "—"}`;
|
|
|
|
const metaStr = (e: FleetHistoryEvent, key: string) => {
|
|
const v = e.metadata?.[key];
|
|
return typeof v === "string" && v ? v : null;
|
|
};
|
|
|
|
function describe(e: FleetHistoryEvent, entity: "driver" | "vehicle") {
|
|
const vehiclePlate = metaStr(e, "vehiclePlate");
|
|
const driverName = metaStr(e, "driverName") ?? (e.label || null);
|
|
const bookingRef = metaStr(e, "bookingRef");
|
|
|
|
// Compose the detail line with whatever the current view doesn't already
|
|
// know: on a driver's timeline show which vehicle; always show the booking.
|
|
const detail = (extra?: string) =>
|
|
[
|
|
entity === "driver" && vehiclePlate ? `Vehicle ${vehiclePlate}` : "",
|
|
bookingRef ? `Booking ${bookingRef}` : "",
|
|
extra ?? "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ");
|
|
|
|
switch (e.eventType) {
|
|
case "DRIVER_REGISTERED":
|
|
return {
|
|
icon: <UserCheck size={14} />,
|
|
title: "Driver registered",
|
|
text: e.toValue ? `Status: ${e.toValue}` : "",
|
|
};
|
|
case "VEHICLE_REGISTERED":
|
|
return {
|
|
icon: <Truck size={14} />,
|
|
title: "Vehicle registered",
|
|
text: e.toValue ? `Availability: ${e.toValue}` : "",
|
|
};
|
|
case "DRIVER_ASSIGNED":
|
|
return {
|
|
icon: <UserPlus size={14} />,
|
|
title: entity === "vehicle" ? "Driver assigned" : "Assigned to vehicle",
|
|
text:
|
|
entity === "vehicle"
|
|
? driverName
|
|
? `Driver ${driverName}`
|
|
: ""
|
|
: vehiclePlate
|
|
? `Vehicle ${vehiclePlate}`
|
|
: "",
|
|
};
|
|
case "DRIVER_UNASSIGNED":
|
|
return {
|
|
icon: <UserMinus size={14} />,
|
|
title:
|
|
entity === "vehicle"
|
|
? "Driver unassigned"
|
|
: "Unassigned from vehicle",
|
|
text:
|
|
entity === "vehicle"
|
|
? driverName
|
|
? `Driver ${driverName}`
|
|
: ""
|
|
: vehiclePlate
|
|
? `Vehicle ${vehiclePlate}`
|
|
: "",
|
|
};
|
|
case "VEHICLE_STATUS_CHANGED":
|
|
return {
|
|
icon: <CircleDot size={14} />,
|
|
title: "Status changed",
|
|
text: arrow(e.fromValue, e.toValue),
|
|
};
|
|
case "VEHICLE_AVAILABILITY_CHANGED":
|
|
return {
|
|
icon: <Activity size={14} />,
|
|
title: `Marked ${e.toValue ?? ""}`.trim(),
|
|
text: e.fromValue ? arrow(e.fromValue, e.toValue) : "",
|
|
};
|
|
case "MILE_VEHICLE_ASSIGNED":
|
|
return {
|
|
icon: <Route size={14} />,
|
|
title: `${mileLabel(e)}: vehicle assigned`,
|
|
text: detail(e.label ? `Status: ${e.label}` : ""),
|
|
};
|
|
case "MILE_VEHICLE_RELEASED":
|
|
return {
|
|
icon: <Route size={14} />,
|
|
title: `${mileLabel(e)}: vehicle released`,
|
|
text: detail(),
|
|
};
|
|
case "MILE_STATUS_CHANGED":
|
|
return {
|
|
icon: <Route size={14} />,
|
|
title: `${mileLabel(e)} status`,
|
|
text: detail(arrow(e.fromValue, e.toValue)),
|
|
};
|
|
default:
|
|
return { icon: <CircleDot size={14} />, title: e.eventType, text: "" };
|
|
}
|
|
}
|
|
|
|
const fmt = (iso: string) => {
|
|
const d = new Date(iso);
|
|
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
|
|
};
|
|
|
|
const FleetHistoryModal = ({
|
|
opened,
|
|
onClose,
|
|
entity,
|
|
record,
|
|
}: FleetHistoryModalProps) => {
|
|
const id = asObj(record).id ? String(asObj(record).id) : "";
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ["fleet-history", entity, id],
|
|
queryFn: () =>
|
|
entity === "vehicle"
|
|
? fleetHistoryService.vehicle(id)
|
|
: fleetHistoryService.driver(id),
|
|
enabled: opened && Boolean(id),
|
|
});
|
|
|
|
const events = data ?? [];
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={<Text fw={600}>{titleFor(entity, record)}</Text>}
|
|
radius="lg"
|
|
size="lg"
|
|
centered
|
|
>
|
|
{isLoading ? (
|
|
<Center py="xl">
|
|
<Loader size="sm" />
|
|
</Center>
|
|
) : events.length === 0 ? (
|
|
<Text c="dimmed" ta="center" py="lg" size="sm">
|
|
No history recorded yet. Activity appears here as this{" "}
|
|
{entity} is assigned, reassigned, or its status changes.
|
|
</Text>
|
|
) : (
|
|
<Timeline active={events.length} bulletSize={24} lineWidth={2}>
|
|
{events.map((e) => {
|
|
const d = describe(e, entity);
|
|
return (
|
|
<Timeline.Item key={e.id} bullet={d.icon} title={d.title}>
|
|
{d.text && (
|
|
<Text size="sm" c="dimmed">
|
|
{d.text}
|
|
</Text>
|
|
)}
|
|
<Text size="xs" mt={4} c="dimmed">
|
|
{fmt(e.createdAt)}
|
|
</Text>
|
|
</Timeline.Item>
|
|
);
|
|
})}
|
|
</Timeline>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default FleetHistoryModal;
|