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
@@ -2,93 +2,38 @@ import type { DemoManifest } from "../api/types";
|
||||
|
||||
export interface DemoGuideStep {
|
||||
id: string;
|
||||
title: string;
|
||||
whatYouWillSee: string;
|
||||
whyItMatters: string;
|
||||
startAction: string;
|
||||
expectedOutcome: string;
|
||||
/** Resolves the route to link to, using live manifest data where a scenario supplies
|
||||
* a concrete record (booking/issue ref) instead of hardcoding one that could drift. */
|
||||
route: (manifest: DemoManifest | null) => string;
|
||||
/** CSS selector for the on-page element this step is about. After navigating, the guide
|
||||
* scrolls it into view, moves keyboard focus to it and applies a brief highlight -- this
|
||||
* only works because the target already carries a stable id/class in the page itself, so
|
||||
* a missing target (selector not found post-navigation) is a silent no-op, never an error. */
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export const DEMO_GUIDE_STEPS: DemoGuideStep[] = [
|
||||
{
|
||||
id: "understand-state",
|
||||
title: "1. Begrijp de operationele status",
|
||||
whatYouWillSee: "Het dashboard toont de wagenparkstatus, openstaande aandachtspunten en de bewegingen van vandaag.",
|
||||
whyItMatters: "Een Operations Manager start elke dag met dit overzicht om te bepalen waar ingrijpen nodig is.",
|
||||
startAction: "Open het dashboard en bekijk de aandachtslijst en de tijdlijn van vandaag.",
|
||||
expectedOutcome: "Je ziet welke boekingen, voertuigen of datakwaliteitsproblemen aandacht vragen.",
|
||||
route: () => "/dashboard",
|
||||
},
|
||||
{ id: "understand-state", route: () => "/dashboard", target: "#attention-heading" },
|
||||
{
|
||||
id: "open-booking",
|
||||
title: "2. Open een boeking die aandacht nodig heeft",
|
||||
whatYouWillSee: "De boeking BK-DEMO-RETURN, actief en klaar voor retour vandaag.",
|
||||
whyItMatters: "Retours zijn het moment waarop foute kilometerstanden of schade voor het eerst zichtbaar worden.",
|
||||
startAction: "Open de boeking vanuit het dashboard of de boekingenlijst.",
|
||||
expectedOutcome: "Je ziet de boekingsdetails en de knop om de retour te verwerken.",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings",
|
||||
target: ".record-surface",
|
||||
},
|
||||
{
|
||||
id: "process-return",
|
||||
title: "3. Verwerk een retour met een afwijkende kilometerstand",
|
||||
whatYouWillSee: "Een retourformulier met een vooringevulde, verdachte kilometerstand lager dan de laatst gekende stand.",
|
||||
whyItMatters: "Een dalende kilometerstand wijst op een foutieve invoer of een verwisseld voertuig — dit moet vóór vrijgave worden opgemerkt.",
|
||||
startAction: "Vul de retour in met de voorgestelde waarde en bekijk de serverpreview vóór je bevestigt.",
|
||||
expectedOutcome: "De retour wordt verwerkt, het voertuig krijgt een passende status en er wordt automatisch een datakwaliteitsprobleem aangemaakt.",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings",
|
||||
target: "#return-form-heading",
|
||||
},
|
||||
{
|
||||
id: "handle-quality-issue",
|
||||
title: "4. Bekijk en behandel het gecreëerde datakwaliteitsprobleem",
|
||||
whatYouWillSee: "Een nieuw issue van het type 'kilometerstand-afwijking' bovenaan de werklijst.",
|
||||
whyItMatters: "Elk gedetecteerd probleem heeft één afgebakende oplossingsstap — niets wordt automatisch stilzwijgend gecorrigeerd.",
|
||||
startAction: "Open het datakwaliteitsoverzicht en kies het nieuwste issue.",
|
||||
expectedOutcome: "Je ziet de aanbevolen actie, kiest een oplossing en het issue wordt opgelost met een audit-spoor.",
|
||||
route: () => "/data-quality",
|
||||
},
|
||||
{ id: "handle-quality-issue", route: () => "/data-quality", target: ".table-shell" },
|
||||
{
|
||||
id: "merge-duplicate",
|
||||
title: "5. Beoordeel en behandel een mogelijke dubbele klant",
|
||||
whatYouWillSee: "Twee klantprofielen met hetzelfde e-mailadres en telefoonnummer, naast elkaar vergeleken.",
|
||||
whyItMatters: "Dubbele klanten leiden tot verspreide boekingsgeschiedenis en verwarrende communicatie.",
|
||||
startAction: "Vergelijk beide profielen en kies welk profiel behouden blijft.",
|
||||
expectedOutcome: "De profielen worden samengevoegd, boekingen worden herverbonden en het verliezende profiel wordt een tombstone.",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "duplicate-customer")?.start_path ?? "/data-quality",
|
||||
target: "#compare-heading",
|
||||
},
|
||||
{
|
||||
id: "ask-knowledge",
|
||||
title: "6. Stel een vraag aan de procedureassistent",
|
||||
whatYouWillSee: "Een antwoord met bronvermelding uit de afgebakende demokennisbank.",
|
||||
whyItMatters: "Medewerkers moeten snel een onderbouwd antwoord krijgen over procedures, zonder te gokken.",
|
||||
startAction:
|
||||
'Klik op één van de voorbeeldvragen (bv. "What must I do when a vehicle returns ' +
|
||||
'with damage?"). De geïndexeerde procedures zijn Engelstalig, dus gebruik de ' +
|
||||
"voorgestelde vragen of stel je eigen vraag in het Engels.",
|
||||
expectedOutcome: "Je ziet het antwoord, de gebruikte procedure en de brontekst — of een eerlijk 'onvoldoende informatie' als dat niet aanwezig is.",
|
||||
route: () => "/knowledge",
|
||||
},
|
||||
{
|
||||
id: "check-automation-audit",
|
||||
title: "7. Controleer automatisering en audit trail",
|
||||
whatYouWillSee: "De status van de n8n-aflevering voor je retour, en de bijhorende audit-gebeurtenissen.",
|
||||
whyItMatters: "Elke belangrijke actie moet naspeurbaar zijn: wie deed wat, wanneer, en wat was het gevolg.",
|
||||
startAction: "Open Systemen om de afleverstatus te zien, en Audit trail voor het volledige spoor.",
|
||||
expectedOutcome: "Je ziet een geslaagde (of herstelbare) aflevering en een leesbaar audit-overzicht van je acties.",
|
||||
route: () => "/automation",
|
||||
},
|
||||
{
|
||||
id: "review-real-vs-simulated",
|
||||
title: "8. Bekijk wat echt is, gesimuleerd is, of nog niet gekoppeld",
|
||||
whatYouWillSee: "Een overzicht van wat in deze demo functioneel geïmplementeerd is, wat synthetisch is, en welke koppelingen nog niet live zijn.",
|
||||
whyItMatters: "Een demo is pas overtuigend als bezoekers zelf kunnen nagaan wat echt werkt en wat nog toekomstmuziek is.",
|
||||
startAction: "Lees de pagina 'Over deze demo'.",
|
||||
expectedOutcome: "Je kan zelf uitleggen wat MobilityOps wel en niet is, zonder mondelinge toelichting.",
|
||||
route: () => "/about",
|
||||
},
|
||||
{ id: "ask-knowledge", route: () => "/knowledge", target: "#ask-heading" },
|
||||
{ id: "check-automation-audit", route: () => "/automation", target: ".integration-cards" },
|
||||
{ id: "review-real-vs-simulated", route: () => "/about", target: ".about-cta" },
|
||||
];
|
||||
|
||||
@@ -3,16 +3,17 @@ import type { IntegrationStatus } from "../api/types";
|
||||
// Plain-language status per section 12 of the demo brief: a visitor shouldn't have to
|
||||
// decode raw backend state strings to tell whether an integration is actually working.
|
||||
// `statusClass` maps onto an existing `.status-*` CSS modifier so the badge keeps
|
||||
// correct colour-coding; `label` is the human-readable text shown instead of the raw value.
|
||||
export const N8N_STATE_META: Record<IntegrationStatus["n8n"]["state"], { statusClass: string; label: string }> = {
|
||||
disabled: { statusClass: "not_configured", label: "Not connected" },
|
||||
unavailable: { statusClass: "unavailable", label: "Delivery failed" },
|
||||
degraded: { statusClass: "needs_attention", label: "Retry available" },
|
||||
operational: { statusClass: "available", label: "Operational" },
|
||||
no_evidence: { statusClass: "no_events", label: "Prepared" },
|
||||
// correct colour-coding; `labelKey` resolves to a localized label via the
|
||||
// `integrations:statusLabels.*` namespace (see i18n/locales/*/integrations.json).
|
||||
export const N8N_STATE_META: Record<IntegrationStatus["n8n"]["state"], { statusClass: string; labelKey: string }> = {
|
||||
disabled: { statusClass: "not_configured", labelKey: "notConnected" },
|
||||
unavailable: { statusClass: "unavailable", labelKey: "deliveryFailed" },
|
||||
degraded: { statusClass: "needs_attention", labelKey: "retryAvailable" },
|
||||
operational: { statusClass: "available", labelKey: "operational" },
|
||||
no_evidence: { statusClass: "no_events", labelKey: "prepared" },
|
||||
};
|
||||
|
||||
export const MCP_STATE_META: Record<IntegrationStatus["mcp_hub"]["state"], { statusClass: string; label: string }> = {
|
||||
not_configured: { statusClass: "not_configured", label: "Not connected" },
|
||||
configured: { statusClass: "no_events", label: "Prepared" },
|
||||
export const MCP_STATE_META: Record<IntegrationStatus["mcp_hub"]["state"], { statusClass: string; labelKey: string }> = {
|
||||
not_configured: { statusClass: "not_configured", labelKey: "notConnected" },
|
||||
configured: { statusClass: "no_events", labelKey: "prepared" },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user