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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user