feat(demo): add Demo Guide (8-step guided tour) and scenario overview

Adds a compact "Probeer een demonstratiescenario" page listing the 5 named
scenarios with live readiness from the manifest, plus a Demo Guide side
panel (bottom sheet on mobile) that walks an Operations Manager through
all 8 steps with per-step context, live-resolved routes, sessionStorage
progress, and a "Demo opnieuw voorbereiden" restart. Login's guided-demo
CTA now actually opens the guide. Fixes a real mobile topbar overflow the
new guide trigger introduced.
This commit is contained in:
NuklearRabbit
2026-08-03 14:15:15 +02:00
parent c63903cc94
commit 9fff84dc68
11 changed files with 647 additions and 29 deletions
+36 -1
View File
@@ -1,8 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { Link, useSearchParams } from "react-router-dom";
import { api } from "../api/client";
import type { Dashboard as DashboardData, IntegrationStatus, KnowledgeHealth } from "../api/types";
import { useAuth } from "../context/AuthContext";
import { useDemoGuide } from "../context/DemoGuideContext";
import { useDemoManifest } from "../context/DemoManifestContext";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { Icon } from "../components/Icons";
import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
@@ -26,8 +28,21 @@ function localTime(value: string, withDate = false) {
export function Dashboard() {
const { user } = useAuth();
const { manifest } = useDemoManifest();
const { openGuide, restart, currentIndex, completed, totalSteps } = useDemoGuide();
const [searchParams, setSearchParams] = useSearchParams();
const canSeeQuality = user?.role === "operations_manager";
const canSeeAutomation = user?.role === "operations_manager";
useEffect(() => {
if (searchParams.get("guide") === "start") {
restart();
openGuide();
setSearchParams({}, { replace: true });
}
// Only ever react to the initial `?guide=start` marker set by the login screen's
// "Start begeleide demo" CTA, so this intentionally runs once on mount.
}, []);
const [data, setData] = useState<DashboardData | null>(null);
const [knowledge, setKnowledge] = useState<KnowledgeHealth | null>(null);
const [integrationStatus, setIntegrationStatus] = useState<IntegrationStatus | null>(null);
@@ -64,6 +79,26 @@ export function Dashboard() {
<div className="page dashboard-page">
<PageHeader eyebrow="Operations / Live overview" title="Good morning. Here’s the fleet." description="Readiness, exceptions and hand-offs across today’s operation." actions={<Link className="button button-secondary" to="/vehicles"><Icon name="fleet" /> View fleet</Link>} />
<section className="demo-start-panel" aria-label="Demo starten">
<div>
<Icon name="spark" />
<div>
<strong>Probeer een demonstratiescenario</strong>
<span>
{manifest ? `${manifest.scenarios.filter((s) => s.ready).length} van ${manifest.scenarios.length} scenario's klaar voor demo.` : "Vijf afgebakende scenario's."}
</span>
</div>
</div>
<div className="demo-start-actions">
{user?.role === "operations_manager" && (
<button type="button" className="button button-secondary" onClick={openGuide}>
<Icon name="spark" /> {completed.size > 0 ? `Verder met demo-gids (${currentIndex + 1}/${totalSteps})` : "Start demo-gids"}
</button>
)}
<Link className="button button-primary" to="/scenarios">Bekijk scenario's <Icon name="chevron" /></Link>
</div>
</section>
<section className="readiness-band" aria-labelledby="readiness-heading">
<div className="readiness-label">
<span className="live-indicator" />
+67
View File
@@ -0,0 +1,67 @@
import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import { useDemoManifest } from "../context/DemoManifestContext";
import { Icon } from "../components/Icons";
import { LoadingState, PageHeader } from "../components/PageChrome";
const ROLE_LABEL: Record<string, string> = {
operations_manager: "Operations Manager",
rental_employee: "Rental Employee",
};
export function Scenarios() {
const { manifest, loading } = useDemoManifest();
const { user } = useAuth();
return (
<div className="page">
<PageHeader
eyebrow="Demonstratiescenario's"
title="Probeer een demonstratiescenario"
description="Vijf afgebakende scenario's die telkens dezelfde vaste boekingen, klanten en voertuigen gebruiken — na een reset zijn ze altijd opnieuw te vinden."
/>
{loading && <LoadingState label="Scenario's laden…" />}
{manifest && (
<div className="scenario-grid">
{manifest.scenarios.map((scenario) => {
const canRun = !user || scenario.required_roles.includes(user.role);
return (
<article key={scenario.id} className="scenario-card">
<header>
<h2>{scenario.title}</h2>
<span className={`badge ${scenario.ready ? "status-available" : "status-unavailable"}`}>
{scenario.ready ? "Klaar voor demo" : "Niet beschikbaar"}
</span>
</header>
<p className="scenario-problem">{scenario.operational_problem}</p>
<dl className="scenario-meta">
<div><dt>Duur</dt><dd>± {scenario.estimated_minutes} min</dd></div>
<div><dt>Rol</dt><dd>{scenario.required_roles.map((r) => ROLE_LABEL[r]).join(" of ")}</dd></div>
</dl>
<p className="scenario-demonstrates"><strong>Toont aan:</strong> {scenario.demonstrates}</p>
{!scenario.ready && scenario.blocked_reason && (
<p className="scenario-blocked"><Icon name="alert" /> {scenario.blocked_reason}</p>
)}
{!canRun && (
<p className="scenario-blocked"><Icon name="alert" /> Vereist rol: {scenario.required_roles.map((r) => ROLE_LABEL[r]).join(" of ")}.</p>
)}
<Link
className={`button ${scenario.ready && canRun ? "button-primary" : "button-secondary"}`}
to={scenario.start_path}
aria-disabled={!scenario.ready || !canRun}
onClick={(event) => {
if (!scenario.ready || !canRun) event.preventDefault();
}}
>
Start scenario <Icon name="chevron" />
</Link>
</article>
);
})}
</div>
)}
</div>
);
}