// 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 (
);
}
// ============ 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' }}>
{(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}
| Item |
{(loc === 'Both' || loc === 'Back') && Back store | }
{(loc === 'Both' || loc === 'Front') && Shop front | }
Pack / expiry |
Status |
|
{items.map((i) => { const st = statusOf(i); return (
{i.name} |
{(loc === 'Both' || loc === 'Back') && {i.back} {i.unit} | }
{(loc === 'Both' || loc === 'Front') && {i.front} {i.unit} | }
{i.packSize}{i.expiry ? ' · exp ' + i.expiry : ''} |
|
|
); })}
setMove(null)} />
);
}
function Stat({ k, v, danger }) { return ; }
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.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.
}
{item.hotcold && Default served
}
Expected yield · a range, not a false-precise number
to
);
}
window.VMAXMGR1 = { Dashboard, Inventory, Transfers, Recipes };
})();