feat(demo): add demo manifest, Dutch demo entry, permanent badge and About page
Adds GET /api/v1/demo/manifest as a single source of truth for the demo's fictional org identity (Northstar Mobility -- surfacing the project's already-locked tenant name), synthetic-data/reset state, and live scenario readiness. Rewrites the login screen in Dutch with an honest, no-password demo entry and a guided-demo entry point, replaces the loud full-width demo banner with a subtle badge + popover, and adds a compact About page explaining what's real vs. synthetic vs. not yet connected.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useDemoManifest } from "../context/DemoManifestContext";
|
||||
import { Icon } from "../components/Icons";
|
||||
import { IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
|
||||
|
||||
function formatDateTime(value: string | null): string {
|
||||
if (!value) return "onbekend";
|
||||
return new Date(value).toLocaleString("nl-BE", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
timeZone: "Europe/Brussels",
|
||||
});
|
||||
}
|
||||
|
||||
const INTEGRATION_ICON: Record<string, "n8n" | "rag" | "mcp"> = {
|
||||
n8n: "n8n",
|
||||
ragcore: "rag",
|
||||
mcp_hub: "mcp",
|
||||
};
|
||||
|
||||
export function AboutDemo() {
|
||||
const { manifest, loading } = useDemoManifest();
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
eyebrow="Over deze demo"
|
||||
title="Wat MobilityOps wel en niet is"
|
||||
description={
|
||||
manifest
|
||||
? `${manifest.organization_name} is een fictieve verhuurorganisatie die dient om deze demo tastbaar te maken — geen bestaand bedrijf.`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{loading && <LoadingState label="Demo-informatie laden…" />}
|
||||
|
||||
{manifest && (
|
||||
<>
|
||||
<section className="record-surface about-card">
|
||||
<h2>Het fictieve probleem</h2>
|
||||
<p>
|
||||
{manifest.organization_name} verhuurt zo'n 50 campers en bestelwagens vanuit één
|
||||
hoofdlocatie. Boekingen, retours, klantgegevens en onderhoud kwamen tot nu toe uit
|
||||
losse spreadsheets en mondelinge afspraken, waardoor fouten (dubbele klanten,
|
||||
foutieve kilometerstanden, dubbel geboekte voertuigen) laat aan het licht kwamen.
|
||||
MobilityOps toont hoe één samenhangend systeem die problemen vroeg signaleert en
|
||||
gecontroleerd laat oplossen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Wat écht werkt</h2>
|
||||
<p>
|
||||
Alles hieronder is functionele code, niet alleen een mockup: rol-gebaseerde
|
||||
toegang en sessies, voertuig- en boekingsbeheer, retourverwerking met
|
||||
serverzijdige validatie, vijf datakwaliteitsregels met elk een eigen
|
||||
oplossingsstap, een volledige audit trail, geautomatiseerde aflevering naar n8n
|
||||
met begrensde herpogingen, Docker-gebaseerde deployment en een geautomatiseerde
|
||||
testsuite (backend en Playwright end-to-end).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Wat synthetisch is</h2>
|
||||
<p>
|
||||
De organisatie, alle klanten, voertuigen, boekingen, onderhoudsgeschiedenis,
|
||||
procedures in de kennisbank en de vooraf ingerichte scenario's zijn volledig
|
||||
verzonnen. Geen enkel gegeven verwijst naar een bestaand persoon, voertuig of
|
||||
bedrijf; e-mailadressen gebruiken uitsluitend het testdomein <code>.test</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-label="Koppelingsstatus" className="record-surface">
|
||||
<SectionHeading
|
||||
title="Koppelingen — eerlijk gelabeld"
|
||||
description="Wat operationeel is, wat demomodus is, en wat nog niet gekoppeld is."
|
||||
/>
|
||||
<div className="integration-cards">
|
||||
{manifest.integrations.map((integration) => (
|
||||
<article key={integration.key}>
|
||||
<IntegrationMark kind={INTEGRATION_ICON[integration.key]} />
|
||||
<div>
|
||||
<span className="integration-kicker">{integration.status_label}</span>
|
||||
<h2>{integration.label}</h2>
|
||||
<p>{integration.detail}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Demo-omgeving herstellen</h2>
|
||||
<p>
|
||||
De omgeving is op elk moment terug te zetten naar de startsituatie. Laatste reset:{" "}
|
||||
<strong>{formatDateTime(manifest.last_reset_at)}</strong>.{" "}
|
||||
{user?.role === "operations_manager" ? (
|
||||
<>
|
||||
Gebruik <strong>Reset demo data</strong> in de zijbalk om opnieuw te beginnen.
|
||||
</>
|
||||
) : (
|
||||
<>Een Operations Manager kan de demo-omgeving herstellen via de zijbalk.</>
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="access-note record-surface" aria-label="Beperkingen">
|
||||
<Icon name="shield" />
|
||||
<p>
|
||||
<strong>Beperkingen</strong>
|
||||
<span>
|
||||
Dit is een gerichte proof of concept, geen volledig ERP. RAGcore en de ITWorx
|
||||
MCP Hub zijn nog niet live gekoppeld; de kennisassistent gebruikt een lokale,
|
||||
afgebakende demokennisbank in plaats van een live RAGcore-omgeving.
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useDemoManifest } from "../context/DemoManifestContext";
|
||||
import type { Role } from "../api/types";
|
||||
import { BrandMark, Icon } from "../components/Icons";
|
||||
|
||||
export function Login() {
|
||||
const { loginAs, loading } = useAuth();
|
||||
const { manifest } = useDemoManifest();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleLogin(role: Role) {
|
||||
async function handleLogin(role: Role, guided = false) {
|
||||
setError(null);
|
||||
try {
|
||||
await loginAs(role);
|
||||
navigate("/dashboard");
|
||||
navigate(guided ? "/dashboard?guide=start" : "/dashboard");
|
||||
} catch {
|
||||
setError("Could not start a demo session. The API may be unavailable.");
|
||||
setError("De demo-sessie kon niet gestart worden. De API is mogelijk niet bereikbaar.");
|
||||
}
|
||||
}
|
||||
|
||||
const orgName = manifest?.organization_name ?? "Northstar Mobility";
|
||||
const description =
|
||||
manifest?.organization_description ??
|
||||
"MobilityOps brengt voertuig-, boekings- en operationele gegevens samen, ondersteunt " +
|
||||
"verhuurprocessen, detecteert datakwaliteitsproblemen en automatiseert gecontroleerde " +
|
||||
"vervolgstappen.";
|
||||
|
||||
return (
|
||||
<main className="login-shell">
|
||||
<section className="login-story" aria-labelledby="product-name">
|
||||
<div className="brand-lockup login-brand"><BrandMark className="brand-mark" /><div><strong>MobilityOps</strong><span>Control centre</span></div></div>
|
||||
<div className="brand-lockup login-brand"><BrandMark className="brand-mark" /><div><strong>MobilityOps</strong><span>Bedieningscentrum</span></div></div>
|
||||
<div className="login-message">
|
||||
<p className="eyebrow">Connected mobility operations</p>
|
||||
<h1 id="product-name">Every hand-off.<br />One clear view.</h1>
|
||||
<p>Turn fleet state, rental returns, data quality and automation into one calm operational rhythm.</p>
|
||||
<p className="eyebrow">Demo-organisatie: {orgName} (fictief)</p>
|
||||
<h1 id="product-name">Elke overdracht.<br />Eén helder overzicht.</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<div className="control-illustration" aria-hidden="true">
|
||||
<div className="illustration-orbit orbit-one"><span /></div>
|
||||
@@ -36,28 +45,39 @@ export function Login() {
|
||||
<span className="illustration-node node-two"><Icon name="bookings" /></span>
|
||||
<span className="illustration-node node-three"><Icon name="quality" /></span>
|
||||
</div>
|
||||
<p className="login-footnote"><Icon name="shield" /> Synthetic proof of concept · no real customer data</p>
|
||||
<p className="login-footnote"><Icon name="shield" /> Synthetische demo · geen echte klant- of voertuiggegevens · op elk moment herstelbaar</p>
|
||||
</section>
|
||||
|
||||
<section className="login-access" aria-labelledby="login-heading">
|
||||
<div className="login-panel">
|
||||
<p className="page-eyebrow">Demo access</p>
|
||||
<h2 id="login-heading">Choose your workspace</h2>
|
||||
<p className="login-intro">No password is required. Each role opens a scoped synthetic environment.</p>
|
||||
<p className="page-eyebrow">Demo-toegang</p>
|
||||
<h2 id="login-heading">Kies hoe je wil starten</h2>
|
||||
<p className="login-intro">Geen wachtwoord nodig. Elke rol opent een afgebakende, synthetische omgeving — alle workflows en controles zijn echt geïmplementeerd.</p>
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="button button-primary login-guide-cta"
|
||||
disabled={loading}
|
||||
onClick={() => handleLogin("operations_manager", true)}
|
||||
>
|
||||
<Icon name="spark" />
|
||||
Start begeleide demo
|
||||
</button>
|
||||
|
||||
<div className="login-options">
|
||||
<button type="button" aria-label="Open as Operations Manager" disabled={loading} onClick={() => handleLogin("operations_manager")}>
|
||||
<button type="button" aria-label="Verken als Operations Manager" disabled={loading} onClick={() => handleLogin("operations_manager")}>
|
||||
<span className="role-icon"><Icon name="activity" /></span>
|
||||
<span><strong>Operations Manager</strong><small>Full overview, quality resolution and retries</small></span>
|
||||
<span><strong>Verken als Operations Manager</strong><small>Volledig overzicht, kwaliteitsoplossing en herpogingen</small></span>
|
||||
<Icon name="chevron" />
|
||||
</button>
|
||||
<button type="button" aria-label="Open as Rental Employee" disabled={loading} onClick={() => handleLogin("rental_employee")}>
|
||||
<button type="button" aria-label="Verken als Rental Employee" disabled={loading} onClick={() => handleLogin("rental_employee")}>
|
||||
<span className="role-icon"><Icon name="user" /></span>
|
||||
<span><strong>Rental Employee</strong><small>Bookings, returns, fleet and procedures</small></span>
|
||||
<span><strong>Verken als Rental Employee</strong><small>Boekingen, retours, wagenpark en procedures</small></span>
|
||||
<Icon name="chevron" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="access-note"><Icon name="shield" /><p><strong>Safe by design</strong><span>All actions are audited and resettable in this demo.</span></p></div>
|
||||
<div className="access-note"><Icon name="shield" /><p><strong>Veilig ontworpen</strong><span>Elke actie wordt gelogd en is in deze demo herstelbaar.</span></p></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user