/* @jsx React.createElement */ const { useState, useEffect, useRef, useMemo } = React; /* ============================================================ Yappari — PENJUALAN (Sales) ============================================================ */ function downloadCSV(filename, rows) { const csv = rows.map(r => r.map(c => { const s = String(c); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }).join(',')).join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } const DATE_PRESETS = [ { id: 'today', label: 'Hari ini', days: 1 }, { id: '7d', label: '7 hari', days: 7 }, { id: '14d', label: '14 hari', days: 14 }, { id: '30d', label: '30 hari', days: 30 }, ]; function SalesView({ selectedMachine, machines }) { const Y = window.YAPPARI; const [range, setRange] = useState('14d'); const [sort, setSort] = useState({ key: 'omzet', dir: 'desc' }); const mid = selectedMachine; const mObj = machines.find(m => m.id === mid); const data = mid ? Y.SALES_BY_MACHINE[mid] : Y.aggregateAll(); const series = useMemo(() => { const full = Y.dailySeries(mid); const days = DATE_PRESETS.find(p => p.id === range).days; return full.slice(Math.max(0, full.length - days)); }, [mid, range]); const prodMap = Object.fromEntries(Y.PRODUCTS.map(p => [p.id, p])); const totalRev = data.totalRev || 1; const byProducts = useMemo(() => Object.entries(data.agg).map(([pid, sold]) => { const p = prodMap[pid]; return { p, sold, omzet: sold * p.price, share: (sold * p.price) / totalRev }; }), [mid]); const sorted = useMemo(() => { const arr = [...byProducts]; const { key, dir } = sort; arr.sort((a, b) => { let av, bv; if (key === 'produk') { av = a.p.name; bv = b.p.name; } else if (key === 'harga') { av = a.p.price; bv = b.p.price; } else if (key === 'terjual') { av = a.sold; bv = b.sold; } else { av = a.omzet; bv = b.omzet; } if (av < bv) return dir === 'asc' ? -1 : 1; if (av > bv) return dir === 'asc' ? 1 : -1; return 0; }); return arr; }, [byProducts, sort]); const avg = data.totalTx ? data.totalRev / data.totalTx : 0; const setSortKey = (key) => setSort(s => s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'desc' }); const Th = (key, label, alignRight) => React.createElement('th', { className: 'sortable', style: alignRight ? { textAlign: 'right' } : null, onClick: () => setSortKey(key) }, React.createElement('span', { className: 'row gap-1', style: { display: 'inline-flex', alignItems: 'center', justifyContent: alignRight ? 'flex-end' : 'flex-start' } }, label, sort.key === key && React.createElement(Icon, { name: sort.dir === 'asc' ? 'chevronUp' : 'chevronDown', size: 13 }))); const exportRows = () => { const rows = [['No', 'Produk', 'Kategori', 'Harga', 'Terjual', 'Omzet', 'Share %']]; sorted.forEach((r, i) => rows.push([i + 1, r.p.name, r.p.cat, r.p.price, r.sold, r.omzet, (r.share * 100).toFixed(1)])); return rows; }; return React.createElement('div', { className: 'view' }, // filter bar React.createElement('div', { className: 'card card-pad', style: { marginBottom: 'var(--sp-5)', display: 'flex', gap: 'var(--sp-4)', alignItems: 'center', flexWrap: 'wrap' } }, React.createElement('div', { className: 'row gap-2', style: { color: 'var(--fg-2)', fontWeight: 700, fontSize: 13 } }, React.createElement(Icon, { name: 'filter', size: 16 }), mObj ? mObj.name : 'Semua mesin'), React.createElement('div', { style: { width: 1, height: 24, background: 'var(--border)' } }), React.createElement('div', { className: 'row gap-2' }, DATE_PRESETS.map(p => React.createElement('button', { key: p.id, className: 'chip ' + (range === p.id ? 'active' : ''), onClick: () => setRange(p.id) }, p.label)), React.createElement('button', { className: 'chip' }, React.createElement(Icon, { name: 'calendar', size: 14 }), 'Custom'), ), React.createElement('div', { style: { flex: 1 } }), React.createElement('button', { className: 'btn btn-ghost', onClick: () => downloadCSV('yappari-penjualan.csv', exportRows()) }, React.createElement(Icon, { name: 'download', size: 16 }), 'CSV'), React.createElement('button', { className: 'btn btn-accent', onClick: () => downloadCSV('yappari-penjualan.xls', exportRows()) }, React.createElement(Icon, { name: 'download', size: 16 }), 'Excel'), ), // metrics React.createElement('div', { className: 'metric-grid' }, React.createElement(MetricCard, { label: 'Total omzet', value: data.totalRev, prefix: 'Rp ', icon: 'wallet', bg: 'linear-gradient(135deg,#F04000,#B02B00)', sub: range === 'today' ? 'hari ini' : 'periode terpilih', trend: 12 }), React.createElement(MetricCard, { label: 'Jumlah transaksi', value: data.totalTx, icon: 'cart', bg: 'linear-gradient(135deg,#004080,#002E5C)', sub: 'transaksi', trend: 8 }), React.createElement(MetricCard, { label: 'Rata-rata / transaksi', value: avg, prefix: 'Rp ', icon: 'dollar', bg: 'linear-gradient(135deg,#0078A8,#005A82)', sub: 'per pembelian' }), React.createElement(MetricCard, { label: 'Produk terjual', value: data.totalTx, icon: 'cube', bg: 'linear-gradient(135deg,#1FA463,#127A47)', sub: 'unit keluar', trend: 5 }), ), // charts React.createElement('div', { className: 'l-split l-sales', style: { marginBottom: 'var(--sp-5)' } }, React.createElement(SectionCard, { title: 'Omzet harian', sub: DATE_PRESETS.find(p => p.id === range).label, motif: true, bodyStyle: { padding: 'var(--sp-5)' } }, React.createElement(LineChart, { data: series, height: 250 }), ), React.createElement(SectionCard, { title: 'Metode pembayaran', sub: 'QRIS vs Cash', bodyStyle: { padding: 'var(--sp-5)' } }, React.createElement(Donut, { centerLabel: Math.round((data.qris / (data.totalTx || 1)) * 100) + '%', centerSub: 'QRIS', segments: [ { label: 'QRIS', value: data.qris, color: 'var(--st-maint)', note: fmtNum(data.qris) + ' tx' }, { label: 'Cash', value: data.cash, color: 'var(--st-online)', note: fmtNum(data.cash) + ' tx' }, ], }), ), ), // by products table React.createElement(SectionCard, { title: 'Penjualan per produk', sub: byProducts.length + ' produk · klik kolom untuk urutkan', bodyStyle: { overflowX: 'auto' } }, React.createElement('table', { className: 'tbl' }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: { width: 40 } }, 'No'), Th('produk', 'Produk'), Th('harga', 'Harga', true), Th('terjual', 'Terjual', true), Th('omzet', 'Omzet', true), React.createElement('th', { style: { width: '24%' } }, 'Share of revenue'), )), React.createElement('tbody', null, sorted.map((r, i) => React.createElement('tr', { key: r.p.id }, React.createElement('td', { className: 'num muted' }, i + 1), React.createElement('td', null, React.createElement('div', { className: 'row gap-3' }, React.createElement(ProductGlyph, { glyph: r.p.glyph, color: r.p.color, size: 34 }), React.createElement('div', null, React.createElement('div', { className: 'strong' }, r.p.name), React.createElement('div', { style: { fontSize: 11.5, color: 'var(--fg-3)' } }, r.p.cat), ))), React.createElement('td', { className: 'num', style: { textAlign: 'right' } }, fmtRp(r.p.price)), React.createElement('td', { className: 'num strong', style: { textAlign: 'right' } }, fmtNum(r.sold)), React.createElement('td', { className: 'num strong', style: { textAlign: 'right' } }, fmtRp(r.omzet)), React.createElement('td', null, React.createElement('div', { className: 'row gap-3' }, React.createElement(ProgressBar, { value: r.share * 100, max: Math.max(...byProducts.map(x => x.share)) * 100, color: r.p.color, className: 'tall' }), React.createElement('span', { className: 'num', style: { fontWeight: 700, color: 'var(--fg-1)', minWidth: 44, textAlign: 'right' } }, (r.share * 100).toFixed(1) + '%'), )), )), ), ), ), ); } Object.assign(window, { SalesView, downloadCSV });