feat(demo): make return, data-quality and knowledge flows demo-legible

Fixes a real honesty bug in Knowledge.tsx (body copy named "RAGcore" while
the active provider is the demo one) and a second real bug discovered
while fixing it: the brief's suggested Dutch questions would silently
return "insufficient evidence" against the English-only demo knowledge
base -- verified empirically and fixed by keeping suggested questions in
English. The return flow now pre-fills the odometer-regression scenario's
suspicious reading instead of asking a visitor to invent one, and links
to automation/audit after committing. Data-quality issues get a shared
plain-language "what's wrong / why it matters" explainer per rule type,
a post-resolution confirmation with audit/vehicle links, and a "demo
scenario's only" list filter. Also fixes a real async race where the
odometer pre-fill could clobber text a visitor had already started typing.
This commit is contained in:
NuklearRabbit
2026-08-03 14:41:02 +02:00
parent 6b864596e0
commit ddc3a98e4b
9 changed files with 394 additions and 29 deletions
+42 -6
View File
@@ -1,8 +1,11 @@
import { useState, type FormEvent } from "react";
import { Link } from "react-router-dom";
import { useEffect, useRef, useState, type FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import { api, ApiError } from "../api/client";
import type { RegisterReturnRequest, RegisterReturnResult, ReturnPreviewResult } from "../api/types";
import { useAuth } from "../context/AuthContext";
import { useDemoGuide } from "../context/DemoGuideContext";
import { useDemoManifest } from "../context/DemoManifestContext";
import { DEMO_GUIDE_STEPS } from "../data/demoGuideSteps";
import { Icon } from "./Icons";
import { StatusBadge } from "./Badge";
@@ -14,7 +17,17 @@ function newIdempotencyKey(): string {
export function ReturnResultPanel({ result }: { result: RegisterReturnResult }) {
const { user } = useAuth();
const navigate = useNavigate();
const { manifest } = useDemoManifest();
const { open: guideOpen, currentIndex, completeAndAdvance } = useDemoGuide();
const canSeeQualityIssue = user?.role === "operations_manager";
function continueDemo() {
completeAndAdvance();
const nextIndex = Math.min(currentIndex + 1, DEMO_GUIDE_STEPS.length - 1);
navigate(DEMO_GUIDE_STEPS[nextIndex].route(manifest));
}
return (
<section className="panel return-result success-panel" aria-labelledby="return-result-heading" aria-live="polite">
<div className="result-heading"><span><Icon name="check" /></span><div><p className="page-eyebrow">Committed locally</p><h2 id="return-result-heading">Return registered</h2></div></div>
@@ -58,9 +71,16 @@ export function ReturnResultPanel({ result }: { result: RegisterReturnResult })
opened for review.
</p>
)}
<p>
<div className="result-links">
<Link className="button button-secondary" to={`/vehicles/${result.vehicle_ref}`}>View vehicle {result.vehicle_ref}<Icon name="chevron" /></Link>
</p>
<Link className="button button-secondary" to="/automation">View automation status<Icon name="chevron" /></Link>
<Link className="button button-secondary" to="/audit">View audit trail<Icon name="chevron" /></Link>
{guideOpen && (
<button type="button" className="button button-primary" onClick={continueDemo}>
Ga verder met de demo <Icon name="chevron" />
</button>
)}
</div>
</section>
);
}
@@ -68,11 +88,14 @@ export function ReturnResultPanel({ result }: { result: RegisterReturnResult })
export function ReturnForm({
bookingRef,
onRegistered,
suggestedOdometerKm,
}: {
bookingRef: string;
onRegistered: (result: RegisterReturnResult) => void;
suggestedOdometerKm?: number;
}) {
const [odometer, setOdometer] = useState("");
const [odometer, setOdometer] = useState(suggestedOdometerKm !== undefined ? String(suggestedOdometerKm) : "");
const odometerEditedByUser = useRef(false);
const [fuel, setFuel] = useState("50");
const [cleanlinessOk, setCleanlinessOk] = useState(true);
const [damageReported, setDamageReported] = useState(false);
@@ -85,6 +108,16 @@ export function ReturnForm({
const [step, setStep] = useState<"capture" | "review">("capture");
const [preview, setPreview] = useState<ReturnPreviewResult | null>(null);
useEffect(() => {
// suggestedOdometerKm arrives asynchronously (BookingDetail fetches the vehicle's
// canonical odometer after this form has already mounted), so the initial useState
// value alone won't catch it -- sync it once it resolves. Never overwrite a value the
// visitor has already started typing.
if (suggestedOdometerKm !== undefined && !odometerEditedByUser.current) {
setOdometer(String(suggestedOdometerKm));
}
}, [suggestedOdometerKm]);
function currentBody(): RegisterReturnRequest {
return {
end_odometer_km: Number(odometer),
@@ -151,7 +184,10 @@ export function ReturnForm({
required
min={0}
value={odometer}
onChange={(e) => setOdometer(e.target.value)}
onChange={(e) => {
odometerEditedByUser.current = true;
setOdometer(e.target.value);
}}
/>
</label>