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,90 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { Vehicle } from "../api/types";
|
||||
import { StatusBadge } from "../components/Badge";
|
||||
|
||||
const STATUS_OPTIONS = ["available", "rented", "cleaning", "maintenance", "blocked"];
|
||||
|
||||
export function Vehicles() {
|
||||
const [vehicles, setVehicles] = useState<Vehicle[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState("");
|
||||
const [attentionOnly, setAttentionOnly] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.set("status", status);
|
||||
if (attentionOnly) params.set("attention_only", "true");
|
||||
api
|
||||
.get<Vehicle[]>(`/api/v1/vehicles?${params.toString()}`)
|
||||
.then(setVehicles)
|
||||
.catch(() => setError("Vehicle list is unavailable right now."));
|
||||
}, [status, attentionOnly]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Vehicles</h1>
|
||||
|
||||
<form className="filters" aria-label="Filter vehicles">
|
||||
<label>
|
||||
Status
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<option value="">All statuses</option>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={attentionOnly}
|
||||
onChange={(e) => setAttentionOnly(e.target.checked)}
|
||||
/>
|
||||
Attention only
|
||||
</label>
|
||||
</form>
|
||||
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
{!error && !vehicles && <p>Loading vehicles…</p>}
|
||||
{vehicles && vehicles.length === 0 && <p>No vehicles match these filters.</p>}
|
||||
|
||||
{vehicles && vehicles.length > 0 && (
|
||||
<table className="data-table">
|
||||
<caption className="visually-hidden">Vehicle fleet</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Reference</th>
|
||||
<th scope="col">Make / model</th>
|
||||
<th scope="col">Location</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Odometer (km)</th>
|
||||
<th scope="col">Attention</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vehicles.map((v) => (
|
||||
<tr key={v.public_ref}>
|
||||
<th scope="row">
|
||||
<Link to={`/vehicles/${v.public_ref}`}>{v.public_ref}</Link>
|
||||
</th>
|
||||
<td>
|
||||
{v.make} {v.model} ({v.model_year})
|
||||
</td>
|
||||
<td>{v.location}</td>
|
||||
<td>
|
||||
<StatusBadge status={v.operational_status} />
|
||||
</td>
|
||||
<td>{v.odometer_km.toLocaleString("en-GB")}</td>
|
||||
<td>{v.attention ? "Needs attention" : "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user