/* Platform Overview — superadmin sin landing page. * * Viser plattform-helse per tenant: hvor mange er klar, hvor mange mangler * Stripe/firma-info, sist aktivitet, alarmer, etc. * * Tilstrebt design: handlingsorientert. Tenants som trenger oppmerksomhet * skal være synlige øverst med tydelig CTA. */ const PlatformOverview = ({ lang, dataVersion, onPickTenant, onNavigate }) => { const [tenants, setTenants] = React.useState([]); const [stripeAccounts, setStripeAccounts] = React.useState([]); const [loading, setLoading] = React.useState(true); // Systemstatus (2026-09-05): EKTE verdier fra /health og /stripe/config. // undefined = laster, null = kunne ikke hentes. const [health, setHealth] = React.useState(undefined); const [stripeCfg, setStripeCfg] = React.useState(undefined); const refresh = React.useCallback(async () => { setLoading(true); window.api.health().then(setHealth).catch(() => setHealth(null)); window.api.stripeConfig().then(setStripeCfg).catch(() => setStripeCfg(null)); try { const [tlist, slist] = await Promise.all([ window.api.tenantsList(), window.api.stripeListAccounts().catch(() => []), ]); // Hent full info per tenant const detailed = await Promise.all(tlist.map(async t => { try { const detail = await window.api.tenantGet(t.id); return { ...t, ...detail }; } catch { return t; } })); setTenants(detailed); setStripeAccounts(slist); } catch (e) { console.warn(e); } setLoading(false); }, []); React.useEffect(() => { refresh(); }, [refresh, dataVersion]); // Klassifiser per setup-status const tenantStatus = (t) => { const stripe = stripeAccounts.find(s => s.tenant_id === t.id); const hasCompanyInfo = t.business_name && t.org_number; const hasStripe = stripe?.ready; const hasAutomats = (DATA.machines || []).some(m => m.tenantId === t.id); const steps = [ { label: 'Firmainfo', done: hasCompanyInfo }, { label: 'Stripe Connect', done: hasStripe }, { label: 'Automat tildelt', done: hasAutomats }, ]; const completed = steps.filter(s => s.done).length; return { steps, completed, total: steps.length, ready: completed === steps.length }; }; // Plattform-sammendrag const totalAutomats = (DATA.machines || []).length; const totalOnline = (DATA.machines || []).filter(m => m.status === 'online').length; const totalLegacy = (DATA.machines || []).filter(m => m.kind === 'legacy').length; const totalV2 = totalAutomats - totalLegacy; const orphanAutomats = (DATA.machines || []).filter(m => !m.tenantId).length; const tenantsReady = tenants.filter(t => tenantStatus(t).ready).length; // Sorter tenants etter "needs attention"-score const sorted = [...tenants].sort((a, b) => { const sa = tenantStatus(a); const sb = tenantStatus(b); return sa.completed - sb.completed; }); return (

{lang === 'no' ? 'Plattform-oversikt' : 'Platform overview'}

{tenants.length} tenants · {totalAutomats} automater ({totalV2} v2 · {totalLegacy} v1)
{/* Sammendrag-KPIer */}
{lang === 'no' ? 'Tenants' : 'Tenants'}
{tenants.length}
{tenantsReady} {lang === 'no' ? 'fullt oppsatt' : 'fully set up'}
{lang === 'no' ? 'Automater' : 'Automats'}
{totalAutomats}
{totalOnline}/{totalAutomats} {lang === 'no' ? 'online' : 'online'}
{lang === 'no' ? 'V1 vs V2' : 'V1 vs V2'}
{totalV2} / {totalLegacy}
v2 / legacy
{orphanAutomats > 0 && (
⚠ {lang === 'no' ? 'Uten tenant' : 'Orphans'}
{orphanAutomats}
onNavigate({ page: 'settings' })} style={{ cursor: 'pointer', color: 'var(--accent)' }}> {lang === 'no' ? 'Tildel tenant →' : 'Assign tenant →'}
)}
{/* Tenants-tabell */}

{lang === 'no' ? 'Tenants — setup-status' : 'Tenants — setup status'}

{tenantsReady}/{tenants.length} {lang === 'no' ? 'klar' : 'ready'}
{loading ? (
⏳
) : tenants.length === 0 ? (
{lang === 'no' ? 'Ingen tenants ennå' : 'No tenants yet'}
) : (
{sorted.map(t => ( ))}
)}
{/* Quick actions */}

{lang === 'no' ? 'Hurtighandlinger' : 'Quick actions'}

{lang === 'no' ? 'System' : 'System'}

{/* Før 2026-09-05: hardkodet «OK»/«tilkoblet»/«stub-modus» (falsk status) */}
Backend {health === undefined ? … : health && health.status === 'ok' ? OK : {lang === 'no' ? 'ikke tilgjengelig' : 'unavailable'}}
MQTT {health === undefined ? … : health && health.mqtt ? {lang === 'no' ? 'tilkoblet' : 'connected'} : {lang === 'no' ? 'frakoblet' : 'disconnected'}}
Stripe {stripeCfg === undefined ? … : !stripeCfg || !stripeCfg.available ? {lang === 'no' ? 'ikke konfigurert' : 'not configured'} : stripeCfg.test_mode ? test : live}
); }; /* Tenant onboarding-card: viser hver tenant + 3 steg med klikkbar CTA per uferdig steg */ const TenantOnboardingCard = ({ t, lang, tenantStatus, onPickTenant, onNavigate }) => { const status = tenantStatus(t); const nAutomats = (DATA.machines || []).filter(m => m.tenantId === t.id).length; const pickAndGo = (page) => { onPickTenant({ id: t.id, name: t.name, sub: '', avatar: t.name.charAt(0).toUpperCase() }); if (page) onNavigate({ page }); }; const goSettings = () => pickAndGo('settings'); const goStripe = () => pickAndGo('settings'); // StripeConnectManager finnes der const goAutomat = () => pickAndGo('settings'); // AutomatTenantManager finnes der // CTA per uferdig steg const stepCtas = [ { key: 'company', done: !!(t.business_name && t.org_number), label: lang === 'no' ? 'Firmainfo (org.nr + adresse)' : 'Company info', cta: lang === 'no' ? 'Fyll ut' : 'Fill in', onClick: goSettings }, { key: 'stripe', done: status.steps[1].done, label: lang === 'no' ? 'Stripe Connect (kortbetaling)' : 'Stripe Connect', cta: lang === 'no' ? 'Sett opp' : 'Set up', onClick: goStripe }, { key: 'automat', done: status.steps[2].done, label: lang === 'no' ? 'Automat tildelt tenant' : 'Automat assigned', cta: lang === 'no' ? 'Tildel' : 'Assign', onClick: goAutomat }, ]; return (
pickAndGo('overview')} style={{ cursor: 'pointer', flex: 1 }}>
{t.name}
{t.id} · {nAutomats} {lang === 'no' ? 'automater' : 'automats'}
{status.ready ? ✓ {lang === 'no' ? 'Klar' : 'Ready'} : {status.completed}/{status.total} {lang === 'no' ? 'fullført' : 'done'} }
{stepCtas.map(s => (
{s.done ? ✓ : ○} {s.label} {!s.done && ( )}
))}
); }; Object.assign(window, { PlatformOverview, TenantOnboardingCard });