import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { fetchSnapshots, snapshotImageUrl, type PlateRead } from "../api.js"; /** Keep one entry per (plate, direction) — newest wins (the list is newest-first). */ function dedupePlates(plates: PlateRead[]): PlateRead[] { const seen = new Set(); const out: PlateRead[] = []; for (const p of plates) { const key = `${p.plate}|${p.direction}`; if (seen.has(key)) continue; seen.add(key); out.push(p); } return out; } // Entry/exit evidence images for a session. Lets the operator verify the car at the // booth against the ticket. Thumbnails load from /api/snapshots/:id (cookie-authed, // served with a long immutable cache); clicking one enlarges it. Read-only. export function SnapshotStrip({ identity }: { identity: string }) { const { t } = useTranslation(); const { data, isLoading } = useQuery({ queryKey: ["snapshots", identity], queryFn: () => fetchSnapshots(identity), enabled: !!identity, }); const [zoom, setZoom] = useState(null); const shots = data?.snapshots ?? []; const failures = data?.failures ?? []; const plates = data?.plates ?? []; /** Localized direction label for a snapshot/failure tile. */ const dirLabel = (dir: "entry" | "exit" | null): string => dir === "entry" ? t("pay.snapEntry") : dir === "exit" ? t("pay.snapExit") : "—"; if (isLoading) return
{t("pay.loadingSnapshots")}
; if (shots.length === 0 && failures.length === 0 && plates.length === 0) return
{t("pay.noSnapshots")}
; return ( <> {/* Recognized plate(s) (ANPR) — advisory. Dedup by plate+direction so an entry+exit read of the same plate shows once per direction. */} {plates.length > 0 && (
{dedupePlates(plates).map((p, i) => ( {t("pay.plate")} {p.plate} {typeof p.confidence === "number" && ( {(p.confidence * 100).toFixed(0)}% )} ))}
)}
{shots.map((s) => ( ))} {/* Failed captures — a placeholder tile so an absent image is explained, not silently missing. Shown only when no successful shot exists for the same direction (the server already filters recovered captures out). */} {failures.map((f, i) => (
⚠ {dirLabel(f.direction)} {t("pay.snapFailed")}
))}
{zoom && (
setZoom(null)} > snapshot
)} ); }