polish: rebrand to Fleet Ops, add trilingual i18n, adaptive demo guide, and UX overhaul
Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with eager-bundled per-namespace resources, a persisted accessible language switcher (topbar and mobile drawer), locale-aware date/number formatting, and a coverage test that fails the build on any missing or empty translation key. Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves from fixed English/Dutch prose to stable message codes + params so the frontend can localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus (11 documents each) with per-language retrieval and localized evidence-state messages. The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a floating panel that auto-collapses to a persistent, closable progress chip on standard desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+ highlight on "go to this step", Escape handling, and reduced-motion support. The Data Quality Workbench gets accessible choice-card decisions with a clear primary/ secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and uses meaningful short refs; the Audit trail groups events by correlation id with human action labels and readable before/after diffs. Attention Queue, Today's movements, Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern) with independent secondary links, keyboard support and mobile touch targets. Fixes a topbar overflow on mobile caused by the new language switcher (moved into the mobile drawer at <=960px) and two dangling aria-labelledby references introduced this session. Updates all affected Playwright specs for the new nl-BE default and the new Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
257a4cf6c0
commit
337f8716bb
@@ -1,147 +1,108 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useDemoGuide } from "../context/DemoGuideContext";
|
||||
import { useDemoManifest } from "../context/DemoManifestContext";
|
||||
import { useLocaleFormat } from "../i18n/format";
|
||||
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",
|
||||
};
|
||||
|
||||
const N8N_STATUS_LABEL_KEY: Record<string, string> = {
|
||||
disabled: "notConnected",
|
||||
unavailable: "deliveryFailed",
|
||||
degraded: "retryAvailable",
|
||||
operational: "operational",
|
||||
no_evidence: "prepared",
|
||||
};
|
||||
|
||||
export function AboutDemo() {
|
||||
const { t } = useTranslation(["demo", "integrations"]);
|
||||
const { formatDateTime } = useLocaleFormat();
|
||||
const { manifest, loading } = useDemoManifest();
|
||||
const { user } = useAuth();
|
||||
const { openGuide } = useDemoGuide();
|
||||
|
||||
function statusLabelKey(key: string, statusCode: string): string {
|
||||
if (key === "n8n") return N8N_STATUS_LABEL_KEY[statusCode] ?? statusCode;
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
eyebrow={t("about.eyebrow")}
|
||||
title={t("about.title")}
|
||||
description={manifest ? t("about.description", { orgName: manifest.organization_name }) : undefined}
|
||||
/>
|
||||
|
||||
{loading && <LoadingState label="Demo-informatie laden…" />}
|
||||
{loading && <LoadingState label={t("about.loading")} />}
|
||||
|
||||
{manifest && (
|
||||
<>
|
||||
{user?.role === "operations_manager" && (
|
||||
<section className="record-surface about-card about-cta">
|
||||
<div>
|
||||
<strong>Liever meteen aan de slag?</strong>
|
||||
<p>De gegidste demo doorloopt alle acht stappen hierboven in de praktijk.</p>
|
||||
<strong>{t("about.ctaTitle")}</strong>
|
||||
<p>{t("about.ctaBody")}</p>
|
||||
</div>
|
||||
<button type="button" className="button button-primary" onClick={openGuide}>
|
||||
<Icon name="spark" /> Start begeleide demo
|
||||
<Icon name="spark" /> {t("about.ctaButton")}
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<h2>{t("about.problemTitle")}</h2>
|
||||
<p>{t("about.problemBody", { orgName: manifest.organization_name })}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Voor wie en met welke scope</h2>
|
||||
<p>
|
||||
Deze demo is bedoeld voor wie wil zien hoe MobilityOps operationele problemen bij
|
||||
een kleine verhuurder aanpakt: Operations Managers en Rental Employees, en
|
||||
iedereen die de aanpak evalueert. De scope is bewust afgebakend tot één
|
||||
samenhangende proof of concept — geen boekhouding, geen betalingen, geen publieke
|
||||
reservaties, geen volledig CRM of ERP.
|
||||
</p>
|
||||
<h2>{t("about.scopeTitle")}</h2>
|
||||
<p>{t("about.scopeBody")}</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>
|
||||
<h2>{t("about.realTitle")}</h2>
|
||||
<p>{t("about.realBody")}</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>
|
||||
<h2>{t("about.syntheticTitle")}</h2>
|
||||
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Architectuur in het kort</h2>
|
||||
<p>
|
||||
Een React/TypeScript-frontend praat met een FastAPI-backend (PostgreSQL via
|
||||
SQLAlchemy/Alembic-migraties); belangrijke bedrijfsregels leven in de backend, niet
|
||||
in n8n of in prompts. Retours en andere gebeurtenissen worden eerst lokaal
|
||||
gecommit en pas daarna asynchroon via een outbox-patroon aan n8n afgeleverd, zodat
|
||||
een tijdelijke storing in de automatisering nooit een operationele actie blokkeert.
|
||||
</p>
|
||||
<h2>{t("about.architectureTitle")}</h2>
|
||||
<p>{t("about.architectureBody")}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Beveiliging en toegang</h2>
|
||||
<p>
|
||||
Toegang verloopt via ondertekende, HTTP-only sessiecookies per rol; elke rol
|
||||
gebonden aan een set toegestane routes, zowel serverzijdig afgedwongen als in de
|
||||
navigatie weerspiegeld. Belangrijke statuswijzigingen worden altijd gecontroleerd
|
||||
en gelogd — nooit stilzwijgend automatisch gecorrigeerd.
|
||||
</p>
|
||||
<h2>{t("about.securityTitle")}</h2>
|
||||
<p>{t("about.securityBody")}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Hoe dit getest is</h2>
|
||||
<p>
|
||||
Een geautomatiseerde backend-testsuite dekt bedrijfsregels en API-contracten;
|
||||
een volledige Playwright-eindtot-eind-suite dekt de gebruikersstromen, inclusief
|
||||
deze demo-ervaring zelf. Elke wijziging wordt bovendien tegen een schone checkout
|
||||
(lege database, opnieuw opgebouwd vanaf de seed-data) gevalideerd voor deployment.
|
||||
</p>
|
||||
<h2>{t("about.testingTitle")}</h2>
|
||||
<p>{t("about.testingBody")}</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."
|
||||
/>
|
||||
<section aria-label={t("about.integrationsTitle")} className="record-surface">
|
||||
<SectionHeading title={t("about.integrationsTitle")} description={t("about.integrationsDescription")} />
|
||||
<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>
|
||||
<span className="integration-kicker">
|
||||
{t(`integrations:statusLabels.${statusLabelKey(integration.key, integration.status_code)}`)}
|
||||
</span>
|
||||
<h2>{t(`integrationSummary.titles.${integration.key}`, { ns: "demo" })}</h2>
|
||||
<p>{t(`integrationSummary.${integration.detail_code}`, { ns: "demo", ...integration.detail_params })}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -149,29 +110,22 @@ export function AboutDemo() {
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>Demo-omgeving herstellen</h2>
|
||||
<h2>{t("about.resetTitle")}</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.</>
|
||||
)}
|
||||
<Trans
|
||||
i18nKey={user?.role === "operations_manager" ? "about.resetBodyManager" : "about.resetBodyEmployee"}
|
||||
t={t}
|
||||
values={{ when: manifest.last_reset_at ? formatDateTime(manifest.last_reset_at) : t("about.unknown") }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="access-note record-surface" aria-label="Beperkingen">
|
||||
<section className="access-note record-surface" aria-label={t("about.limitationsTitle")}>
|
||||
<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>
|
||||
<strong>{t("about.limitationsTitle")}</strong>
|
||||
<span>{t("about.limitationsBody")}</span>
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user