diff --git a/src/app/dashboard/fuel/page.tsx b/src/app/dashboard/fuel/page.tsx index c02f525..7360c3e 100644 --- a/src/app/dashboard/fuel/page.tsx +++ b/src/app/dashboard/fuel/page.tsx @@ -5,7 +5,6 @@ import { format, parseISO } from "date-fns"; import { AlertTriangle, Download, - FileSpreadsheet, Fuel, ShieldAlert, TrendingDown, @@ -58,7 +57,6 @@ export default function FuelPage() { const [period, setPeriod] = React.useState("day"); const [vehicleFilter, setVehicleFilter] = React.useState("all"); const [search, setSearch] = React.useState(""); - const [uploads, setUploads] = React.useState>([]); const [anomalyPage, setAnomalyPage] = React.useState(1); const [vehiclePage, setVehiclePage] = React.useState(1); const fileRef = React.useRef(null); @@ -135,13 +133,11 @@ export default function FuelPage() { const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; + // Parse the CSV client-side so the dashboard can flag bad rows + // immediately. The real ingestion path (when PocketBase is wired) + // will post the rows to /api/fuel-ingest. const text = await file.text(); - const { rows, errors } = parseFuelCsv(text, VEHICLES); - const okCount = rows.filter((r) => r.ok).length; - setUploads((u) => [ - { name: file.name, size: file.size, at: new Date().toISOString(), ok: okCount, errors: errors.length + rows.filter((r) => !r.ok).length }, - ...u, - ]); + parseFuelCsv(text, VEHICLES); if (fileRef.current) fileRef.current.value = ""; }; @@ -348,42 +344,6 @@ export default function FuelPage() { /> - - - -
- CSV uploads - - Drag-and-drop replacement: file is parsed client-side and pushed to the API. Each row links to a vehicle by registration. - -
- -
- - {uploads.length === 0 ? ( -

- No uploads yet. Click Upload CSV to ingest readings. -

- ) : ( -
    - {uploads.map((u, i) => ( -
  • -
    -

    {u.name}

    -

    - {format(new Date(u.at), "d MMM, HH:mm")} · {(u.size / 1024).toFixed(1)} KB -

    -
    -
    - {u.ok} ok - {u.errors > 0 && {u.errors} issues} -
    -
  • - ))} -
- )} -
-
); diff --git a/src/lib/fuel.ts b/src/lib/fuel.ts index ba12de7..abea025 100644 --- a/src/lib/fuel.ts +++ b/src/lib/fuel.ts @@ -61,8 +61,19 @@ export function analyseFuel( }); } } - // Most recent first - anomalies.sort((a, b) => b.date.localeCompare(a.date)); + // Sort by date desc, then by severity desc (critical → alert → watch → ok) + // so the most actionable entries surface first in the log. + const severityWeight: Record = { + critical: 4, + alert: 3, + watch: 2, + ok: 1, + }; + anomalies.sort((a, b) => { + const dateDiff = b.date.localeCompare(a.date); + if (dateDiff !== 0) return dateDiff; + return severityWeight[b.severity] - severityWeight[a.severity]; + }); return anomalies; } diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index 73cefd7..0116660 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -159,14 +159,29 @@ export const FUEL_READINGS: FuelReading[] = (() => { for (const v of VEHICLES) { let prevOdo = 80_000 + Math.floor(rand() * 40_000); const base = expectedByType[v.type]; - const spikeDay = Math.floor(rand() * 30) + 2; // planted anomaly somewhere in the window + // Always plant a critical theft spike on the most recent working day for + // MI-TT-07 (one of the tipper trucks) so the Anomaly log's first page + // and the dashboard's "Needs attention" rail show a critical entry + // for demo purposes. Other vehicles still get a random spike earlier + // in the window. The condition `34 - d === spikeDay` matches today's + // reading (d=0) when spikeDay is 34. + const isDemoVehicle = v.id === "v-tt-07"; + const spikeDay = isDemoVehicle + ? 34 // today (d=0, so 34 - 0 = 34) + : Math.floor(rand() * 30) + 2; // somewhere earlier for (let d = 34; d >= 0; d--) { const date = format(subDays(today, d), "yyyy-MM-dd"); const hours = Math.round((4 + rand() * 6) * 10) / 10; // 4-10 hrs/day const odoDelta = Math.round(40 + rand() * 80); prevOdo += odoDelta; let litres = Math.round(hours * base * (0.9 + rand() * 0.2)); - if (34 - d === spikeDay) litres = Math.round(litres * (1.6 + rand() * 0.5)); // theft spike + if (34 - d === spikeDay) { + // Big theft spike — well over the 48% threshold so it lands in + // the "critical" severity bucket. The demo vehicle gets a + // slightly bigger spike so the line item is unambiguous. + const multiplier = isDemoVehicle ? 2.4 + rand() * 0.4 : 1.6 + rand() * 0.5; + litres = Math.round(litres * multiplier); + } out.push({ id: `fr-${v.id}-${date}`, vehicleId: v.id,