M1: implement operational core

Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
This commit is contained in:
NuklearRabbit
2026-08-01 21:20:53 +02:00
parent 04d26f1f2e
commit 03c5b60235
47 changed files with 2518 additions and 70 deletions
+124
View File
@@ -0,0 +1,124 @@
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { api } from "../api/client";
import type { VehicleDetail as VehicleDetailData } from "../api/types";
import { SeverityBadge, StatusBadge } from "../components/Badge";
const TABS = ["overview", "bookings", "inspections", "maintenance", "quality"] as const;
type Tab = (typeof TABS)[number];
export function VehicleDetail() {
const { publicRef } = useParams<{ publicRef: string }>();
const [vehicle, setVehicle] = useState<VehicleDetailData | null>(null);
const [error, setError] = useState<string | null>(null);
const [tab, setTab] = useState<Tab>("overview");
useEffect(() => {
if (!publicRef) return;
setVehicle(null);
setError(null);
api
.get<VehicleDetailData>(`/api/v1/vehicles/${publicRef}`)
.then(setVehicle)
.catch(() => setError("This vehicle could not be found."));
}, [publicRef]);
if (error) return <p className="error" role="alert">{error}</p>;
if (!vehicle) return <p>Loading vehicle</p>;
return (
<div className="page">
<p><Link to="/vehicles"> Back to vehicles</Link></p>
<h1>
{vehicle.public_ref} {vehicle.make} {vehicle.model}
</h1>
<p>
<StatusBadge status={vehicle.operational_status} />
{vehicle.attention && <span className="badge severity-high">Needs attention</span>}
</p>
<div role="tablist" aria-label="Vehicle sections" className="tabs">
{TABS.map((t) => (
<button
key={t}
role="tab"
type="button"
aria-selected={tab === t}
className={tab === t ? "active" : ""}
onClick={() => setTab(t)}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
{tab === "overview" && (
<dl className="detail-grid">
<div><dt>Registration</dt><dd>{vehicle.registration_number}</dd></div>
<div><dt>Model year</dt><dd>{vehicle.model_year}</dd></div>
<div><dt>Location</dt><dd>{vehicle.location}</dd></div>
<div><dt>Odometer</dt><dd>{vehicle.odometer_km.toLocaleString("en-GB")} km</dd></div>
<div><dt>Next service</dt><dd>{vehicle.next_service_km.toLocaleString("en-GB")} km</dd></div>
<div><dt>Active</dt><dd>{vehicle.active ? "Yes" : "No"}</dd></div>
</dl>
)}
{tab === "bookings" && (
<ul className="record-list">
{vehicle.bookings.length === 0 && <li>No bookings recorded.</li>}
{vehicle.bookings.map((b) => (
<li key={b.public_ref}>
<Link to={`/bookings/${b.public_ref}`}>{b.public_ref}</Link>
<StatusBadge status={b.status} />
<span>
{new Date(b.starts_at).toLocaleDateString("en-GB")} {new Date(b.ends_at).toLocaleDateString("en-GB")}
</span>
</li>
))}
</ul>
)}
{tab === "inspections" && (
<ul className="record-list">
{vehicle.inspections.length === 0 && <li>No inspections recorded.</li>}
{vehicle.inspections.map((i) => (
<li key={i.public_ref}>
<span>{i.type}</span>
<span>{i.odometer_km.toLocaleString("en-GB")} km</span>
<span>Fuel {i.fuel_level_percent}%</span>
{i.damage_reported && <span className="badge severity-high">Damage</span>}
{i.technical_warning && <span className="badge severity-high">Technical warning</span>}
<time dateTime={i.completed_at}>{new Date(i.completed_at).toLocaleDateString("en-GB")}</time>
</li>
))}
</ul>
)}
{tab === "maintenance" && (
<ul className="record-list">
{vehicle.maintenance.length === 0 && <li>No maintenance records.</li>}
{vehicle.maintenance.map((m) => (
<li key={m.public_ref}>
<span>{m.category}</span>
<span>{m.summary}</span>
<time dateTime={m.occurred_at}>{new Date(m.occurred_at).toLocaleDateString("en-GB")}</time>
</li>
))}
</ul>
)}
{tab === "quality" && (
<ul className="record-list">
{vehicle.quality_issues.length === 0 && <li>No quality issues recorded.</li>}
{vehicle.quality_issues.map((q) => (
<li key={q.public_ref}>
<SeverityBadge severity={q.severity} />
<span>{q.rule_type.replace(/_/g, " ")}</span>
<StatusBadge status={q.status} />
</li>
))}
</ul>
)}
</div>
);
}