// 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'}
every edit is revertible}>
{changes.length === 0 ? No catalogue changes yet. Edits land here with a revert option.
: (
{changes.map((e) => (
{e.text}
{e.at} · {e.by}{e.reverted ? ' · reverted' : ''}
{!e.reverted &&
}
))}
)}
);
}
function MiniCard({ icon, title, sub }) {
return ;
}
// ============ People ============
const ALL_ROLES = ['cashier', 'maker', 'manager'];
function People({ mobile, who }) {
const [s, a] = useVMAX();
const [sortBy, setSortBy] = React.useState('role');
const [add, setAdd] = React.useState(false);
const [detail, setDetail] = React.useState(null);
const order = { owner: 0, manager: 1, cashier: 2, maker: 3 };
let people = [...s.people];
people.sort((x, y) => sortBy === 'name' ? x.name.localeCompare(y.name) : (order[x.roles[0]] - order[y.roles[0]]));
const statusOf = (p) => p.resetPending ? { t: 'reset pending', tone: 'amber' } : p.pending ? { t: 'PIN pending', tone: 'amber' } : { t: 'active', tone: 'mint' };
return (
}>
{s.people.length <= 1 && setAdd(true)}>+ Add person} />}
{s.people.length > 1 && (
{people.map((p) => { const st = statusOf(p); return (
); })}
)}
{add && setAdd(false)} />}
{detail && setDetail(null)} />}
);
}
function AddPerson({ onClose, who }) {
const [, a] = useVMAX();
const [name, setName] = React.useState('');
const [roles, setRoles] = React.useState({ cashier: true, maker: true, manager: false });
const [pinMode, setPinMode] = React.useState('first'); // 'first' | 'now'
const [pin, setPin] = React.useState('');
const chosen = ALL_ROLES.filter((r) => roles[r]);
const valid = name.trim() && chosen.length && (pinMode === 'first' || pin.length === 4);
return (
Add a person
{ALL_ROLES.map((r) => )}
{pinMode === 'now' && setPin(v.replace(/\D/g, '').slice(0, 4))} placeholder="4-digit starter PIN" />}
{pinMode === 'first' && Account is issued; they choose their own PIN when they first open the app.
}
);
}
function PersonDetail({ person, onClose, who }) {
const [, a] = useVMAX();
const [confirm, setConfirm] = React.useState(false);
const [roles, setRoles] = React.useState(ALL_ROLES.reduce((o, r) => ({ ...o, [r]: person.roles.includes(r) }), {}));
const chosen = ALL_ROLES.filter((r) => roles[r]);
return (
{person.name}
{person.root ? 'Owner · the root account' : person.pending ? 'PIN set on first sign-in' : person.resetPending ? 'PIN reset requested' : 'Active'}
{!person.root &&
{ALL_ROLES.map((r) => )}
}
{!person.root && chosen.join() !== person.roles.join() &&
}
{!person.resetPending &&
}
{!person.root && (confirm ? (
Remove access? History stays.
) :
)}
{person.root &&
The owner is the root account — it can't be removed.
}
);
}
const rowBtn = { width: '100%', display: 'flex', alignItems: 'center', gap: 11, padding: '12px 13px', border: '1.5px solid var(--vmax-line)', background: 'var(--surface-card)', borderRadius: 'var(--radius-md)', cursor: 'pointer', fontFamily: 'var(--font-body)', fontWeight: 600, fontSize: 14, color: 'var(--vmax-ink)' };
window.VMAXMGR3 = { Catalogue, People };
})();