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,108 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { Dashboard as DashboardData } from "../api/types";
|
||||
import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
|
||||
const METRIC_LABELS: Record<keyof DashboardData["metrics"], string> = {
|
||||
available: "Available",
|
||||
rented: "Rented",
|
||||
cleaning: "Cleaning",
|
||||
maintenance: "Maintenance",
|
||||
blocked: "Blocked",
|
||||
open_quality_issues: "Open quality issues",
|
||||
pending_or_failed_workflows: "Pending/failed workflows",
|
||||
};
|
||||
|
||||
export function Dashboard() {
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<DashboardData>("/api/v1/dashboard")
|
||||
.then(setData)
|
||||
.catch(() => setError("Dashboard data is unavailable right now."));
|
||||
}, []);
|
||||
|
||||
if (error) return <p className="error" role="alert">{error}</p>;
|
||||
if (!data) return <p>Loading dashboard…</p>;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<section aria-labelledby="metrics-heading">
|
||||
<h2 id="metrics-heading">Operational metrics</h2>
|
||||
<ul className="metric-grid">
|
||||
{(Object.keys(METRIC_LABELS) as (keyof DashboardData["metrics"])[]).map((key) => (
|
||||
<li key={key} className="metric-tile">
|
||||
<span className="metric-value">{data.metrics[key]}</span>
|
||||
<span className="metric-label">{METRIC_LABELS[key]}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="attention-heading" className="panel">
|
||||
<h2 id="attention-heading">Attention required</h2>
|
||||
{data.attention_items.length === 0 && <p>Nothing needs attention right now.</p>}
|
||||
<ul className="attention-list">
|
||||
{data.attention_items.map((item, index) => (
|
||||
<li key={`${item.link_ref}-${index}`}>
|
||||
<SeverityBadge severity={item.severity} />
|
||||
<div>
|
||||
<p className="attention-title">
|
||||
{item.link_type === "vehicle" ? (
|
||||
<Link to={`/vehicles/${item.link_ref}`}>{item.title}</Link>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</p>
|
||||
<p className="attention-detail">{item.detail}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="today-heading" className="panel">
|
||||
<h2 id="today-heading">Today</h2>
|
||||
{data.today.length === 0 && <p>No departures or returns scheduled today.</p>}
|
||||
<ul className="today-list">
|
||||
{data.today.map((item) => (
|
||||
<li key={`${item.kind}-${item.booking_ref}`}>
|
||||
<span className="today-kind">{item.kind === "departure" ? "Departure" : "Return"}</span>
|
||||
<Link to={`/bookings/${item.booking_ref}`}>{item.booking_ref}</Link>
|
||||
<span>{item.vehicle_ref}</span>
|
||||
<time dateTime={item.scheduled_at}>
|
||||
{new Date(item.scheduled_at).toLocaleTimeString("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Brussels",
|
||||
})}
|
||||
</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="automation-heading" className="panel">
|
||||
<h2 id="automation-heading">Recent automation</h2>
|
||||
{data.recent_automation.length === 0 && <p>No automation runs recorded yet.</p>}
|
||||
<ul className="automation-list">
|
||||
{data.recent_automation.map((run) => (
|
||||
<li key={run.event_id}>
|
||||
<StatusBadge status={run.status} />
|
||||
<span>{run.event_type}</span>
|
||||
<span>{run.aggregate_ref}</span>
|
||||
<time dateTime={run.occurred_at}>
|
||||
{new Date(run.occurred_at).toLocaleString("en-GB", { timeZone: "Europe/Brussels" })}
|
||||
</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user