// VMAX Store Manager — read/back views: Dashboard, Inventory (two-location), Transfers, Recipes. (function () { const DS = window.VMAX365DesignSystem_0f78b2; const { StatTile, StockBadge, SegmentedControl, Button, Badge } = DS; const { Icon, Cup, Sheet, Overline } = window.VMAXUI; const { Avatar, EmptyState, Field, TextInput, NumberInput, Select, Toggle, pushToast } = window.VMAXW; const { K, dashboard, sizes } = window.VMAXP; const { useVMAX, statusOf, lowStock, missingRecipes, notifications } = window.VMAXStore; function Panel({ title, children, right, flush, mobile, style }) { return (

{title}

{right}
{children}
); } window.VMAXMGRUI = { Panel }; // ============ Dashboard ============ function Dashboard({ mobile, go }) { const [s] = useVMAX(); const low = lowStock(s); const notif = notifications(s); const mobileTotal = Object.values(s.tally.mobile).reduce((a, b) => a + b, 0); const turnover = s.tally.cash + mobileTotal; const dishes = s.tally.dishes; const cashPct = turnover ? Math.round((s.tally.cash / turnover) * 100) : 0; const flagged = s.tweaks.dayState !== 'clean'; const flagCount = flagged ? (s.tweaks.dayState === 'closing' ? 2 : 3) : 0; return (
{(notif.front > 0 || notif.back > 0) && ( )}
Live}>
{dashboard.spark.map((v, i) => (
{['Sa', 'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr'][i]}
))}
}> {flagged ? ( ) :
Everything balanced today
}
Quiet when all is well; tap to reconcile while the shift is still on.
hot / cold split}>
{dashboard.ranking.map((d, i) => (
{i + 1}
{d.name}
{(d.cold || d.hot) ? (
{d.cold ? {d.cold} cold : null} {d.hot ? {d.hot} hot : null}
) : null}
{!mobile &&
} {d.qty}
))}
); } function Split({ label, pct }) { return (
{label}{pct}%
); } // ============ Inventory ============ function Inventory({ mobile, who }) { const [s, a] = useVMAX(); const [move, setMove] = React.useState(null); const notif = notifications(s); const [loc, setLoc] = React.useState('Both'); const [q, setQ] = React.useState(''); const [open, setOpen] = React.useState(null); let items = s.ingredients.filter((i) => i.name.toLowerCase().includes(q.toLowerCase())); const rank = { out: 0, low: 1, ok: 2 }; items = [...items].sort((a, b) => rank[statusOf(a)] - rank[statusOf(b)]); const moves = (id) => s.ledger.filter((e) => e.text && e.text.toLowerCase().includes((s.ingredients.find((x) => x.id === id) || {}).name.toLowerCase().split(' ')[0])).slice(0, 3); const search = (
setQ(e.target.value)} placeholder="Search items…" style={{ border: 'none', background: 'transparent', padding: '11px 0', fontFamily: 'var(--font-body)', fontSize: 14, width: '100%', outline: 'none' }} />
); if (s.ingredients.every((i) => i.back + i.front === 0)) return ; if (mobile) { return ( {(notif.front > 0 || notif.back > 0) && }
}> {search}
{items.map((i) => { const st = statusOf(i); const isOpen = open === i.id; return (
setOpen(isOpen ? null : i.id)} style={{ border: '1.5px solid var(--vmax-line)', borderRadius: 'var(--radius-md)', padding: '12px 14px', cursor: 'pointer' }}>
{i.name}
{(loc === 'Both' || loc === 'Back') && } {(loc === 'Both' || loc === 'Front') && }
{isOpen &&
{i.packSize} · buy by {i.buy}{i.expiry && Nearest expiry · {i.expiry}} {moves(i.id).map((m) => {m.at} · {m.text})}
}
); })}
setMove(null)} />
); } return ( {(notif.front > 0 || notif.back > 0) && }
}> {search} {(loc === 'Both' || loc === 'Back') && } {(loc === 'Both' || loc === 'Front') && } {items.map((i) => { const st = statusOf(i); return ( {(loc === 'Both' || loc === 'Back') && } {(loc === 'Both' || loc === 'Front') && } ); })}
ItemBack storeShop frontPack / expiry Status
{i.name}
{i.back} {i.unit}{i.front} {i.unit}{i.packSize}{i.expiry ? ' · exp ' + i.expiry : ''}
setMove(null)} /> ); } function Stat({ k, v, danger }) { return
{v}
{k}
; } function RunOutChip({ notif }) { return {notif.front} front · {notif.back} back low; } function MoveStock({ item, who, onClose }) { const [, a] = useVMAX(); const [tab, setTab] = React.useState('transfer'); const [qty, setQty] = React.useState(1); const [mloc, setMloc] = React.useState('front'); const [val, setVal] = React.useState(0); React.useEffect(() => { if (item) { setTab('transfer'); setQty(Math.min(1, item.back) || 1); setMloc('front'); setVal(item.front); } }, [item && item.id]); if (!item) return null; return (
{item.name}
Back {item.back} {item.unit} · Front {item.front} {item.unit}
{tab === 'transfer' ? (
Move back → front
of {item.back} in back
) : (
Set a counted level
{ setMloc(v); setVal(v === 'back' ? item.back : item.front); }} />
)}
); } // ============ Transfers ============ function Transfers({ mobile, who }) { const [s, a] = useVMAX(); const inv = s.ingredients; const name = (id) => (inv.find((x) => x.id === id) || {}).name; const pending = s.requests.filter((r) => r.status === 'pending'); const done = s.requests.filter((r) => r.status !== 'pending'); return (
{pending.length} waiting}>

