From 29524b362d754cf4f598db040274a1e5e02dcbe5 Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 28 Aug 2026 12:45:59 +0200 Subject: [PATCH] feat(fuel): Dismiss button on attention rows + optimistic overlay Click on an AlertCard or AlertListRow to hide the vehicle from 'Attention required' instantly. POST /api/vehicles/{id}/attention-dismiss stamps the field; optimistic state mirrors that locally so the row disappears before the network roundtrip. Error path reverts the overlay and shows window.alert (same pattern as inline-edit). --- src/app/dashboard/fuel/fuel-view.tsx | 121 +++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 6 deletions(-) diff --git a/src/app/dashboard/fuel/fuel-view.tsx b/src/app/dashboard/fuel/fuel-view.tsx index 34d30bb..c1adb9e 100644 --- a/src/app/dashboard/fuel/fuel-view.tsx +++ b/src/app/dashboard/fuel/fuel-view.tsx @@ -10,6 +10,7 @@ import { Banknote, CheckCircle2, Droplets, + EyeOff, Filter, Fuel, Gauge, @@ -211,6 +212,14 @@ export function FuelView({ const [page, setPage] = React.useState(1); const [fleetPage, setFleetPage] = React.useState(1); + // Per-vehicle optimistic dismissals — keyed by vehicleId, value is + // the ISO timestamp the user just clicked. The mapper on the server + // emits attentionDismissedAt, but for an instant hide we merge this + // overlay into the vehicles prop before the flaggedVehicles filter + // runs. router.refresh() pulls the real value from the server. + const [dismissedAt, setDismissedAt] = React.useState>({}); + const [busyId, setBusyId] = React.useState(null); + const filtered = React.useMemo(() => { const q = search.trim().toLowerCase(); return rows.filter((r) => { @@ -234,17 +243,27 @@ export function FuelView({ }); }, [vehicles, projectFilter, search]); + const vehiclesWithOverlay = React.useMemo( + () => + vehicles.map((v) => + dismissedAt[v.id] !== undefined && v.attentionDismissedAt === null + ? { ...v, attentionDismissedAt: dismissedAt[v.id] } + : v, + ), + [vehicles, dismissedAt], + ); + // Attention-required: every flagged vehicle the user hasn't already // acknowledged. Worst problems first: alert desc → warn desc → spend desc. const flaggedVehicles = React.useMemo(() => { - return vehicles + return vehiclesWithOverlay .filter((v) => v.flagged > 0 && !isAlertAcknowledged(v)) .sort((a, b) => { if (b.alert !== a.alert) return b.alert - a.alert; if (b.warn !== a.warn) return b.warn - a.warn; return b.totalSpend - a.totalSpend; }); - }, [vehicles]); + }, [vehiclesWithOverlay]); React.useEffect(() => { setPage(1); @@ -308,6 +327,36 @@ export function FuelView({ } } + async function handleDismiss(vehicleId: string) { + if (busyId !== null) return; + const stamp = new Date().toISOString(); + setBusyId(vehicleId); + setDismissedAt((m) => ({ ...m, [vehicleId]: stamp })); + try { + const res = await fetch( + `/api/vehicles/${vehicleId}/attention-dismiss`, + { method: "POST" }, + ); + if (!res.ok) { + const data: { error?: string } = await res.json().catch(() => ({})); + throw new Error(data.error ?? `Dismiss failed (${res.status})`); + } + // Re-pull server data so the overlay becomes a no-op and any + // other server-side state stays in sync. + router.refresh(); + } catch (err) { + setDismissedAt((m) => { + const next = { ...m }; + delete next[vehicleId]; + return next; + }); + // eslint-disable-next-line no-alert + window.alert(err instanceof Error ? err.message : "Dismiss failed"); + } finally { + setBusyId(null); + } + } + return ( <> {flaggedVehicles.map((v) => (
  • - +
  • ))} @@ -494,7 +548,12 @@ export function FuelView({
      {flaggedVehicles.map((v) => (
    • - +
    • ))}
    @@ -724,7 +783,17 @@ export function FuelView({ ); } -function AlertCard({ v, href }: { v: VehicleSummary; href: string }) { +function AlertCard({ + v, + href, + onDismiss, + busy, +}: { + v: VehicleSummary; + href: string; + onDismiss: (id: string) => void; + busy: boolean; +}) { const dominant = v.alert > 0 ? "alert" : "warn"; const toneClasses = dominant === "alert" @@ -794,12 +863,36 @@ function AlertCard({ v, href }: { v: VehicleSummary; href: string }) { )} + ); } -function AlertListRow({ v, href }: { v: VehicleSummary; href: string }) { +function AlertListRow({ + v, + href, + onDismiss, + busy, +}: { + v: VehicleSummary; + href: string; + onDismiss: (id: string) => void; + busy: boolean; +}) { const dominant = v.alert > 0 ? "alert" : "warn"; const stripeColor = dominant === "alert" ? "bg-red-500" : "bg-[#F2B165]"; @@ -851,6 +944,22 @@ function AlertListRow({ v, href }: { v: VehicleSummary; href: string }) { {v.totalSpend > 0 ? formatCurrency(v.totalSpend) : "—"} +