import { useEffect, useMemo, useState } from "react"; import { Link, useSearchParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; 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 { useLocaleFormat } from "../i18n/format"; import { useGreetingPeriod } from "../i18n/useGreetingPeriod"; import { SeverityBadge, StatusBadge } from "../components/Badge"; import { Icon } from "../components/Icons"; import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome"; import { N8N_STATE_META, MCP_STATE_META } from "../data/integrationLabels"; const FLEET_METRIC_KEYS: Array<{ key: keyof DashboardData["metrics"]; labelKey: string; tone: string }> = [ { key: "available", labelKey: "readiness.available", tone: "ready" }, { key: "rented", labelKey: "readiness.rented", tone: "neutral" }, { key: "cleaning", labelKey: "readiness.cleaning", tone: "neutral" }, { key: "maintenance", labelKey: "readiness.maintenance", tone: "warning" }, { key: "blocked", labelKey: "readiness.blocked", tone: "critical" }, ]; function attentionItemTitle( t: (key: string, options?: Record) => string, item: { rule_type: string; link_ref: string }, ): string { const ruleLabel = t(`quality:ruleTypes.${item.rule_type}`, { defaultValue: item.rule_type.replace(/_/g, " ") }); return `${ruleLabel} — ${item.link_ref}`; } export function Dashboard() { const { t } = useTranslation(["dashboard", "common", "integrations"]); const { formatTime, formatShortDate } = useLocaleFormat(); const greetingPeriod = useGreetingPeriod(); 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(null); const [knowledge, setKnowledge] = useState(null); const [integrationStatus, setIntegrationStatus] = useState(null); const [error, setError] = useState(null); const [severity, setSeverity] = useState("all"); const [query, setQuery] = useState(""); useEffect(() => { api.get("/api/v1/dashboard").then(setData).catch(() => setError(t("common:status.error"))); api.get("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null)); }, []); useEffect(() => { if (!canSeeAutomation) return; api .get("/api/v1/integrations/status") .then(setIntegrationStatus) .catch(() => setIntegrationStatus(null)); }, [canSeeAutomation]); const attention = useMemo(() => data?.attention_items.filter((item) => { const matchesSeverity = severity === "all" || item.severity === severity; const haystack = `${attentionItemTitle(t, item)} ${item.detail} ${item.link_ref}`.toLowerCase(); return matchesSeverity && haystack.includes(query.toLowerCase()); }) ?? [], [data, query, severity, t]); const isUnfiltered = severity === "all" && query === ""; const attentionTiers = useMemo(() => { if (!isUnfiltered) return [{ key: "all", labelKey: "", items: attention.slice(0, 6) }]; const bySeverity = (level: string) => attention.filter((item) => item.severity === level); return [ { key: "high", labelKey: "attention.tierNow", items: bySeverity("high") }, { key: "medium", labelKey: "attention.tierToday", items: bySeverity("medium") }, { key: "low", labelKey: "attention.tierLater", items: bySeverity("low") }, ].filter((tier) => tier.items.length > 0); }, [attention, isUnfiltered]); if (error) return ; if (!data) return ; const latestRun = data.recent_automation[0]; const readyCount = manifest?.scenarios.filter((s) => s.ready).length ?? 0; const greetingTitle = `${t(`greeting.${greetingPeriod}`)}. ${t(`greetingBody.${greetingPeriod}`)}`; return (
{t("viewFleet")}} />
{t("demoStart.title")} {manifest ? t("demoStart.readyCount", { ready: readyCount, total: manifest.scenarios.length }) : t("demoStart.readyCountFallback")}
{user?.role === "operations_manager" && ( )} {t("demoStart.viewScenarios")}

{t("readiness.title")}

{t("readiness.description")}

{FLEET_METRIC_KEYS.map((metric) => (
{t(metric.labelKey)}
{data.metrics[metric.key]}
))}
{t("readiness.openFleet")}
{t("attention.reviewQueue")} : undefined} />
{attention.length === 0 ?

{t("attention.empty")}

: ( <> {attentionTiers.map((tier) => (
{attentionTiers.length > 1 &&

{t(tier.labelKey)}

}
    {tier.items.map((item, index) => { const href = item.issue_ref && canSeeQuality ? `/data-quality/${item.issue_ref}` : item.link_type === "vehicle" ? `/vehicles/${item.link_ref}` : null; const title = attentionItemTitle(t, item); return (
  • {title}

    {item.detail}

    {item.link_ref} {href && ( {title} )}
  • ); })}
))} )}
{t("movements.allBookings")} } /> {data.today.length === 0 ?

{t("movements.empty")}

: (
    {data.today.map((item) => (
  1. {item.kind === "return" ? t("movements.return") : t("movements.departure")}{item.booking_ref}{item.vehicle_ref}
    {item.booking_ref}
  2. ))}
)}
{t("integrationPulse.systemDetail")} : undefined} />
  • {t("integrationPulse.n8nTitle")} {integrationStatus ? t("integrationPulse.n8nSummary", { succeeded: integrationStatus.n8n.succeeded, failed: integrationStatus.n8n.failed }) : latestRun ? t("integrationPulse.n8nLatest", { ref: latestRun.aggregate_ref }) : t("integrationPulse.n8nNoEvidence")}
    {(() => { const meta = integrationStatus ? N8N_STATE_META[integrationStatus.n8n.state] : null; return ( ); })()}
  • {t("integrationPulse.knowledgeTitle")} {knowledge ? t("integrationPulse.knowledgeSummary", { count: knowledge.document_count }) : t("integrationPulse.knowledgeUnavailable")}
  • {t("integrationPulse.mcpTitle")} {integrationStatus?.mcp_hub.registration_enabled ? t("integrationPulse.mcpEnabled") : t("integrationPulse.mcpNotConnected")}
    {(() => { const meta = integrationStatus ? MCP_STATE_META[integrationStatus.mcp_hub.state] : null; return ; })()}
{data.recent_automation.length === 0 ?

{t("recent.empty")}

: (
    {data.recent_automation.slice(0, 4).map((run) => (
  • {t(`integrations:ledger.eventTypes.${run.event_type}`, { defaultValue: run.event_type.replace(/_/g, " ") })}{run.aggregate_ref}
  • ))}
)}
); }