cafe / public /js /order.js
3v324v23's picture
💰 Convert all prices to ₹ (rupees)
6c1a79a
Raw
History Blame Contribute Delete
13.1 kB
// ── Order Page — The Roasted Bean ─────────────────────────────────────
let menuData = null;
let cart = [];
let tableNumber = null;
let tablesCount = 0;
document.addEventListener('DOMContentLoaded', init);
async function init() {
// Get table from URL
const params = new URLSearchParams(window.location.search);
const tableParam = params.get('table');
if (tableParam) {
tableNumber = parseInt(tableParam);
document.getElementById('tableNumber').textContent = `#${tableNumber}`;
}
// Load data
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;
// No table from URL — show table selector
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();
// Start live order polling if table is set
if (tableNumber) {
loadLiveOrders();
setInterval(loadLiveOrders, 10000);
}
}
// ── Filters ──────────────────────────────────────────────────────────
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);
});
}
// ── Items ────────────────────────────────────────────────────────────
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('');
// Click handler
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);
});
});
}
// ── Cart ──────────────────────────────────────────────────────────────
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();
}
// ── Live Orders ───────────────────────────────────────────────────────
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 { /* silently fail — customer-facing */ }
}
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`;
}
// ── Submit ────────────────────────────────────────────────────────────
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');
// Clear cart, show toast, refresh live orders
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);
}
});
// ── Cart Toggle (mobile) ──────────────────────────────────────────────
function setupCartToggle() {
const cart = document.getElementById('ordCart');
const head = cart.querySelector('.ord-cart-head');
head.addEventListener('click', () => {
cart.classList.toggle('collapsed');
});
}
// ── Toast ────────────────────────────────────────────────────────────
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);
}
// ── Utils ─────────────────────────────────────────────────────────────
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'];
}