diff --git a/apps/server/src/routes/snapshots.ts b/apps/server/src/routes/snapshots.ts index da9bf21..ec413cc 100644 --- a/apps/server/src/routes/snapshots.ts +++ b/apps/server/src/routes/snapshots.ts @@ -68,7 +68,44 @@ export async function snapshotRoutes(app: FastifyInstance, db: Db): Promise. */ + * attempts PLUS any recognized plates. Image bytes are at `/api/snapshots/:id`. */ export function fetchSnapshots( identity: string, -): Promise<{ snapshots: SnapshotMeta[]; failures?: SnapshotFailure[] }> { +): Promise<{ snapshots: SnapshotMeta[]; failures?: SnapshotFailure[]; plates?: PlateRead[] }> { return apiFetch(`/api/snapshots/by-identity/${encodeURIComponent(identity)}`); } diff --git a/apps/web/src/lib/i18n/en.ts b/apps/web/src/lib/i18n/en.ts index 97bb73c..852d0c7 100644 --- a/apps/web/src/lib/i18n/en.ts +++ b/apps/web/src/lib/i18n/en.ts @@ -558,5 +558,6 @@ export const en: Catalog = { snapEntry: "entry", snapExit: "exit", snapFailed: "camera unreachable", + plate: "Plate", }, }; diff --git a/apps/web/src/lib/i18n/sq.ts b/apps/web/src/lib/i18n/sq.ts index 765739d..5160df2 100644 --- a/apps/web/src/lib/i18n/sq.ts +++ b/apps/web/src/lib/i18n/sq.ts @@ -573,6 +573,7 @@ export const sq = { snapEntry: "hyrje", snapExit: "dalje", snapFailed: "kamera e paarritshme", + plate: "Targa", }, }; diff --git a/apps/web/src/ui/SnapshotStrip.tsx b/apps/web/src/ui/SnapshotStrip.tsx index f637b0d..0bdacf3 100644 --- a/apps/web/src/ui/SnapshotStrip.tsx +++ b/apps/web/src/ui/SnapshotStrip.tsx @@ -1,7 +1,20 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; -import { fetchSnapshots, snapshotImageUrl } from "../api.js"; +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, @@ -18,17 +31,40 @@ export function SnapshotStrip({ identity }: { identity: string }) { 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) + 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) => (