Remove Fuel CSV uploads card + add demo critical anomaly
Two small cleanup changes: 1. Drop the 'CSV uploads' card at the bottom of the Fuel page. The Template / Upload CSV buttons in the topbar still let users ingest readings, but the recent-uploads mini-list was redundant — the Uploads page already shows every upload. The fuelCsvTemplate import is still in use by the topbar download button. Also clean up the now-unused uploads/setUploads state, the FileSpreadsheet icon, and the unused CardDescription / Badge / FileSpreadsheet references in the bottom block. 2. Plant a guaranteed critical anomaly on the most recent working day so the Anomaly log's first page and the dashboard's 'Needs attention' rail show one for demo purposes. The previous random spike day often landed too far back to be visible without paginating. Now MI-TT-07 (one of the tipper trucks) always has a 2.4x-2.8x theft spike on the latest day, while the other eight vehicles still get a random earlier spike for variety. 3. Sort the Anomaly log by date desc, then by severity desc (critical → alert → watch → ok) so actionable entries surface first within each day. The dashboard's 'Needs attention' rail also benefits from the more recent critical sitting at the top. 4. Restore the Uploads nav item in the sidebar that was removed in the previous commit by mistake.
This commit is contained in:
parent
4b03125002
commit
c6ac2f06b8
3 changed files with 34 additions and 48 deletions
|
|
@ -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<Period>("day");
|
||||
const [vehicleFilter, setVehicleFilter] = React.useState<string>("all");
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [uploads, setUploads] = React.useState<Array<{ name: string; size: number; at: string; ok: number; errors: number }>>([]);
|
||||
const [anomalyPage, setAnomalyPage] = React.useState(1);
|
||||
const [vehiclePage, setVehiclePage] = React.useState(1);
|
||||
const fileRef = React.useRef<HTMLInputElement>(null);
|
||||
|
|
@ -135,13 +133,11 @@ export default function FuelPage() {
|
|||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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() {
|
|||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between">
|
||||
<div>
|
||||
<CardTitle>CSV uploads</CardTitle>
|
||||
<CardDescription>
|
||||
Drag-and-drop replacement: file is parsed client-side and pushed to the API. Each row links to a vehicle by registration.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<FileSpreadsheet className="h-5 w-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{uploads.length === 0 ? (
|
||||
<p className="rounded-lg border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
|
||||
No uploads yet. Click <span className="font-semibold text-foreground">Upload CSV</span> to ingest readings.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{uploads.map((u, i) => (
|
||||
<li key={i} className="flex items-center justify-between rounded-lg border border-border p-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{u.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{format(new Date(u.at), "d MMM, HH:mm")} · {(u.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="success">{u.ok} ok</Badge>
|
||||
{u.errors > 0 && <Badge variant="warning">{u.errors} issues</Badge>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<FuelAnomaly["severity"], number> = {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue