// VMAX POS — the order screen: menu grid, ticket, variant (hot/cold + size) sheet,
// member attach + recall view + new-member enrol, and the low-stock nudge.
(function () {
const DS = window.VMAX365DesignSystem_0f78b2;
const { MenuItemCard, CategoryChip, TempSelector, SizeSelector, TempTag, SegmentedControl, QtyStepper, Button, Badge } = DS;
const { Icon, Cup, Sheet, Overline } = window.VMAXUI;
const { Avatar, EmptyState, TextInput, Field, pushToast } = window.VMAXW;
const { K, categories, sizes } = window.VMAXP;
const { useVMAX, lowStock } = window.VMAXStore;
const { PaymentSheet, OrdersSheet } = window.VMAXPAY;
const { RequestStockSheet } = window.VMAXEXTRA;
const sizeLabel = (s) => ({ reg: 'Regular', lg: 'Large', jumbo: 'Jumbo' }[s] || '');
const hasVariants = (m) => m.hotcold || m.sizes;
// ---------- Variant sheet ----------
function VariantSheet({ item, onClose, onAdd }) {
const [temp, setTemp] = React.useState('cold');
const [size, setSize] = React.useState('reg');
React.useEffect(() => { if (item) { setTemp(item.defaultTemp || 'cold'); setSize('reg'); } }, [item]);
if (!item) return null;
const up = (sizes.find((s) => s.value === size) || {}).upcharge || 0;
const price = item.price + up, member = item.member + up;
return (
{item.hotcold && Served}
{item.sizes && Size}
{K(price)}
);
}
// ---------- Member: search → recall view → attach; or enrol new ----------
function MemberSheet({ open, onClose }) {
const [s, a] = useVMAX();
const [q, setQ] = React.useState('');
const [view, setView] = React.useState(null); // a member object (recall view)
const [enrol, setEnrol] = React.useState(false);
const [form, setForm] = React.useState({ name: '', phone: '' });
React.useEffect(() => { if (open) { setQ(''); setView(null); setEnrol(false); setForm({ name: '', phone: '' }); } }, [open]);
const list = s.members.filter((m) => (m.name + m.phone).toLowerCase().includes(q.toLowerCase()));
if (view) {
const fav = [...view.history].sort((x, y) => y.times - x.times);
return (
{view.name}
{view.phone} · member since {view.since}
Member
Recognition only — ring the order at the counter as usual.
{fav.length ? 'Their usuals' : 'No history yet'}
{fav.length ? (
{fav.map((h, i) => (
{i === 0 &&
}
{h.name}{h.temp && {h.temp}}
{h.zh &&
{h.zh}
}
×{h.times}
))}
) :
Just joined — nothing here yet. Their next orders will start building their usuals.
}
);
}
if (enrol) {
return (
New member
The customer scans this and enters their own details on their phone. For the demo, enter them here:
setForm({ ...form, name: v })} placeholder="Full name" autoFocus />
setForm({ ...form, phone: v })} placeholder="097 000 0000" />
);
}
return (
{list.map((m) => (
))}
{list.length === 0 &&
No member matches that.
}
);
}
// ---------- Order screen ----------
function OrderSurface({ session }) {
const [s, a] = useVMAX();
const [cat, setCat] = React.useState('All');
const [search, setSearch] = React.useState('');
const [sheetItem, setSheetItem] = React.useState(null);
const [memberOpen, setMemberOpen] = React.useState(false);
const [payOpen, setPayOpen] = React.useState(false);
const [ordersOpen, setOrdersOpen] = React.useState(false);
const [nudge, setNudge] = React.useState(true);
const [reqOpen, setReqOpen] = React.useState(false);
const order = s.order;
const member = order.member;
const menu = s.menu;
let list = menu.filter((m) => cat === 'All' || m.cat === cat);
if (search) list = menu.filter((m) => m.name.toLowerCase().includes(search.toLowerCase()));
const series = [...new Set(list.map((m) => m.series))];
const sub = order.lines.reduce((acc, o) => acc + (member ? o.member : o.price) * o.qty, 0);
const ordSub = order.lines.reduce((acc, o) => acc + o.price * o.qty, 0);
const discount = member ? ordSub - order.lines.reduce((acc, o) => acc + o.member * o.qty, 0) : 0;
const low = lowStock(s);
const lowItem = low[0];
const tap = (m) => { if (hasVariants(m)) setSheetItem(m); else a.addLine({ ...m, temp: null, size: null, price: m.price, member: m.member }); };
return (
{/* MENU */}
setSearch(e.target.value)} placeholder="Search drinks…" style={{ border: 'none', background: 'transparent', padding: '12px 0', fontFamily: 'var(--font-body)', fontSize: 15, width: '100%', outline: 'none', color: 'var(--vmax-ink)' }} />
{menu.length > 0 &&
{categories.map((c) => { setCat(c); setSearch(''); }}>{c})}
}
{menu.length === 0 &&
}
{menu.length > 0 && series.length === 0 &&
No drinks match that. Check the spelling or pick a category.
}
{series.map((ser) => (
{ser}
{list.filter((m) => m.series === ser).map((m) => tap(m)} />)}
))}
{nudge && lowItem && (
{lowItem.front === 0 ? 'Out of' : 'Running low on'} {lowItem.name.toLowerCase()}
)}
{/* TICKET */}
Order #A-0{s.orderNo}
{order.lines.length === 0 ? (
Tap a drink to start the order. Hot or cold is chosen as you add it.
) : (
{order.lines.map((o, i) => (
{o.name}
{o.temp && }{o.size && {sizeLabel(o.size)}}
a.bumpLine(i, v)} min={0} />
{K((member ? o.member : o.price) * o.qty)}
))}
)}
{order.lines.length > 0 && (
|
{member &&
|
}
Total
{K(sub)}
)}
setSheetItem(null)} onAdd={(l) => { a.addLine(l); setSheetItem(null); }} />
setMemberOpen(false)} />
setPayOpen(false)} />
setOrdersOpen(false)} session={session} />
setReqOpen(false)} who={session.active ? session.active.name : ''} preset={lowItem ? lowItem.id : null} />
);
}
function Row({ k, v, accent }) {
return {k}{v}
;
}
window.VMAXORDER = { OrderSurface };
})();