feat(ui): redesign fleet and booking workflows

This commit is contained in:
NuklearRabbit
2026-08-02 03:27:13 +02:00
parent 6f0054c878
commit 3d78cac850
4 changed files with 68 additions and 49 deletions
+27 -14
View File
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import { api } from "../api/client";
import type { Booking } from "../api/types";
import { StatusBadge } from "../components/Badge";
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
const STATUS_OPTIONS = ["reserved", "active", "returned", "cancelled", "blocked"];
@@ -10,8 +11,13 @@ export function Bookings() {
const [bookings, setBookings] = useState<Booking[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState("");
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const perPage = 25;
useEffect(() => {
setBookings(null);
setError(null);
const params = new URLSearchParams();
if (status) params.set("status", status);
api
@@ -22,12 +28,16 @@ export function Bookings() {
return (
<div className="page">
<h1>Bookings</h1>
<PageHeader eyebrow="Operations / Schedule" title="Bookings" description="Review active rental windows and upcoming vehicle commitments." />
<form className="filters" aria-label="Filter bookings">
<label>
Search
<input type="text" value={query} onChange={(e) => { setQuery(e.target.value); setPage(1); }} placeholder="Booking, customer or vehicle" />
</label>
<label>
Status
<select value={status} onChange={(e) => setStatus(e.target.value)}>
<select value={status} onChange={(e) => { setStatus(e.target.value); setPage(1); }}>
<option value="">All statuses</option>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>
@@ -38,12 +48,15 @@ export function Bookings() {
</label>
</form>
{error && <p className="error" role="alert">{error}</p>}
{!error && !bookings && <p>Loading bookings…</p>}
{bookings && bookings.length === 0 && <p>No bookings match these filters.</p>}
{error && <ErrorState message={error} />}
{!error && !bookings && <LoadingState label="Loading booking ledger…" />}
{bookings && bookings.length === 0 && <EmptyState icon="bookings" title="No bookings found" detail="Adjust the booking status filter." />}
{bookings && bookings.length > 0 && (
<table className="data-table">
{bookings && bookings.length > 0 && (() => {
const filtered = bookings.filter((b) => `${b.public_ref} ${b.customer_name} ${b.vehicle_ref}`.toLowerCase().includes(query.toLowerCase()));
const totalPages = Math.max(1, Math.ceil(filtered.length / perPage));
const visible = filtered.slice((page - 1) * perPage, page * perPage);
return filtered.length === 0 ? <EmptyState icon="search" title="No matching bookings" detail="Try a broader search term." /> : <div className="table-shell"><div className="table-meta"><span>{filtered.length} bookings</span><span>Page {page} of {totalPages}</span></div><table className="data-table">
<caption className="visually-hidden">Bookings</caption>
<thead>
<tr>
@@ -57,24 +70,24 @@ export function Bookings() {
<tbody>
{bookings.map((b) => (
<tr key={b.public_ref}>
<th scope="row">
<th scope="row" data-label="Reference">
<Link to={`/bookings/${b.public_ref}`}>{b.public_ref}</Link>
</th>
<td>{b.customer_name}</td>
<td>
<td data-label="Customer">{b.customer_name}</td>
<td data-label="Vehicle">
<Link to={`/vehicles/${b.vehicle_ref}`}>{b.vehicle_ref}</Link>
</td>
<td>
<td data-label="Window">
{new Date(b.starts_at).toLocaleDateString("en-GB")} → {new Date(b.ends_at).toLocaleDateString("en-GB")}
</td>
<td>
<td data-label="Status">
<StatusBadge status={b.status} />
</td>
</tr>
))}
</tbody>
</table>
)}
</table><div className="pagination" aria-label="Booking pages"><button type="button" disabled={page === 1} onClick={() => setPage((p) => p - 1)}>Previous</button><span>{(page - 1) * perPage + 1}–{Math.min(page * perPage, filtered.length)} of {filtered.length}</span><button type="button" disabled={page === totalPages} onClick={() => setPage((p) => p + 1)}>Next</button></div></div>;
})()}
</div>
);
}