// ============================================================ // Dashboard + Products screens (live / API-backed) // ============================================================ (function () { const { Icon, Badge, BrandDot, Btn, Field, Modal, KPI, fmtNum } = window; const { useState, useMemo } = React; // ---------------- DASHBOARD ---------------- function Dashboard({ products, history, stats, printer, user, go, addToQueue }) { stats = stats || {}; const labelsToday = stats.today || 0; const batchesToday = stats.todayBatch || 0; const labelsTotal = stats.allTime || 0; const week = (stats.week && stats.week.length === 7) ? stats.week : [0, 0, 0, 0, 0, 0, labelsToday]; const wkMax = Math.max(1, ...week); const days = ['', '', '', '', '', '', 'Today']; const dayNames = lastSevenDayNames(); const topProducts = (stats.top || []).map(t => ({ ...t, printed: +t.printed })); const luxN = stats.lux != null ? stats.lux : products.filter(p => p.brand === 'LUXURIATE').length; const vmvN = stats.vmv != null ? stats.vmv : products.filter(p => p.brand === 'VITMINVEDA').length; const totalP = stats.prodCount || products.length || 1; const luxPct = Math.round(luxN / Math.max(1, luxN + vmvN) * 100); return React.createElement('div', { className: 'screen' }, React.createElement('div', { className: 'screen-head' }, React.createElement('div', null, React.createElement('h1', null, 'Good ', greeting(), ', ', user.name.split(' ')[0]), React.createElement('p', { className: 'screen-sub' }, todayStr(), ' · ', fmtNum(totalP), ' SKUs across 2 brands') ), React.createElement(Btn, { variant: 'primary', icon: 'print', onClick: () => go('print') }, 'New Print Job') ), React.createElement('div', { className: 'kpi-row' }, React.createElement(KPI, { label: 'Labels printed today', value: fmtNum(labelsToday), icon: 'tag', tone: 'amber' }), React.createElement(KPI, { label: 'Print batches today', value: batchesToday, icon: 'layers', tone: 'green', sub: fmtNum(labelsTotal) + ' all-time' }), React.createElement(KPI, { label: 'Active products', value: fmtNum(totalP), icon: 'box', tone: 'blue', sub: luxN + ' LUX · ' + vmvN + ' VMV' }), React.createElement('div', { className: 'kpi kpi-printer' }, React.createElement('div', { className: 'kpi-top' }, React.createElement('div', { className: 'kpi-ico kpi-ico-' + (printer.connected ? 'green' : 'amber') }, React.createElement(Icon, { name: 'print', size: 18, stroke: 1.8 })), React.createElement('span', { className: 'pulse-dot ' + (printer.connected ? 'live' : 'off') }) ), React.createElement('div', { className: 'kpi-val', style: { fontSize: 22 } }, printer.connected ? 'Ready' : 'Check'), React.createElement('div', { className: 'kpi-label' }, printer.model + ' · ' + (printer.mode === 'usb' ? 'USB' : 'TSPL/TCP')), React.createElement('div', { className: 'kpi-sub mono' }, printer.mode === 'usb' ? printer.share : printer.ip + ':' + printer.port) ) ), React.createElement('div', { className: 'dash-grid' }, React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'panel-head' }, React.createElement('h3', null, 'Print volume'), React.createElement('span', { className: 'panel-meta' }, 'Last 7 days') ), React.createElement('div', { className: 'chart' }, week.map((v, i) => React.createElement('div', { key: i, className: 'chart-col' }, React.createElement('div', { className: 'chart-bar-wrap' }, React.createElement('div', { className: 'chart-bar' + (i === 6 ? ' chart-bar-now' : ''), style: { height: (v / wkMax * 100) + '%' } }, v > 0 && React.createElement('span', { className: 'chart-val' }, v) ) ), React.createElement('span', { className: 'chart-x' }, dayNames[i]) )) ) ), React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'panel-head' }, React.createElement('h3', null, 'Brand split')), React.createElement('div', { className: 'donut-row' }, React.createElement('div', { className: 'donut', style: { background: `conic-gradient(var(--lux) 0 ${luxPct}%, var(--vmv) ${luxPct}% 100%)` } }, React.createElement('div', { className: 'donut-hole' }, React.createElement('b', null, luxN + vmvN), React.createElement('span', null, 'SKUs') ) ), React.createElement('div', { className: 'donut-legend' }, React.createElement('div', { className: 'leg' }, React.createElement(BrandDot, { brand: 'LUXURIATE', size: 10 }), React.createElement('span', null, 'LUXURIATE'), React.createElement('b', null, luxN)), React.createElement('div', { className: 'leg' }, React.createElement(BrandDot, { brand: 'VITMINVEDA', size: 10 }), React.createElement('span', null, 'VITMINVEDA'), React.createElement('b', null, vmvN)) ) ) ), React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'panel-head' }, React.createElement('h3', null, 'Recent batches'), React.createElement('button', { className: 'link-btn', onClick: () => go('history') }, 'View all', React.createElement(Icon, { name: 'chevronR', size: 14 })) ), history.length === 0 ? React.createElement('div', { className: 'mini-empty' }, 'No print jobs yet — head to Print Studio') : React.createElement('div', { className: 'mini-list' }, history.slice(0, 5).map(h => React.createElement('div', { key: h.id, className: 'mini-row' }, React.createElement(BrandDot, { brand: h.brand }), React.createElement('div', { className: 'mini-main' }, React.createElement('span', { className: 'mini-title' }, h.title), React.createElement('span', { className: 'mini-meta mono' }, h.sku, ' · B.No ', h.batch) ), React.createElement('span', { className: 'mini-qty' }, '×', h.qty), React.createElement('span', { className: 'mini-time' }, h.time) )) ) ), React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'panel-head' }, React.createElement('h3', null, 'Top products'), React.createElement('span', { className: 'panel-meta' }, 'By labels printed') ), topProducts.length === 0 ? React.createElement('div', { className: 'mini-empty' }, 'Print some labels to see your top movers') : React.createElement('div', { className: 'mini-list' }, topProducts.map((p, i) => React.createElement('div', { key: p.sku, className: 'mini-row top-row', onClick: () => addToQueue(productFor(products, p)) }, React.createElement('span', { className: 'rank' }, i + 1), React.createElement('div', { className: 'mini-main' }, React.createElement('span', { className: 'mini-title' }, (p.title || '').replace(p.brand, '').trim()), React.createElement('span', { className: 'mini-meta mono' }, p.barcode) ), React.createElement('div', { className: 'bar-track' }, React.createElement('div', { className: 'bar-fill', style: { width: (p.printed / topProducts[0].printed * 100) + '%', background: p.brand === 'LUXURIATE' ? 'var(--lux)' : 'var(--vmv)' } })), React.createElement('span', { className: 'mini-qty' }, fmtNum(p.printed)) )) ) ) ) ); } function productFor(products, p) { return products.find(x => x.sku === p.sku) || { ...p, netwt: p.netwt || '', mrp: p.mrp || '', batch: p.batch || '34011542', mfg: p.mfg || '', exp: p.exp || '' }; } // ---------------- PRODUCTS ---------------- const CATS = ['Aloe Vera','Aroma Oil','Bathing Salt','Body Wash','Face Wash','Soap','Oil','Cream','Serum','Shampoo','Scrub','Other']; function Products({ products, onSaveProduct, onDeleteProduct, addToQueue, go, toast, user }) { const [q, setQ] = useState(''); const [brand, setBrand] = useState('All'); const [cat, setCat] = useState('All'); const [page, setPage] = useState(0); const [editing, setEditing] = useState(null); const [del, setDel] = useState(null); const [busy, setBusy] = useState(false); const perPage = 11; const filtered = useMemo(() => products.filter(p => (brand === 'All' || p.brand === brand) && (cat === 'All' || p.category === cat) && (!q || (p.title + p.sku + p.barcode).toLowerCase().includes(q.toLowerCase())) ), [products, q, brand, cat]); const pages = Math.max(1, Math.ceil(filtered.length / perPage)); const pg = Math.min(page, pages - 1); const rows = filtered.slice(pg * perPage, pg * perPage + perPage); React.useEffect(() => setPage(0), [q, brand, cat]); const save = async (prod) => { setBusy(true); try { await onSaveProduct(prod); setEditing(null); } catch (e) { toast(e.message, 'err'); } finally { setBusy(false); } }; const doDelete = async () => { try { await onDeleteProduct(del); toast('Deleted ' + del.sku, 'err'); } catch (e) { toast(e.message, 'err'); } setDel(null); }; return React.createElement('div', { className: 'screen' }, React.createElement('div', { className: 'screen-head' }, React.createElement('div', null, React.createElement('h1', null, 'Products'), React.createElement('p', { className: 'screen-sub' }, fmtNum(filtered.length), ' of ', fmtNum(products.length), ' SKUs') ), user.role === 'admin' && React.createElement(Btn, { variant: 'primary', icon: 'plus', onClick: () => setEditing(blankProduct()) }, 'Add Product') ), React.createElement('div', { className: 'toolbar' }, React.createElement('div', { className: 'search' }, React.createElement(Icon, { name: 'search', size: 17, color: 'var(--txt-mut)' }), React.createElement('input', { value: q, onChange: e => setQ(e.target.value), placeholder: 'Search title, SKU or barcode…' }), q && React.createElement('button', { className: 'icon-btn-sm', onClick: () => setQ('') }, React.createElement(Icon, { name: 'x', size: 14 })) ), React.createElement('div', { className: 'seg' }, ['All', 'LUXURIATE', 'VITMINVEDA'].map(b => React.createElement('button', { key: b, className: 'seg-btn' + (brand === b ? ' on' : ''), onClick: () => setBrand(b) }, b !== 'All' && React.createElement(BrandDot, { brand: b }), b === 'All' ? 'All' : b === 'LUXURIATE' ? 'LUX' : 'VMV')) ), React.createElement('select', { className: 'select', value: cat, onChange: e => setCat(e.target.value) }, React.createElement('option', { value: 'All' }, 'All categories'), CATS.map(c => React.createElement('option', { key: c, value: c }, c)) ) ), React.createElement('div', { className: 'panel table-panel' }, React.createElement('div', { className: 'table' }, React.createElement('div', { className: 'tr th' }, React.createElement('span', { className: 'c-brand' }, ''), React.createElement('span', { className: 'c-sku' }, 'SKU'), React.createElement('span', { className: 'c-title' }, 'Product'), React.createElement('span', { className: 'c-bc' }, 'Barcode'), React.createElement('span', { className: 'c-wt' }, 'Net Wt'), React.createElement('span', { className: 'c-mrp' }, 'MRP'), React.createElement('span', { className: 'c-dates' }, 'MFG / EXP'), React.createElement('span', { className: 'c-act' }, '') ), rows.length === 0 && React.createElement('div', { className: 'empty' }, React.createElement(Icon, { name: 'search', size: 28, color: 'var(--txt-mut)' }), React.createElement('p', null, 'No products match your filters')), rows.map(p => React.createElement('div', { key: p.sku, className: 'tr' }, React.createElement('span', { className: 'c-brand' }, React.createElement(BrandDot, { brand: p.brand, size: 9 })), React.createElement('span', { className: 'c-sku mono' }, p.sku), React.createElement('span', { className: 'c-title' }, React.createElement('b', null, p.title.replace(p.brand, '').trim()), React.createElement('em', null, p.brand, ' · ', p.category) ), React.createElement('span', { className: 'c-bc' }, React.createElement(window.BarcodeSVG, { code: p.barcode, height: 30, moduleW: 0.95, showText: false, color: 'var(--txt)' }), React.createElement('i', { className: 'mono bc-num' }, p.barcode)), React.createElement('span', { className: 'c-wt' }, p.netwt || '—'), React.createElement('span', { className: 'c-mrp' }, p.mrp), React.createElement('span', { className: 'c-dates mono' }, p.mfg, ' → ', p.exp), React.createElement('span', { className: 'c-act' }, React.createElement('button', { className: 'icon-btn', title: 'Print', onClick: () => { addToQueue(p); go('print'); } }, React.createElement(Icon, { name: 'print', size: 16 })), user.role === 'admin' && React.createElement('button', { className: 'icon-btn', title: 'Edit', onClick: () => setEditing({ ...p, __orig: p.sku }) }, React.createElement(Icon, { name: 'edit', size: 15 })), user.role === 'admin' && React.createElement('button', { className: 'icon-btn danger', title: 'Delete', onClick: () => setDel(p) }, React.createElement(Icon, { name: 'trash', size: 15 })) ) )) ), pages > 1 && React.createElement('div', { className: 'pager' }, React.createElement('span', { className: 'pager-info' }, 'Page ', pg + 1, ' of ', pages), React.createElement('div', { className: 'pager-btns' }, React.createElement('button', { className: 'icon-btn', disabled: pg === 0, onClick: () => setPage(pg - 1) }, React.createElement(Icon, { name: 'chevronL', size: 16 })), React.createElement('button', { className: 'icon-btn', disabled: pg >= pages - 1, onClick: () => setPage(pg + 1) }, React.createElement(Icon, { name: 'chevronR', size: 16 })) ) ) ), editing && React.createElement(ProductEditor, { product: editing, busy, onSave: save, onClose: () => setEditing(null) }), React.createElement(Modal, { open: !!del, onClose: () => setDel(null), title: 'Delete product?', width: 420, footer: React.createElement(React.Fragment, null, React.createElement(Btn, { onClick: () => setDel(null) }, 'Cancel'), React.createElement(Btn, { variant: 'danger', icon: 'trash', onClick: doDelete }, 'Delete') ) }, del && React.createElement('p', { className: 'confirm-txt' }, 'Remove ', React.createElement('b', null, del.title), ' (', React.createElement('span', { className: 'mono' }, del.sku), ') from the database?')) ); } function ProductEditor({ product, onSave, onClose, busy }) { const [p, setP] = useState(product); const set = (k, v) => setP(o => ({ ...o, [k]: v })); const isNew = product.__new; return React.createElement(Modal, { open: true, onClose, width: 720, title: isNew ? 'Add product' : 'Edit · ' + product.sku, footer: React.createElement(React.Fragment, null, React.createElement(Btn, { onClick: onClose }, 'Cancel'), React.createElement(Btn, { variant: 'primary', icon: 'check', disabled: busy, onClick: () => onSave(p) }, busy ? 'Saving…' : (isNew ? 'Add product' : 'Save changes')) ) }, React.createElement('div', { className: 'editor-grid' }, React.createElement('div', { className: 'editor-fields' }, React.createElement('div', { className: 'frow' }, React.createElement(Field, { label: 'Brand' }, React.createElement('select', { className: 'select', value: p.brand, onChange: e => set('brand', e.target.value) }, React.createElement('option', null, 'LUXURIATE'), React.createElement('option', null, 'VITMINVEDA'))), React.createElement(Field, { label: 'Category' }, React.createElement('select', { className: 'select', value: p.category, onChange: e => set('category', e.target.value) }, CATS.map(c => React.createElement('option', { key: c }, c)))) ), React.createElement(Field, { label: 'Product title' }, React.createElement('input', { className: 'input', value: p.title, onChange: e => set('title', e.target.value) })), React.createElement('div', { className: 'frow' }, React.createElement(Field, { label: 'SKU' }, React.createElement('input', { className: 'input mono', value: p.sku, onChange: e => set('sku', e.target.value) })), React.createElement(Field, { label: 'Barcode (EAN-13)' }, React.createElement('input', { className: 'input mono', value: p.barcode, maxLength: 13, onChange: e => set('barcode', e.target.value.replace(/\D/g,'')) })) ), React.createElement('div', { className: 'frow' }, React.createElement(Field, { label: 'Net Wt.' }, React.createElement('input', { className: 'input', value: p.netwt, onChange: e => set('netwt', e.target.value) })), React.createElement(Field, { label: 'MRP' }, React.createElement('input', { className: 'input', value: p.mrp, onChange: e => set('mrp', e.target.value) })) ), React.createElement('div', { className: 'frow' }, React.createElement(Field, { label: 'Batch No.' }, React.createElement('input', { className: 'input mono', value: p.batch, onChange: e => set('batch', e.target.value) })), React.createElement(Field, { label: 'MFG (MM/YYYY)' }, React.createElement('input', { className: 'input mono', value: p.mfg, onChange: e => set('mfg', e.target.value) })), React.createElement(Field, { label: 'EXP (MM/YYYY)' }, React.createElement('input', { className: 'input mono', value: p.exp, onChange: e => set('exp', e.target.value) })) ) ), React.createElement('div', { className: 'editor-preview' }, React.createElement('span', { className: 'prev-label' }, 'Live label preview'), React.createElement('div', { className: 'label-stage sm' }, React.createElement(window.ThermalLabel, { product: p, scale: 8 })), React.createElement('span', { className: 'prev-note' }, '102 × 152 mm · 203 dpi') ) ) ); } function blankProduct() { return { __new: true, brand: 'LUXURIATE', category: 'Other', title: '', sku: '', barcode: '', netwt: '', mrp: '₹', batch: '34011542', mfg: '05/2026', exp: '04/2028' }; } function greeting() { const h = new Date().getHours(); return h < 12 ? 'morning' : h < 17 ? 'afternoon' : 'evening'; } function todayStr() { return new Date().toLocaleDateString('en-IN', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } function lastSevenDayNames() { const names = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const out = []; for (let i = 6; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); out.push(i === 0 ? 'Today' : names[d.getDay()]); } return out; } Object.assign(window, { Dashboard, Products }); })();