/* Machine detail page with tabs */ const MachineDetail = ({ machineId, lang, onBack, onReload }) => { const t = I18N[lang]; const baseMachine = DATA.machines.find(m => m.id === machineId); const machine = baseMachine ? { ...baseMachine, _onReload: onReload } : null; // Legacy-automater starter på inventory (ingen statistics-tab) const initialTab = baseMachine?.kind === 'legacy' ? 'inventory' : 'statistics'; const [tab, setTab] = React.useState(initialTab); if (!machine) return
Ingen automat funnet
; const isLegacyForTabs = baseMachine?.kind === 'legacy'; const isSuper = !!(window.isSuperView && window.isSuperView()); // Innstillinger krever tenant_admin (backend: 403 for tenant_worker/operator). // Fanen skjules derfor for sjåfører/operatører som kommer hit fra Daglig // drift (2026-09-05). roleForTenant tar hensyn til «se som»-simulering. const roleHere = window.roleForTenant ? window.roleForTenant(baseMachine?.tenantId) : null; const canSettings = roleHere === 'superadmin' || roleHere === 'tenant_admin'; const tabs = isLegacyForTabs ? [ { k: 'inventory', label: t.tab_inventory, icon: 'inventory' }, { k: 'shelves', label: t.tab_shelves, icon: 'door' }, { k: 'transactions', label: t.tab_transactions, icon: 'transaction' }, ...(canSettings ? [{ k: 'legacy-settings', label: t.tab_settings, icon: 'settings' }] : []), ] : [ { k: 'statistics', label: t.tab_statistics, icon: 'chart' }, { k: 'inventory', label: t.tab_inventory, icon: 'inventory' }, { k: 'fill', label: lang === 'no' ? 'Påfylling' : 'Refill', icon: 'bottle' }, { k: 'shelves', label: t.tab_shelves, icon: 'door' }, // Mock-revisjon 2026-09-04: den dekorative 'cameras'-fanen (tegnet SVG, // falsk LIVE-HUD, døde slidere) er fjernet — Kamera-oppsett er den ekte. // Kamera-oppsett er infrastruktur (ROI, binding, helse) — kun superadmin. ...(isSuper ? [{ k: 'cam-setup', label: t.tab_cameras, icon: 'camera' }] : []), { k: 'transactions', label: t.tab_transactions, icon: 'transaction' }, { k: 'events', label: t.tab_events, icon: 'bell' }, ...(canSettings ? [{ k: 'settings', label: t.tab_settings, icon: 'settings' }] : []), ]; const isLegacy = machine.kind === 'legacy'; return ( <>

{machine.name}

{isLegacy && ( v1 GAMMEL TYPE — FIRESTORE )} {!isLegacy && ( ny type (v2) )} {/* Produksjonsfase fra backend (2026-09-05): test/oppsett vs. i drift */} {!isLegacy && machine.productionStage && ( {machine.productionStage === 'levert' ? (lang === 'no' ? 'i drift' : 'live') : (lang === 'no' ? 'test / oppsett' : 'test / setup')} )} {machine.status === 'online' ? {t.live} : machine.lastSeen}
{machine.id} · {machine.address} · {t.firmware} {machine.firmware} {isLegacy && machine.firestoreLocId && <> · Firestore: {machine.firestoreLocId}}
{!isLegacy && ( )}
{isLegacy && (
ℹ v1-automat: kommandoer og innstillinger skrives til Firestore (krever tenant_admin). Automaten kjører v1 på Raspberry Pi og henter kommandoer derfra — svar på dør-/kamerakommandoer kan ta noen sekunder. Ingen påfyllingsflyt (v2-funksjon).
)}
{tabs.map(tb => (
setTab(tb.k)}> {tb.label}
))}
{tab === 'statistics' && } {tab === 'inventory' && } {tab === 'fill' && } {tab === 'shelves' && } {tab === 'cam-setup' && } {tab === 'transactions' && } {tab === 'events' && } {tab === 'settings' && } {tab === 'legacy-settings' && }
); }; /* ===== Statistics tab ===== */ const Statistics = ({ machine, lang }) => { const t = I18N[lang]; const isSuper = !!(window.isSuperView && window.isSuperView()); const [range, setRange] = React.useState('30'); // EKTE data (mock-revisjon 2026-09-04): alt under kommer fra // /api/admin/sales/series — Math.sin-illustrasjonene er fjernet. const [serie, setSerie] = React.useState(null); React.useEffect(() => { let dau = false; window.api.salesSeries(parseInt(range, 10), machine.id) .then(d => { if (!dau) setSerie(d); }) .catch(() => { if (!dau) setSerie({ feil: true }); }); return () => { dau = true; }; }, [range, machine.id]); const s = serie && !serie.feil ? serie : null; const dagRevs = s ? s.series.map(x => x.revenue_ore) : []; const maxRev = Math.max(1, ...dagRevs); const bars = dagRevs.map(v => Math.max(2, (v / maxRev) * 100)); // Backend gir ØRE — fmtKr forventer KRONER (før viste omsetning/salgs- // fordeling 100× for høyt, f.eks. «190 000 kr» for 1 900 kr). const totalRev = s ? s.revenue_total_ore / 100 : 0; const swapTx = s ? s.split.exchange.count : 0; const buyTx = s ? s.split.buy.count : 0; const swapRev = s ? s.split.exchange.revenue_ore / 100 : 0; const buyRev = s ? s.split.buy.revenue_ore / 100 : 0; const matrix = s ? s.matrix : null; const maxCell = matrix ? Math.max(1, ...matrix.flat()) : 1; const days = lang === 'no' ? ['Man','Tir','Ons','Tor','Fre','Lør','Søn'] : ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; return ( <> {serie && serie.feil && (
{lang === 'no' ? 'Kunne ikke hente salgsserien.' : 'Could not load sales series.'}
)}
{t.revenue} ({range}d)
{s ? fmtKr(totalRev, lang) : '—'}
{t.transactions}
{s ? fmtNum(s.orders_total, lang) : '—'}
{t.avg_basket}
{s && s.orders_total > 0 ? fmtKr(totalRev / s.orders_total, lang) : '—'}
{/* AI-konfidens er drifts-/plattforminfo — kun superadmin */} {isSuper && (
{t.classification_conf}
{s && s.avg_detected_score != null ? `${Math.round(s.avg_detected_score * 100)} %` : '—'}
)}

