/* 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} >}
onReload && onReload()}>
{t.refresh}
{!isLegacy && (
{
try {
await window.api.updateAutomat(machine.id, { maintenance_mode: !machine.maintenanceMode });
if (onReload) await onReload();
} catch (e) { alert(e.message); }
}}>
{machine.maintenanceMode ? (lang === 'no' ? 'Avslutt vedlikehold' : 'End maintenance')
: (lang === 'no' ? 'Sett i vedlikehold' : 'Maintenance mode')}
)}
{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}
setRange('7')}>7d
setRange('30')}>30d
setRange('90')}>90d
{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. */}
{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'}
{(s?.by_bottle || []).map(b => {
const bt = (window.DATA?.BOTTLE_TYPES || []).find(x => x.id === b.code);
return (
{bt?.image ? (
{ e.target.style.display = 'none'; }} />
) : }
{bt?.name || b.name || b.code}
{b.count_7d}
{b.count_30d}
{b.count_365d}
);
})}
{(!s || (s.by_bottle || []).length === 0) && (
{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 });