/* @jsx React.createElement */ const { useState, useEffect, useRef, useMemo } = React; /* ============================================================ Yappari — PLANOGRAM (physical machine view) ============================================================ */ function Coil({ spinning, tint }) { // horizontal helix — ~8 loops const loops = 8, W = 96, H = 26; const seg = W / loops; let d = `M2 ${H / 2}`; for (let i = 0; i < loops; i++) { const x = 2 + i * seg; d += ` C ${x + seg * 0.25} 2, ${x + seg * 0.75} 2, ${x + seg} ${H / 2}`; d += ` C ${x + seg * 0.75} ${H - 2}, ${x + seg * 0.25} ${H - 2}, ${x} ${H / 2}`; } return React.createElement('svg', { viewBox: `0 0 ${W + 4} ${H}`, width: '100%', height: H, style: { transformOrigin: 'center', animation: spinning ? 'coilspin .6s linear 2' : 'none', opacity: 0.85 }, }, React.createElement('path', { d, fill: 'none', stroke: tint || 'rgba(180,195,210,0.7)', strokeWidth: 2.4, strokeLinecap: 'round' }), ); } function PlanogramView({ selectedMachine, machines }) { const Y = window.YAPPARI; const mid = selectedMachine || machines[0].id; const mObj = machines.find(m => m.id === mid); const slots = Y.SLOTS[mid]; const prodMap = Object.fromEntries(Y.PRODUCTS.map(p => [p.id, p])); const [sel, setSel] = useState(slots.filter(s => !s.hidden)[0].no); const [dispensing, setDispensing] = useState(null); const [toast, setToast] = useState(null); const layout = Y.LAYOUTS[mid]; const visible = slots.filter(s => !s.hidden); useEffect(() => { setSel(slots.filter(s => !s.hidden)[0].no); }, [mid]); const selSlot = slots.find(s => s.no === sel) || visible[0]; const selProd = selSlot && prodMap[selSlot.productId]; const testDispense = (no) => { setDispensing(no); setToast({ type: 'ok', msg: 'Test dispense slot ' + no + ' — produk dikeluarkan (gratis)' }); setTimeout(() => setDispensing(null), 1300); setTimeout(() => setToast(null), 3400); }; // group visible slots by row (merged-away coils skipped; primary spans over them) const rows = []; for (let r = 0; r < layout.rows; r++) rows.push(slots.filter(s => s.row === r && !s.hidden)); return React.createElement('div', { className: 'view' }, React.createElement('div', { className: 'l-split l-plano' }, // ---- CABINET ---- React.createElement('div', null, React.createElement('div', { style: { maxWidth: 780, margin: '0 auto' } }, React.createElement('div', { style: { background: 'linear-gradient(160deg,#0a3158,#04223f)', borderRadius: 20, padding: '18px 18px 22px', boxShadow: '0 24px 48px -12px rgba(0,20,38,.55), inset 0 1px 0 rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.08)', } }, // header bar React.createElement('div', { className: 'row', style: { justifyContent: 'space-between', alignItems: 'center', padding: '0 6px 14px' } }, React.createElement('div', { className: 'row gap-2' }, React.createElement('div', { style: { width: 30, height: 30, borderRadius: '50%', background: '#fff', display: 'grid', placeItems: 'center' } }, React.createElement(PlumBlossom, { size: 20, color: '#004080' })), React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontSize: 22, color: '#fff', letterSpacing: '.04em' } }, 'YAPPARI!'), React.createElement('span', { style: { fontSize: 11, color: 'rgba(255,255,255,.6)', fontWeight: 700, letterSpacing: '.14em' } }, mObj.name.toUpperCase()), ), React.createElement(StatusBadge, { status: mObj.status }), ), // glass cabinet React.createElement('div', { style: { background: 'linear-gradient(135deg, rgba(220,235,250,.10), rgba(160,200,235,.05) 40%, rgba(255,255,255,.02))', borderRadius: 12, padding: 10, border: '1px solid rgba(255,255,255,.14)', position: 'relative', overflow: 'hidden', } }, // glass reflection streak React.createElement('div', { style: { position: 'absolute', top: 0, left: '12%', width: 60, height: '100%', background: 'linear-gradient(105deg, transparent, rgba(255,255,255,.10), transparent)', transform: 'skewX(-12deg)', pointerEvents: 'none' } }), rows.map((row, ri) => React.createElement('div', { key: ri, style: { position: 'relative', marginBottom: ri < rows.length - 1 ? 10 : 0 } }, React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(' + layout.cols + ',1fr)', gap: 5 } }, row.map(s => { const p = prodMap[s.productId]; const col = stockColor(s.cur, s.cap); const empty = s.cur === 0; const isSel = s.no === sel; return React.createElement('button', { key: s.no, onClick: () => setSel(s.no), title: 'Slot ' + s.no + ' · ' + p.name + ' · stok ' + s.cur + (s.span > 1 ? ' · gabungan ×' + s.span : ''), style: { gridColumn: (s.col + 1) + ' / span ' + s.span, position: 'relative', border: isSel ? '2px solid var(--accent)' : '1px solid rgba(255,255,255,.10)', background: 'linear-gradient(180deg, rgba(255,255,255,.06), rgba(0,0,0,.18))', borderRadius: 6, padding: '6px 3px 5px', cursor: 'pointer', overflow: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3, minHeight: 78, boxShadow: isSel ? '0 0 0 3px rgba(240,64,0,.25)' : 'inset 0 -6px 10px -6px rgba(0,0,0,.5)', transition: 'all var(--dur-fast)', }, }, React.createElement('span', { style: { position: 'absolute', top: 3, left: 4, fontSize: 9.5, fontWeight: 800, color: 'rgba(255,255,255,.65)', fontFamily: 'var(--font-mono)' } }, s.no), s.span > 1 && React.createElement('span', { style: { position: 'absolute', top: 3, right: 13, fontSize: 8, fontWeight: 800, color: '#fff', background: 'var(--accent)', borderRadius: 3, padding: '0 4px' } }, '⇆×' + s.span), React.createElement('span', { style: { position: 'absolute', top: 4, right: 4, width: 6, height: 6, borderRadius: '50%', background: col, boxShadow: '0 0 5px ' + col } }), // product sitting in slot React.createElement('div', { style: { marginTop: 8, opacity: empty ? 0.18 : 1, filter: empty ? 'grayscale(1)' : 'none', position: 'relative', zIndex: 2 } }, React.createElement(ProductGlyph, { glyph: p.glyph, color: p.color, size: 34, radius: 5 })), // dispensing drop animation dispensing === s.no && React.createElement('div', { style: { position: 'absolute', top: 8, zIndex: 5, animation: 'drop 1.2s var(--ease-out) forwards' } }, React.createElement(ProductGlyph, { glyph: p.glyph, color: p.color, size: 34, radius: 5 })), // coil(s) — one per merged column React.createElement('div', { style: { width: '100%', marginTop: 'auto', display: 'flex', gap: 2 } }, Array.from({ length: s.span }).map((_, k) => React.createElement('div', { key: k, style: { flex: 1 } }, React.createElement(Coil, { spinning: dispensing === s.no, tint: empty ? 'rgba(208,48,32,.5)' : null })))), empty && React.createElement('span', { style: { position: 'absolute', bottom: 20, fontSize: 8, fontWeight: 800, color: 'var(--st-error)', letterSpacing: '.05em' } }, 'KOSONG'), ); }), ), // shelf lip React.createElement('div', { style: { height: 4, marginTop: 3, borderRadius: 2, background: 'linear-gradient(180deg,#9fb4c8,#5a7186)', boxShadow: '0 2px 4px rgba(0,0,0,.4)' } }), )), ), // delivery tray React.createElement('div', { style: { marginTop: 12, height: 38, borderRadius: 8, background: 'linear-gradient(180deg,rgba(0,0,0,.35),rgba(0,0,0,.15))', border: '1px solid rgba(255,255,255,.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(255,255,255,.5)', fontSize: 11, fontWeight: 700, letterSpacing: '.14em' } }, 'AMBIL PRODUK DI SINI'), ), // legend React.createElement('div', { className: 'row gap-4', style: { justifyContent: 'center', marginTop: 14, fontSize: 12, color: 'var(--fg-3)', fontWeight: 600 } }, React.createElement('span', { className: 'row gap-1' }, React.createElement('i', { style: { width: 9, height: 9, borderRadius: '50%', background: 'var(--st-online)', display: 'inline-block' } }), 'Penuh'), React.createElement('span', { className: 'row gap-1' }, React.createElement('i', { style: { width: 9, height: 9, borderRadius: '50%', background: 'var(--st-warn)', display: 'inline-block' } }), 'Rendah'), React.createElement('span', { className: 'row gap-1' }, React.createElement('i', { style: { width: 9, height: 9, borderRadius: '50%', background: 'var(--st-error)', display: 'inline-block' } }), 'Habis'), ), ), ), // ---- DETAIL PANEL ---- React.createElement('div', { style: { position: 'sticky', top: 0 } }, React.createElement(SectionCard, { title: 'Detail slot ' + sel, sub: mObj.name, motif: true, bodyStyle: { padding: 'var(--sp-5)' } }, selProd && React.createElement('div', { className: 'col gap-4' }, React.createElement('div', { className: 'row gap-4', style: { alignItems: 'center' } }, React.createElement(ProductGlyph, { glyph: selProd.glyph, color: selProd.color, size: 72, radius: 12 }), React.createElement('div', null, React.createElement('div', { style: { fontSize: 18, fontWeight: 800, color: 'var(--fg-1)', lineHeight: 1.2 } }, selProd.name), React.createElement('span', { className: 'badge badge-soft', style: { marginTop: 6 } }, selProd.cat), ), ), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } }, React.createElement('div', { className: 'card', style: { padding: 12, boxShadow: 'none', background: 'var(--bg-sunken)' } }, React.createElement('div', { className: 'field-label', style: { marginBottom: 2 } }, 'Harga'), React.createElement('div', { style: { fontSize: 18, fontWeight: 800 } }, fmtRp(selProd.price))), React.createElement('div', { className: 'card', style: { padding: 12, boxShadow: 'none', background: 'var(--bg-sunken)' } }, React.createElement('div', { className: 'field-label', style: { marginBottom: 2 } }, 'No. slot'), React.createElement('div', { style: { fontSize: 18, fontWeight: 800, fontFamily: 'var(--font-mono)' } }, selSlot.no)), ), selSlot.span > 1 && React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: '8px 12px', borderRadius: 'var(--r-2)', background: 'var(--st-warn-bg)', color: 'var(--st-warn)', fontSize: 12.5, fontWeight: 600 } }, React.createElement(Icon, { name: 'layers', size: 15 }), 'Slot gabungan ×' + selSlot.span + ' — dipanggil sebagai slot ' + selSlot.no), React.createElement('div', null, React.createElement('div', { className: 'row', style: { justifyContent: 'space-between', marginBottom: 7 } }, React.createElement('span', { className: 'field-label', style: { margin: 0 } }, 'Stok saat ini'), React.createElement('span', { style: { fontWeight: 800, color: stockColor(selSlot.cur, selSlot.cap) } }, selSlot.cur + ' / ' + selSlot.cap)), React.createElement(ProgressBar, { value: selSlot.cur, max: selSlot.cap, color: stockColor(selSlot.cur, selSlot.cap), className: 'tall' }), React.createElement('div', { style: { fontSize: 12, color: 'var(--fg-3)', marginTop: 8 } }, 'Isi ulang terakhir: ' + (selSlot.lastRefill === 0 ? 'hari ini' : selSlot.lastRefill + ' hari lalu')), ), React.createElement('div', { className: 'row gap-3', style: { marginTop: 4 } }, React.createElement('button', { className: 'btn btn-ghost', style: { flex: 1, justifyContent: 'center' } }, React.createElement(Icon, { name: 'pencil', size: 16 }), 'Edit'), React.createElement('button', { className: 'btn btn-accent', style: { flex: 1, justifyContent: 'center' }, disabled: selSlot.cur === 0, onClick: () => testDispense(sel) }, React.createElement(Icon, { name: 'play', size: 15 }), 'Test dispense'), ), ), ), ), ), toast && React.createElement('div', { style: { position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)', zIndex: 300, background: 'var(--st-online)', color: '#fff', padding: '12px 20px', borderRadius: 10, fontWeight: 700, fontSize: 14, boxShadow: 'var(--shadow-3)', display: 'flex', alignItems: 'center', gap: 8, animation: 'viewIn .3s' } }, React.createElement(Icon, { name: 'check2', size: 18 }), toast.msg), ); } Object.assign(window, { PlanogramView, Coil });