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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user