// VMAX Store Manager — Catalogue (single + functional bulk entry, correction/revert) and People. (function () { const DS = window.VMAX365DesignSystem_0f78b2; const { Button, Badge, SegmentedControl } = DS; const { Icon, Cup, Sheet, Overline } = window.VMAXUI; const { Avatar, Field, TextInput, Toggle, EmptyState, pushToast } = window.VMAXW; const { useVMAX } = window.VMAXStore; const { Panel } = window.VMAXMGRUI; const PALETTE = [['#D8B48A', '#A87C4F', '#F3E9DC'], ['#C5DCC0', '#6E9A6A', '#E8F0E4'], ['#F1B89A', '#D07B57', '#FBEAE0'], ['#B59AD0', '#7E5BA6', '#EEE7F4'], ['#C9A982', '#8A6440', '#F0E6D8'], ['#F2B45A', '#D38A1E', '#FCEFD8']]; const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '').slice(0, 8); const splitRow = (line) => line.split(line.includes('\t') ? '\t' : line.includes('|') ? '|' : ',').map((x) => x.trim()); const SAMPLE = { menu: 'Taro Milk Tea\tMilk Tea\t52\t50\nWinter Melon Tea\tTea\t30\t28\nMango Smoothie\tSmoothies\t64\t62\nMatcha Latte\tCoffee\t\t', ingredients: 'Taro paste\tpackets\t8\t3\t2\nWinter melon syrup\tbottles\t5\t2\t2\nMango puree\tpackets\t10\t4\t3\nMatcha powder\t\t\t\t', }; function parseRows(kind, text) { const lines = text.split('\n').map((l) => l.trim()).filter(Boolean); return lines.filter((l, i) => !(i === 0 && /name/i.test(l) && /(price|unit)/i.test(l))).map((line) => { const c = splitRow(line); if (kind === 'menu') { const price = Number(c[2]); const member = c[3] === '' || c[3] == null ? price : Number(c[3]); const ok = !!c[0] && c[2] != null && c[2] !== '' && !isNaN(price); return { raw: line, ok, missing: ok ? null : 'price', obj: { id: slug(c[0]) + Math.floor(Math.random() * 99), name: c[0], cat: c[1] || 'New', series: (c[1] || 'New') + ' Series', price, member: isNaN(member) ? price : member, sizes: true } }; } const ok = !!c[0] && !!c[1]; return { raw: line, ok, missing: !c[1] ? 'unit' : null, obj: { id: slug(c[0]) + Math.floor(Math.random() * 99), name: c[0], kind: 'ingredient', unit: c[1] || '', buy: 'box', packSize: '—', back: Number(c[2]) || 0, front: Number(c[3]) || 0, warn: Number(c[4]) || 2, expiry: null } }; }); } // ============ Catalogue ============ function Catalogue({ mobile, who }) { const [s, a] = useVMAX(); const [kind, setKind] = React.useState('menu'); const [text, setText] = React.useState(''); const [rows, setRows] = React.useState(null); React.useEffect(() => { setRows(null); setText(''); }, [kind]); const valid = rows ? rows.filter((r) => r.ok) : []; const flagged = rows ? rows.filter((r) => !r.ok) : []; const commit = () => { rows.forEach((r) => { if (r.ok) PALETTE; }); const objs = valid.map((r, i) => kind === 'menu' ? { ...r.obj, ...{ c1: PALETTE[i % PALETTE.length][0], c2: PALETTE[i % PALETTE.length][1], tint: PALETTE[i % PALETTE.length][2] } } : r.obj); a.bulkCommit(kind, objs, who); pushToast(valid.length + ' ' + kind + ' added' + (flagged.length ? ' · ' + flagged.length + ' skipped' : '')); setRows(null); setText(''); }; const changes = s.ledger.filter((e) => e.type === 'price' && !e.revertOf).slice(0, 4); return (
First-class}>

Paste a table (tab, comma or | separated). It parses and validates before anything commits.

{kind === 'menu' ? 'name · category · price · members' : 'name · unit · back · front · par'}