{t.revenue} · {range === '7' ? t.last7 : range === '30' ? t.last30 : t.last90}

{bars.map((v, i) => (
))}
{lang === 'no' ? `${range} dager siden` : `${range} days ago`} {lang === 'no' ? 'Snitt' : 'Avg'} {s ? fmtKr(totalRev / Math.max(1, bars.length), lang) : '—'}/d {lang === 'no' ? 'I dag' : 'Today'}

{t.sales_split}

{t.swap} {fmtKr(swapRev, lang)}
{swapTx} {lang === 'no' ? 'transaksjoner' : 'transactions'}
{t.buy} ({lang === 'no' ? 'med ny flaske' : 'with new bottle'}) {fmtKr(buyRev, lang)}
{buyTx} {lang === 'no' ? 'transaksjoner' : 'transactions'}

{t.top_bottles}

{/* EKTE salgstall per flasketype (før: lagerbeholdning feilmerket som «mest solgte»). Kilde: /sales/series by_bottle. */} {(s?.by_bottle || []).map(b => { const bt = (window.DATA?.BOTTLE_TYPES || []).find(x => x.id === b.code); return ( ); })} {(!s || (s.by_bottle || []).length === 0) && ( )}
{lang === 'no' ? 'Flaske' : 'Bottle'} {lang === 'no' ? 'Siste uke' : 'Last week'} {lang === 'no' ? 'Siste mnd' : 'Last month'} {lang === 'no' ? 'Totalt (12 mnd)' : 'Total (12 mo)'} {lang === 'no' ? 'Andel' : 'Share'}
{bt?.image ? ( { e.target.style.display = 'none'; }} /> ) : } {bt?.name || b.name || b.code} {b.count_7d} {b.count_30d} {b.count_365d}
{b.share_pct}%
{lang === 'no' ? 'Ingen salg registrert ennå' : 'No sales recorded yet'}

{t.busy_hours}

{/* Mobil: heatmapen side-scroller i egen boks i stedet for å bli grøt */}
{[0,1,2,3,4,5,6].map((d) => ( {days[d]}
{Array.from({ length: 24 }).map((_, h) => { // EKTE ordretall per ukedag×time (mock-revisjon 2026-09-04) const v = matrix ? matrix[d][h] / maxCell : 0; return
; })}
))}
{Array.from({ length: 24 }).map((_, h) => (
{h % 3 === 0 ? h : ''}
))}
); }; const BarRow = ({ pct }) => (
); const BottleGlyph = ({ type, size = 18 }) => { // Farge basert på material — stål=grå, kompositt=oransje, aluminium=blå const mat = window.bottleHelpers?.material(type) || 'unknown'; const color = { stal: '#94a3b8', lettstal: '#cbd5e1', kompositt: '#fb923c', aluminium: '#60a5fa', }[mat] || 'var(--accent)'; return ( ); }; Object.assign(window, { MachineDetail, BottleGlyph });