Requests come in from the counter. Confirm what you send and the back-store and shop-front balances move together.

{pending.length === 0 ? : (
{pending.map((r) => (
{r.by}
#{r.id} · {r.when}
{r.urgent && URGENT}
{r.items.map((it) => { const st = statusOf(inv.find((x) => x.id === it.id) || { front: 1, warn: 0 }); return (
{it.qty}× {name(it.id)}
); })}
))}
)}
{done.length === 0 ?
Nothing fulfilled yet today.
: (
{done.map((r) => (
{r.items.map((it) => it.qty + '× ' + name(it.id)).join(' · ')}
#{r.id} · {r.by} → {r.fulfilledBy} · {r.when}
))}
)}
); } const qbtn = { width: 28, height: 28, borderRadius: 7, border: '1.5px solid var(--vmax-line)', background: 'var(--surface-card)', cursor: 'pointer', fontWeight: 700, color: 'var(--vmax-ink-soft)', display: 'grid', placeItems: 'center' }; // ============ Recipes ============ function Recipes({ mobile, who }) { const [s, a] = useVMAX(); const [sel, setSel] = React.useState(mobile ? null : (s.menu[0] && s.menu[0].id)); const [editing, setEditing] = React.useState(false); const iname = (id) => (s.ingredients.find((x) => x.id === id) || { name: id }).name; React.useEffect(() => { setEditing(false); }, [sel]); if (s.menu.length === 0) return ; const List = (
{s.menu.map((m) => { const missing = !s.recipes[m.id]; return ( ); })}
); const Detail = (id) => { const r = s.recipes[id]; const item = s.menu.find((m) => m.id === id); if (!item) return null; if (editing || (!r && editing)) return setEditing(false)} onSave={(rec) => { a.saveRecipe(id, rec, who); setEditing(false); pushToast('Recipe saved'); }} mobile={mobile} />; return ( setSel(null)} style={{ display: 'flex', alignItems: 'center', gap: 4, border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--vmax-ink-soft)', fontWeight: 600, fontSize: 13 }}>Menu : (r && )}> {r ? (
Ingredients{item.hotcold ? ' · ' + (r.cup === 'hot' ? 'hot' : 'cold') + ' default' : ''}
{r.ings.map((ing) =>
{iname(ing.id)}{ing.q}
)}
≈ {Math.round((r.yield[0] + r.yield[1]) / 2)} {r.unit}
per batch · acceptable {r.yield[0]}–{r.yield[1]}
) : ( setEditing(true)}>Add recipe} /> )}
); }; if (mobile) return sel ? Detail(sel) : List; return
{List}{sel ? Detail(sel) : }
; } function RecipeEditor({ item, initial, onCancel, onSave, mobile }) { const [s] = useVMAX(); const [ings, setIngs] = React.useState(initial ? initial.ings.map((x) => ({ ...x })) : []); const [lo, setLo] = React.useState(initial ? initial.yield[0] : 60); const [hi, setHi] = React.useState(initial ? initial.yield[1] : 70); const [cup, setCup] = React.useState(initial ? initial.cup : (item.defaultTemp || 'cold')); const [addId, setAddId] = React.useState(s.ingredients[0] && s.ingredients[0].id); const iname = (id) => (s.ingredients.find((x) => x.id === id) || { name: id }).name; return ( Ingredients & packaging
{ings.map((ing, i) => (
{iname(ing.id)} setIngs((a) => a.map((x, k) => k === i ? { ...x, q: v } : x))} style={{ width: 96, padding: '7px 10px' }} />
))} {ings.length === 0 &&
Add the ingredients this drink uses.
}