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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user