M2: implement vehicle return vertical slice

Transactional return command with idempotency, row-lock concurrency control, odometer-regression handling, vehicle status derivation, outbox event, audit trail. Result-summary UI on booking detail. 26 backend tests passing, ruff clean. Verified end-to-end via browser against S1 demo scenario; fixed two real defects found only through browser testing (UI state loss on status transition, unflushed UUID default).
This commit is contained in:
NuklearRabbit
2026-08-01 21:49:46 +02:00
parent 03c5b60235
commit 0091c57c7f
14 changed files with 763 additions and 10 deletions
+22 -5
View File
@@ -1,24 +1,36 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { api } from "../api/client";
import type { Booking } from "../api/types";
import type { Booking, RegisterReturnResult } from "../api/types";
import { StatusBadge } from "../components/Badge";
import { ReturnForm, ReturnResultPanel } from "../components/ReturnForm";
export function BookingDetail() {
const { publicRef } = useParams<{ publicRef: string }>();
const [booking, setBooking] = useState<Booking | null>(null);
const [error, setError] = useState<string | null>(null);
const [returnResult, setReturnResult] = useState<RegisterReturnResult | null>(null);
useEffect(() => {
const load = useCallback(() => {
if (!publicRef) return;
setBooking(null);
setError(null);
api
.get<Booking>(`/api/v1/bookings/${publicRef}`)
.then(setBooking)
.catch(() => setError("This booking could not be found."));
}, [publicRef]);
useEffect(() => {
setBooking(null);
setError(null);
setReturnResult(null);
load();
}, [load]);
function handleRegistered(result: RegisterReturnResult) {
setReturnResult(result);
load();
}
if (error) return <p className="error" role="alert">{error}</p>;
if (!booking) return <p>Loading booking</p>;
@@ -36,6 +48,11 @@ export function BookingDetail() {
<div><dt>End odometer</dt><dd>{booking.end_odometer_km ?? "—"} km</dd></div>
<div><dt>Requirements complete</dt><dd>{booking.requirements_complete ? "Yes" : "No"}</dd></div>
</dl>
{returnResult && <ReturnResultPanel result={returnResult} />}
{!returnResult && booking.status === "active" && (
<ReturnForm bookingRef={booking.public_ref} onRegistered={handleRegistered} />
)}
</div>
);
}