/* @jsx React.createElement */ const { useState, useEffect, useRef, useMemo } = React; /* ============================================================ Yappari — OVERVIEW ============================================================ */ function machineStockSummary(mid) { const slots = window.YAPPARI.SLOTS[mid] || []; let empty = 0, low = 0, value = 0, total = 0; const prodMap = Object.fromEntries(window.YAPPARI.PRODUCTS.map(p => [p.id, p])); slots.forEach(s => { if (s.hidden) return; total++; if (s.cur === 0) empty++; else if (s.cur / s.cap <= 0.34) low++; value += s.cur * (prodMap[s.productId] ? prodMap[s.productId].price : 0); }); return { empty, low, value, total }; } function MachineCard({ m, onOpen }) { const sum = machineStockSummary(m.id); const sales = window.YAPPARI.SALES_BY_MACHINE[m.id]; const tempBad = m.temp != null && m.temp > 10; const stat = (icon, label, value, color) => React.createElement('div', { className: 'col', style: { gap: 2 } }, React.createElement('div', { className: 'row gap-1', style: { color: 'var(--fg-3)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.06em' } }, React.createElement(Icon, { name: icon, size: 13 }), label), React.createElement('div', { style: { fontSize: 15, fontWeight: 700, color: color || 'var(--fg-1)' } }, value), ); return React.createElement('button', { className: 'card', onClick: () => onOpen(m), style: { textAlign: 'left', cursor: 'pointer', border: '1px solid var(--border-subtle)', padding: 0, overflow: 'hidden', background: 'var(--bg-elev)', font: 'inherit', color: 'inherit' }, }, React.createElement('div', { style: { padding: '14px 16px 12px' } }, React.createElement('div', { className: 'row', style: { justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 } }, React.createElement('div', { style: { fontSize: 16, fontWeight: 800, color: 'var(--fg-1)' } }, m.name), React.createElement(StatusBadge, { status: m.status }), ), React.createElement('div', { className: 'row gap-1', style: { color: 'var(--fg-3)', fontSize: 12.5, marginBottom: 14 } }, React.createElement(Icon, { name: 'mapPin', size: 13 }), React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, m.loc), ), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 8px' } }, stat('snow', 'Suhu', m.temp != null ? m.temp + '°C' : '—', tempBad ? 'var(--st-error)' : 'var(--st-online)'), stat('door', 'Pintu', m.door === 'closed' ? 'Tertutup' : m.door === 'open' ? 'Terbuka' : '—', m.door === 'open' ? 'var(--st-warn)' : 'var(--fg-1)'), stat('clock', 'Terlihat', relMins(m.lastSeen), m.status === 'offline' ? 'var(--st-error)' : 'var(--fg-1)'), stat('wallet', 'Omzet', fmtRpShort(sales.totalRev), 'var(--fg-1)'), ), ), React.createElement('div', { style: { padding: '9px 16px', background: 'var(--bg-sunken)', borderTop: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, React.createElement('div', { className: 'row gap-2', style: { fontSize: 12 } }, sum.empty > 0 && React.createElement('span', { className: 'badge badge-error', style: { padding: '2px 8px' } }, sum.empty + ' habis'), sum.low > 0 && React.createElement('span', { className: 'badge badge-warn', style: { padding: '2px 8px' } }, sum.low + ' rendah'), sum.empty === 0 && sum.low === 0 && React.createElement('span', { className: 'badge badge-online', style: { padding: '2px 8px' } }, 'Stok sehat'), ), React.createElement('span', { className: 'row gap-1', style: { fontSize: 12, fontWeight: 700, color: 'var(--st-maint)' } }, 'Detail', React.createElement(Icon, { name: 'chevronRight', size: 14 })), ), ); } function AttentionPanel({ machines, onOpen }) { const items = []; machines.forEach(m => { if (m.status === 'offline') items.push({ m, sev: 'error', icon: 'wifiOff', text: 'Mesin offline · ' + relMins(m.lastSeen) }); else if (m.status === 'error') items.push({ m, sev: 'error', icon: 'alert', text: 'Error perangkat · motor coil' }); else if (m.status === 'maintenance') items.push({ m, sev: 'maint', icon: 'cpu', text: 'Mode maintenance aktif' }); if (m.temp != null && m.temp > 10 && m.status !== 'offline') items.push({ m, sev: 'warn', icon: 'thermometer', text: 'Suhu ' + m.temp + '°C di atas ambang' }); const sum = machineStockSummary(m.id); if (sum.empty >= 4) items.push({ m, sev: 'warn', icon: 'packageX', text: sum.empty + ' slot kehabisan stok' }); }); const sevColor = { error: 'var(--st-error)', warn: 'var(--st-warn)', maint: 'var(--st-maint)' }; const sevBg = { error: 'var(--st-error-bg)', warn: 'var(--st-warn-bg)', maint: 'var(--st-maint-bg)' }; return React.createElement(SectionCard, { title: 'Requires attention', sub: items.length + ' isu aktif', right: React.createElement('span', { className: 'badge badge-error' }, items.length), bodyStyle: { maxHeight: 340, overflowY: 'auto' }, }, items.length === 0 ? React.createElement('div', { style: { padding: 32, textAlign: 'center', color: 'var(--fg-3)' } }, 'Semua mesin sehat ✓') : items.map((it, i) => React.createElement('button', { key: i, onClick: () => onOpen(it.m), style: { display: 'flex', gap: 12, alignItems: 'center', width: '100%', textAlign: 'left', padding: '12px 20px', borderBottom: '1px solid var(--border-subtle)', background: 'transparent', border: 'none', borderLeft: 'none', cursor: 'pointer', font: 'inherit' }, }, React.createElement('div', { style: { width: 34, height: 34, borderRadius: 8, display: 'grid', placeItems: 'center', background: sevBg[it.sev], color: sevColor[it.sev], flexShrink: 0 } }, React.createElement(Icon, { name: it.icon, size: 17 })), React.createElement('div', { style: { flex: 1, minWidth: 0 } }, React.createElement('div', { style: { fontWeight: 700, fontSize: 13.5, color: 'var(--fg-1)' } }, it.m.name), React.createElement('div', { style: { fontSize: 12.5, color: 'var(--fg-3)' } }, it.text), ), React.createElement(Icon, { name: 'chevronRight', size: 16, style: { color: 'var(--fg-4)' } }), )), ); } function useLiveFeed(seedCount = 7, intervalMs = 3200) { const Y = window.YAPPARI; const [feed, setFeed] = useState(() => Y.seedFeed(seedCount).map((f, i) => ({ ...f, fresh: false, key: f.id }))); const counter = useRef(900000); useEffect(() => { const id = setInterval(() => { const online = Y.MACHINES.filter(m => m.status === 'online'); const m = Y.pick(online); const p = Y.pick(Y.PRODUCTS); counter.current++; const tx = { id: 'tx' + counter.current, key: 'tx' + counter.current, machineId: m.id, machineName: m.name, productName: p.name, price: p.price, method: Math.random() < 0.68 ? 'QRIS' : 'Cash', secsAgo: 0, fresh: true }; setFeed(prev => [tx, ...prev.map(x => ({ ...x, secsAgo: x.secsAgo + (3 + Math.floor(Math.random() * 20)), fresh: false }))].slice(0, 22)); }, intervalMs); return () => clearInterval(id); }, []); return feed; } function LiveFeed() { const feed = useLiveFeed(); const relSecs = (s) => s < 5 ? 'baru saja' : s < 60 ? s + ' dtk lalu' : Math.floor(s / 60) + ' mnt lalu'; return React.createElement(SectionCard, { title: 'Live sales feed', sub: 'Transaksi real-time', right: React.createElement('span', { className: 'badge badge-online' }, React.createElement('span', { className: 'b-dot', style: { animation: 'ping 2s infinite' } }), 'Live'), bodyStyle: { maxHeight: 360, overflowY: 'auto' }, }, feed.map((tx) => React.createElement('div', { key: tx.key, className: tx.fresh ? 'feed-new' : '', style: { display: 'flex', gap: 12, alignItems: 'center', padding: '11px 20px', borderBottom: '1px solid var(--border-subtle)' }, }, React.createElement('div', { style: { width: 8, height: 8, borderRadius: '50%', background: tx.method === 'QRIS' ? 'var(--st-maint)' : 'var(--st-online)', flexShrink: 0 } }), React.createElement('div', { style: { flex: 1, minWidth: 0 } }, React.createElement('div', { style: { fontWeight: 700, fontSize: 13.5, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, tx.productName), React.createElement('div', { style: { fontSize: 12, color: 'var(--fg-3)' } }, tx.machineName + ' · ' + relSecs(tx.secsAgo)), ), React.createElement('div', { style: { textAlign: 'right' } }, React.createElement('div', { style: { fontWeight: 700, fontSize: 13.5, color: 'var(--fg-1)' } }, fmtRp(tx.price)), React.createElement('span', { className: 'badge ' + (tx.method === 'QRIS' ? 'badge-info' : 'badge-online'), style: { padding: '1px 7px', fontSize: 10.5 } }, tx.method), ), )), ); } function OverviewView({ machines, onOpenMachine }) { const Y = window.YAPPARI; const online = machines.filter(m => m.status === 'online').length; const attention = machines.filter(m => m.status !== 'online' || (m.temp != null && m.temp > 10) || machineStockSummary(m.id).empty >= 4).length; const all = Y.aggregateAll(); const series = useMemo(() => Y.dailySeries(null), []); return React.createElement('div', { className: 'view' }, React.createElement('div', { className: 'metric-grid' }, React.createElement(MetricCard, { label: 'Total mesin', value: machines.length, icon: 'cube', bg: 'linear-gradient(135deg,#004080,#002E5C)', sub: 'armada Yappari', subIcon: 'layers' }), React.createElement(MetricCard, { label: 'Online', value: online, icon: 'wifi', bg: 'linear-gradient(135deg,#1FA463,#127A47)', sub: 'dari ' + machines.length + ' mesin' }), React.createElement(MetricCard, { label: 'Butuh perhatian', value: attention, icon: 'alert', bg: 'linear-gradient(135deg,#E0820E,#B85C00)', sub: 'isu perlu ditindak' }), React.createElement(MetricCard, { label: 'Omzet hari ini', value: all.totalRev, prefix: 'Rp ', icon: 'wallet', bg: 'linear-gradient(135deg,#F04000,#B02B00)', sub: fmtNum(all.totalTx) + ' transaksi', subIcon: 'cart', trend: 12 }), ), React.createElement('div', { className: 'l-split l-overview' }, React.createElement('div', { className: 'col gap-5' }, React.createElement('div', { className: 'row', style: { justifyContent: 'space-between', alignItems: 'baseline' } }, React.createElement('h2', { style: { fontSize: 'var(--fs-lg)', fontWeight: 800, margin: 0 } }, 'Armada mesin'), React.createElement('span', { className: 'muted', style: { fontSize: 13 } }, machines.length + ' mesin · klik untuk detail'), ), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(280px,1fr))', gap: 'var(--sp-4)' } }, machines.map(m => React.createElement(MachineCard, { key: m.id, m, onOpen: onOpenMachine })), ), ), React.createElement('div', { className: 'col gap-5' }, React.createElement(AttentionPanel, { machines, onOpen: onOpenMachine }), React.createElement(LiveFeed, null), ), ), React.createElement('div', { style: { marginTop: 'var(--sp-5)' } }, React.createElement(SectionCard, { title: 'Omzet harian — semua mesin', sub: '14 hari terakhir', motif: true, right: React.createElement('div', { className: 'row gap-2' }, React.createElement('span', { className: 'badge badge-soft' }, 'Total 14 hari: ' + fmtRpShort(series.reduce((a, d) => a + d.value, 0))), ), bodyStyle: { padding: '8px 24px 24px' }, }, React.createElement(BarChart, { data: series, height: 220, highlightLast: true }), ), ), ); } Object.assign(window, { OverviewView, machineStockSummary, MachineCard, useLiveFeed, LiveFeed });