`; } function receiptHTML(order) { const items = order.items || []; const rows = items.map(l => `${l.name}
${l.qty} × ${money(l.price)}
${money(l.qty*l.price)}`).join(""); const line = (a,b,bold) => `${a}${b}`; return `Receipt

Coffee & Bagels

${order.branch||""}
Bill #${order.bill_no||"-"} · ${order.type}${order.table_name?(" · Table "+order.table_name):""}
${new Date(order.settled_at||Date.now()).toLocaleString()}

${rows}

${line("Subtotal", money(order.subtotal))}${order.discount>0?line("Discount","−"+money(order.discount)):""}${line("GST ("+order.tax_rate+"%)", money(order.tax))}${line("TOTAL", money(order.total), true)}

Paid by ${order.payment_mode||"-"}
Thank you! Visit again.
`; } // ---------- Login ---------- function LoginView({ onLogin }) { const [u, setU] = useState(""); const [p, setP] = useState(""); const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); async function go() { setErr(""); setBusy(true); try { const rows = await sb.get("users", "username=eq." + encodeURIComponent(u.trim()) + "&select=*"); const user = rows && rows[0]; if (!user || user.password !== p) { setErr("Wrong username or password."); setBusy(false); return; } onLogin(user); } catch (e) { setErr("Can't reach server. Check connection."); setBusy(false); } } return (
☕🥯

Coffee & Bagels POS

Sign in to start billing
setU(e.target.value)} autoCapitalize="none" /> setP(e.target.value)} onKeyDown={e=>e.key==="Enter"&&go()} /> {err &&
{err}
}
); } // ---------- Menu grid + cart (Order screen) ---------- function OrderView({ branch, menu, order, setOrder, onSend, onSettle, onClose, readOnly }) { const cats = [...new Set(menu.filter(m=>m.active!==false).map(m=>m.category||"Other"))]; const [cat, setCat] = useState(cats[0] || ""); const items = order.items || []; const subtotal = Math.round(items.reduce((s,l)=>s+l.qty*l.price,0)*100)/100; const discount = numv(order.discount); const taxRate = order.tax_rate!=null?numv(order.tax_rate):5; const tax = Math.round((subtotal-discount)*taxRate)/100; const total = Math.round((subtotal-discount+tax)*100)/100; function addItem(m) { if (readOnly) return; const its = [...items]; const ex = its.find(l => l.mid===m.id && !l.notes); if (ex) ex.qty += 1; else its.push({ mid:m.id, name:m.name, price:numv(m.price), station:m.station||"kitchen", qty:1, kotQty:0, notes:"" }); setOrder({ ...order, items: its }); } function setQty(i, d) { const its = [...items]; its[i] = { ...its[i], qty: Math.max(0, its[i].qty + d) }; setOrder({ ...order, items: its.filter(l=>l.qty>0) }); } function setNote(i, v) { const its=[...items]; its[i]={...its[i], notes:v}; setOrder({ ...order, items:its }); } const pendingKot = items.reduce((s,l)=>s+Math.max(0,l.qty-(l.kotQty||0)),0); return (
{order.type==="dine-in" ? "Dine-in" : "Takeaway"}
{/* Menu */}
{cats.map(c => )} {cats.length===0 && No menu yet — add items in Setup.}
{menu.filter(m=>m.active!==false && (m.category||"Other")===cat).sort((a,b)=>(a.sort||0)-(b.sort||0)).map(m => ( ))}
{/* Cart */}
Order
{items.length===0 &&
Tap menu items to add
} {items.map((l,i) => (
{l.name} {(l.kotQty||0)>0 && ✓KOT {l.kotQty}} {l.qty} {money(l.qty*l.price)}
setNote(i,e.target.value)} placeholder="note (e.g. less sugar)" style={{ ...S.small, padding:"4px 8px", fontSize:12, width:"100%", marginTop:4, border:"1px dashed #ddd" }} />
))}
Discount ₹ setOrder({...order, discount:e.target.value})} placeholder="0" style={{ ...S.small, width:80, textAlign:"right", padding:"4px 8px" }} />
GST % setOrder({...order, tax_rate:e.target.value})} style={{ ...S.small, width:80, textAlign:"right", padding:"4px 8px" }} />
Total{money(total)}
); } function Row({ k, v }) { return
{k}{v}
; } // ---------- Payment modal ---------- function PayModal({ total, onPay, onCancel }) { return (
e.stopPropagation()} style={{ ...S.card, width:"100%", maxWidth:420, padding:20, borderRadius:"18px 18px 0 0" }}>
Amount due
{money(total)}
{["Cash","UPI","Card"].map(m => ( ))}
); } // ---------- Tables (dine-in floor) ---------- function TablesView({ branch, tables, openOrders, onOpenTable, onTakeaway }) { const byTable = {}; openOrders.forEach(o => { if (o.type==="dine-in" && o.table_id) byTable[o.table_id] = o; }); const areas = [...new Set(tables.map(t=>t.area||"Main"))]; return (
{tables.length===0 &&
No tables yet — add them in Setup.
} {areas.map(area => (
{area}
{tables.filter(t=>(t.area||"Main")===area).sort((a,b)=>(a.sort||0)-(b.sort||0)).map(t => { const o = byTable[t.id]; const busy = !!o; const tot = o ? Math.round((o.items||[]).reduce((s,l)=>s+l.qty*l.price,0)) : 0; return ( ); })}
))}
); } // ---------- Sales (today) ---------- function SalesView({ orders }) { const settled = orders.filter(o=>o.status==="settled"); const total = settled.reduce((s,o)=>s+numv(o.total),0); const byMode = {}; settled.forEach(o=>{ byMode[o.payment_mode||"-"]=(byMode[o.payment_mode||"-"]||0)+numv(o.total); }); return (
Today's sales
{money(total)}
Bills
{settled.length}
{["Cash","UPI","Card"].map(m =>
{m}
{money(byMode[m]||0)}
)}
BILLS
{settled.length===0 &&
No bills settled today yet.
} {settled.sort((a,b)=>(b.settled_at||"").localeCompare(a.settled_at||"")).map(o => (
#{o.bill_no} · {o.type}{o.table_name?(" · T"+o.table_name):""}
{(o.items||[]).length} item(s) · {o.payment_mode} · {o.settled_at?new Date(o.settled_at).toLocaleTimeString():""}
{money(o.total)}
))}
); } // ---------- Setup: menu + tables ---------- function SetupView({ branch, menu, reloadMenu, tables, reloadTables }) { const [tab, setTab] = useState("menu"); return (
{tab==="menu" ? : }
); } function MenuManager({ menu, reload }) { const blank = { name:"", category:"", price:"", station:"kitchen", veg:true }; const [f, setF] = useState(blank); const [busy, setBusy] = useState(false); const cats = [...new Set(menu.map(m=>m.category).filter(Boolean))]; async function save() { if (!f.name.trim()) return; setBusy(true); try { await sb.upsert("pos_menu", { id: f.id||uid(), name:f.name.trim(), category:(f.category||"Other").trim(), price:numv(f.price), station:f.station, veg:!!f.veg, active:true, sort: f.sort||0 }); setF(blank); await reload(); } catch(e){ alert("Save failed"); } setBusy(false); } async function toggle(m){ try{ await sb.patch("pos_menu","id=eq."+m.id,{active: m.active===false}); reload(); }catch(e){} } async function del(m){ if(!confirm("Delete "+m.name+"?"))return; try{ await sb.del("pos_menu","id=eq."+m.id); reload(); }catch(e){} } return (
{f.id?"Edit item":"Add menu item"}
setF({...f,name:e.target.value})} />
setF({...f,category:e.target.value})} /> {cats.map(c=> setF({...f,price:e.target.value})} />
{f.id && }
{cats.map(cat => (
{cat.toUpperCase()}
{menu.filter(m=>m.category===cat).map(m => (
{m.veg!==false?"🟢":"🔴"} {m.name} · {m.station} {money(m.price)}
))}
))}
); } function TablesManager({ branch, tables, reload }) { const blank = { name:"", area:"Main", seats:4 }; const [f, setF] = useState(blank); async function save() { if (!f.name.trim()) return; try { await sb.upsert("pos_tables", { id:f.id||uid(), branch, name:f.name.trim(), area:(f.area||"Main").trim(), seats:numv(f.seats)||4, sort:f.sort||0 }); setF(blank); await reload(); } catch(e){ alert("Save failed"); } } async function del(t){ if(!confirm("Delete table "+t.name+"?"))return; try{ await sb.del("pos_tables","id=eq."+t.id); reload(); }catch(e){} } return (
Add table — {branch}
setF({...f,name:e.target.value})} /> setF({...f,area:e.target.value})} /> setF({...f,seats:e.target.value})} />
{tables.map(t => (
{t.name} · {t.area} · {t.seats} seats
))}
); } // ---------- App ---------- function App() { const [session, setSession] = useState(() => { try { return JSON.parse(localStorage.getItem("pos_session")||"null"); } catch(e){ return null; } }); const [branch, setBranch] = useState(() => localStorage.getItem("pos_branch")||""); const [branches, setBranches] = useState([]); const [menu, setMenu] = useState([]); const [tables, setTables] = useState([]); const [orders, setOrders] = useState([]); // today's orders for this branch const [view, setView] = useState("tables"); // tables | order | sales | setup const [order, setOrder] = useState(null); // active order const [pay, setPay] = useState(null); // payment modal {total,...} const isAdmin = isAdminRole(session) || roleSet(session).has("manager"); const readOnly = isViewerRole(session); function login(u){ setSession(u); localStorage.setItem("pos_session", JSON.stringify(u)); const b=(u.branches&&u.branches[0])|| ""; if(b){ setBranch(b); localStorage.setItem("pos_branch", b); } } function logout(){ setSession(null); localStorage.removeItem("pos_session"); } async function loadBranches(){ try { const b = await sb.get("branches","select=name&order=name"); setBranches((b||[]).map(x=>x.name)); } catch(e){} } async function reloadMenu(){ try { const m = await sb.get("pos_menu","select=*&order=sort"); setMenu(m||[]); } catch(e){} } async function reloadTables(){ if(!branch) return; try { const t = await sb.get("pos_tables","branch=eq."+encodeURIComponent(branch)+"&order=sort"); setTables(t||[]); } catch(e){} } async function reloadOrders(){ if(!branch) return; try { const o = await sb.get("pos_orders","branch=eq."+encodeURIComponent(branch)+"&opened_at=gte."+todayISO()+"&order=opened_at.desc"); setOrders(o||[]); } catch(e){} } useEffect(() => { if (session) { loadBranches(); reloadMenu(); } }, [session]); useEffect(() => { if (session && branch) { reloadTables(); reloadOrders(); localStorage.setItem("pos_branch", branch); } }, [session, branch]); useEffect(() => { if (!session || !branch) return; const id = setInterval(reloadOrders, 15000); return () => clearInterval(id); }, [session, branch]); async function persist(o){ try { await sb.upsert("pos_orders", o); } catch(e){} } function startTakeaway(){ const o = { id:uid(), branch, type:"takeaway", table_id:null, table_name:null, status:"open", items:[], discount:0, tax_rate:5, opened_by:session.name, opened_at:new Date().toISOString() }; setOrder(o); setView("order"); } function openTable(t){ const existing = orders.find(o=>o.type==="dine-in" && o.table_id===t.id && o.status==="open"); const o = existing || { id:uid(), branch, type:"dine-in", table_id:t.id, table_name:t.name, status:"open", items:[], discount:0, tax_rate:5, opened_by:session.name, opened_at:new Date().toISOString() }; setOrder(o); setView("order"); } function updateOrder(o){ setOrder(o); if (o.items && o.items.length) persist(stripForDb(o)); } function stripForDb(o){ const items = o.items||[]; const subtotal = Math.round(items.reduce((s,l)=>s+l.qty*l.price,0)*100)/100; return { ...o, items, subtotal }; } function closeOrder(){ if (order && (!order.items || order.items.length===0) && order.id) { sb.del("pos_orders","id=eq."+order.id).catch(()=>{}); } else if (order && order.id && order.items && order.items.length > 0) { persist(stripForDb({...order, status:"open"})); } reloadOrders(); setOrder(null); setView("tables"); } function sendKOT(){ if (!order) return; const items = order.items||[]; const stations = [...new Set(items.filter(l=>(l.qty-(l.kotQty||0))>0).map(l=>l.station||"kitchen"))]; stations.forEach(st => { const lines = items.filter(l=>(l.station||"kitchen")===st && (l.qty-(l.kotQty||0))>0).map(l=>({ name:l.name, qty:l.qty-(l.kotQty||0), notes:l.notes })); if (lines.length) openPrint(kotHTML(order, lines, st)); }); const newItems = items.map(l=>({ ...l, kotQty:l.qty })); const o = { ...order, items:newItems }; setOrder(o); persist(stripForDb(o)); } function askSettle(total, subtotal, tax, taxRate, discount){ setPay({ total, subtotal, tax, taxRate, discount }); } async function doPay(mode){ const p = pay; setPay(null); const billNo = (orders.filter(o=>o.status==="settled").reduce((m,o)=>Math.max(m, o.bill_no||0), 0)) + 1; const o = { ...stripForDb(order), bill_no:billNo, status:"settled", subtotal:p.subtotal, tax:p.tax, tax_rate:p.taxRate, discount:p.discount, total:p.total, payment_mode:mode, settled_by:session.name, settled_at:new Date().toISOString() }; await persist(o); openPrint(receiptHTML(o)); await reloadOrders(); setOrder(null); setView("tables"); } if (!session) return ; const tab = (id, label, icon) => ( ); return (
{/* Top bar */}
☕🥯 POS
{!branch ?
Pick an outlet above to begin.
: view==="order" && order ? : view==="sales" ? : view==="setup" && isAdmin ? : o.status==="open")} onOpenTable={openTable} onTakeaway={startTakeaway} />}
{pay && setPay(null)} />} {/* Bottom nav */} {view!=="order" && (
{tab("tables","Tables","🍽️")} {tab("sales","Sales","📊")} {isAdmin && tab("setup","Setup","⚙️")}
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();