/* 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}
)}
{/* 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 });