| |
|
|
| let menuData = null; |
| let cart = []; |
| let tableNumber = null; |
| let tablesCount = 0; |
|
|
| document.addEventListener('DOMContentLoaded', init); |
|
|
| async function init() { |
| |
| const params = new URLSearchParams(window.location.search); |
| const tableParam = params.get('table'); |
| if (tableParam) { |
| tableNumber = parseInt(tableParam); |
| document.getElementById('tableNumber').textContent = `#${tableNumber}`; |
| } |
|
|
| |
| const [menuRes, tablesRes] = await Promise.all([ |
| fetch('/api/menu'), |
| fetch('/api/tables') |
| ]); |
| menuData = await menuRes.json(); |
| const tablesData = await tablesRes.json(); |
| tablesCount = tablesData.tables || 0; |
|
|
| |
| if (!tableNumber) { |
| document.getElementById('tableDisplay').style.display = 'none'; |
| const sel = document.getElementById('cartTableSelect'); |
| sel.innerHTML = '<option value="">Select your table</option>' + |
| Array.from({ length: tablesCount }, (_, i) => |
| `<option value="${i + 1}">Table ${i + 1}</option>` |
| ).join(''); |
| sel.addEventListener('change', () => { |
| tableNumber = parseInt(sel.value) || null; |
| updateSubmit(); |
| }); |
| document.getElementById('cartTableRow').style.display = ''; |
| } else { |
| document.getElementById('cartTableRow').style.display = 'none'; |
| } |
|
|
| renderFilters(); |
| renderItems(); |
| updateSubmit(); |
| setupCartToggle(); |
|
|
| |
| if (tableNumber) { |
| loadLiveOrders(); |
| setInterval(loadLiveOrders, 10000); |
| } |
| } |
|
|
| |
| function renderFilters() { |
| const cats = menuData.categories || []; |
| const container = document.getElementById('ordFilters'); |
| container.innerHTML = cats.map(c => ` |
| <button class="ord-filter-btn${c.id === 'all' ? ' active' : ''}" data-cat="${c.id}"> |
| ${c.name} |
| </button> |
| `).join(''); |
|
|
| container.addEventListener('click', (e) => { |
| const btn = e.target.closest('.ord-filter-btn'); |
| if (!btn) return; |
| container.querySelectorAll('.ord-filter-btn').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| renderItems(btn.dataset.cat); |
| }); |
| } |
|
|
| |
| function renderItems(catId = 'all') { |
| const container = document.getElementById('ordItems'); |
| let items = menuData.items || []; |
| if (catId !== 'all') items = items.filter(i => i.category === catId); |
|
|
| container.innerHTML = items.map(item => { |
| const hasImg = item.image && item.image.startsWith('http'); |
| const thumbSvg = svgPlaceholder(item.category); |
| const inCart = cart.find(c => c.mid === item.id); |
| const qty = inCart ? inCart.qty : 0; |
|
|
| return ` |
| <div class="ord-item${!item.available ? ' sold-out' : ''}" data-id="${item.id}"> |
| <div class="ord-item-img"> |
| ${thumbSvg} |
| ${hasImg ? `<img src="${esc(item.image)}" alt="" loading="lazy" onerror="this.style.display='none'">` : ''} |
| </div> |
| <div class="ord-item-info"> |
| <div class="ord-item-name">${esc(item.name)}</div> |
| <div class="ord-item-desc">${esc(item.description || '')}</div> |
| </div> |
| <div class="ord-item-price">₹${item.price.toFixed(0)}</div> |
| <button class="ord-item-add" ${!item.available ? 'disabled' : ''}>${qty > 0 ? qty : '+'}</button> |
| </div> |
| `; |
| }).join(''); |
|
|
| |
| container.querySelectorAll('.ord-item:not(.sold-out)').forEach(el => { |
| el.addEventListener('click', () => { |
| const id = el.dataset.id; |
| const item = menuData.items.find(i => i.id === id); |
| if (!item) return; |
| addToCart(item); |
| }); |
| }); |
| } |
|
|
| |
| function addToCart(item) { |
| const existing = cart.find(c => c.mid === item.id); |
| if (existing) { |
| existing.qty++; |
| } else { |
| cart.push({ mid: item.id, name: item.name, price: item.price, qty: 1 }); |
| } |
| renderCart(); |
| renderItems(document.querySelector('.ord-filter-btn.active')?.dataset?.cat || 'all'); |
| } |
|
|
| function removeFromCart(mid) { |
| cart = cart.filter(c => c.mid !== mid); |
| renderCart(); |
| renderItems(document.querySelector('.ord-filter-btn.active')?.dataset?.cat || 'all'); |
| } |
|
|
| function changeQty(mid, delta) { |
| const item = cart.find(c => c.mid === mid); |
| if (!item) return; |
| item.qty += delta; |
| if (item.qty <= 0) { |
| removeFromCart(mid); |
| return; |
| } |
| renderCart(); |
| renderItems(document.querySelector('.ord-filter-btn.active')?.dataset?.cat || 'all'); |
| } |
|
|
| function renderCart() { |
| const container = document.getElementById('cartItems'); |
| const total = cart.reduce((sum, c) => sum + c.price * c.qty, 0); |
|
|
| if (cart.length === 0) { |
| container.innerHTML = '<p class="ord-cart-empty">Tap items from the menu to add them here.</p>'; |
| } else { |
| container.innerHTML = cart.map(c => ` |
| <div class="cart-item"> |
| <div class="cart-item-info"> |
| <div class="cart-item-name">${esc(c.name)}</div> |
| <div class="cart-item-meta">₹${c.price.toFixed(0)} each</div> |
| </div> |
| <div class="cart-item-qty"> |
| <button class="cart-qty-btn" data-action="minus" data-mid="${c.mid}">−</button> |
| <span class="cart-qty-num">${c.qty}</span> |
| <button class="cart-qty-btn" data-action="plus" data-mid="${c.mid}">+</button> |
| </div> |
| <div class="cart-item-price">₹${(c.price * c.qty).toFixed(0)}</div> |
| <button class="cart-item-remove" data-action="remove" data-mid="${c.mid}">✕</button> |
| </div> |
| `).join(''); |
|
|
| container.querySelectorAll('.cart-qty-btn, .cart-item-remove').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| const action = btn.dataset.action; |
| const mid = btn.dataset.mid; |
| if (action === 'plus') changeQty(mid, 1); |
| else if (action === 'minus') changeQty(mid, -1); |
| else if (action === 'remove') removeFromCart(mid); |
| }); |
| }); |
| } |
|
|
| document.getElementById('cartTotal').textContent = `₹${total.toFixed(0)}`; |
| document.getElementById('cartCount').textContent = cart.reduce((s, c) => s + c.qty, 0); |
| document.getElementById('cartBadge').style.display = cart.length > 0 ? '' : 'none'; |
|
|
| updateSubmit(); |
| } |
|
|
| |
| async function loadLiveOrders() { |
| if (!tableNumber) return; |
| try { |
| const res = await fetch(`/api/orders/table/${tableNumber}`); |
| if (!res.ok) return; |
| const orders = await res.json(); |
|
|
| const section = document.getElementById('ordLive'); |
| const list = document.getElementById('ordLiveList'); |
|
|
| if (orders.length === 0) { |
| section.style.display = 'none'; |
| return; |
| } |
|
|
| section.style.display = ''; |
| const statusLabel = { pending: 'Preparing', served: 'On its way' }; |
|
|
| list.innerHTML = orders.map(o => ` |
| <div class="live-order ${o.status}"> |
| <div class="live-order-head"> |
| <span class="live-order-status ${o.status}">${statusLabel[o.status] || o.status}</span> |
| <span style="font-size:0.68rem;color:var(--parchment);">${timeAgoShort(o.createdAt)}</span> |
| </div> |
| <ul class="live-order-items"> |
| ${o.items.map(i => i.mid |
| ? `<li><span>${i.qty}× ${esc(i.name)}</span> <span>₹${(i.price * i.qty).toFixed(0)}</span></li>` |
| : `<li class="lo-custom">"${esc(i.text)}"</li>` |
| ).join('')} |
| </ul> |
| ${o.note ? `<p class="live-order-note">Note: ${esc(o.note)}</p>` : ''} |
| <p class="live-order-total">₹${o.total.toFixed(0)}</p> |
| </div> |
| `).join(''); |
| } catch { } |
| } |
|
|
| function timeAgoShort(iso) { |
| const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); |
| if (diff < 60) return 'just now'; |
| if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; |
| return `${Math.floor(diff / 3600)}h ago`; |
| } |
|
|
| |
| function updateSubmit() { |
| const btn = document.getElementById('submitBtn'); |
| const valid = cart.length > 0 && !!tableNumber; |
| btn.disabled = !valid; |
| } |
|
|
| document.getElementById('submitBtn').addEventListener('click', async () => { |
| if (!tableNumber || cart.length === 0) return; |
|
|
| const note = document.getElementById('orderNote').value.trim(); |
| const btn = document.getElementById('submitBtn'); |
| btn.disabled = true; |
| btn.textContent = 'Placing order…'; |
|
|
| try { |
| const res = await fetch('/api/orders', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ table: tableNumber, items: cart, note }) |
| }); |
| if (!res.ok) throw new Error((await res.json()).error || 'Failed'); |
|
|
| |
| cart = []; |
| renderCart(); |
| document.getElementById('orderNote').value = ''; |
| renderItems(document.querySelector('.ord-filter-btn.active')?.dataset?.cat || 'all'); |
| showToast('✓ Order sent to the kitchen!'); |
| loadLiveOrders(); |
| } catch (err) { |
| btn.disabled = false; |
| btn.textContent = 'Place order'; |
| showToast('Something went wrong. Try again.'); |
| console.error(err); |
| } |
| }); |
|
|
| |
| function setupCartToggle() { |
| const cart = document.getElementById('ordCart'); |
| const head = cart.querySelector('.ord-cart-head'); |
| head.addEventListener('click', () => { |
| cart.classList.toggle('collapsed'); |
| }); |
| } |
|
|
| |
| function showToast(msg) { |
| const container = document.getElementById('ordToast'); |
| container.innerHTML = `<div class="ord-toast-inner">${msg}</div>`; |
| container.classList.add('show'); |
| setTimeout(() => container.classList.remove('show'), 2500); |
| } |
|
|
| |
| function esc(str) { |
| const div = document.createElement('div'); |
| div.textContent = str || ''; |
| return div.innerHTML; |
| } |
|
|
| function svgPlaceholder(category) { |
| const svgs = { |
| 'coffee': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h14v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"/><path d="M18 9h2a2 2 0 012 2v1a2 2 0 01-2 2h-2"/><ellipse cx="8" cy="14" rx="2" ry="1.5"/><path d="M8 1v2" opacity="0.4"/><path d="M6 0v2" opacity="0.3"/><path d="M10 0v2" opacity="0.3"/></svg>', |
| 'tea': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h10a3 3 0 013 3v3a3 3 0 01-3 3H5a3 3 0 01-3-3v-3a3 3 0 013-3z"/><path d="M2 11l-2 3h2l1-3"/><path d="M7 8V6a1 1 0 011-1h2a1 1 0 011 1v2"/><circle cx="10" cy="3.5" r="1.2"/><path d="M8 1v2" opacity="0.4"/><path d="M10 0v2" opacity="0.3"/><path d="M12 1v2" opacity="0.3"/></svg>', |
| 'pastries': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 16C4 16 6 10 12 8s10 0 10 0c-4 2-6 8-6 8"/><path d="M5 14c1-3 4-6 7-7"/><path d="M19 14c-1-3-4-6-7-7"/><path d="M8 10h8" opacity="0.3"/></svg>', |
| 'food': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="18" rx="10" ry="3"/><rect x="8" y="8" width="8" height="7" rx="1"/><line x1="10" y1="9.5" x2="14" y2="9.5" opacity="0.4"/><line x1="10" y1="11.5" x2="14" y2="11.5" opacity="0.3"/><ellipse cx="12" cy="16" rx="3" ry="2" fill="currentColor" opacity="0.15"/></svg>' |
| }; |
| return svgs[category] || svgs['coffee']; |
| } |
|
|