// ── Admin Dashboard — The Roasted Bean ───────────────────────────────────── let menuData = null; let editingItemId = null; document.addEventListener('DOMContentLoaded', () => checkAuth()); // ── Auth ──────────────────────────────────────────────────────────────────── async function checkAuth() { try { const res = await fetch('/api/auth'); const data = await res.json(); data.authenticated ? showDashboard() : showLogin(); } catch { showLogin(); } } function showLogin() { document.getElementById('loginScreen').style.display = ''; document.getElementById('dashboard').style.display = 'none'; document.getElementById('loginForm').addEventListener('submit', handleLogin); } function showDashboard() { document.getElementById('loginScreen').style.display = 'none'; document.getElementById('dashboard').style.display = ''; setupDashboard(); } async function handleLogin(e) { e.preventDefault(); const pw = document.getElementById('password').value; const errEl = document.getElementById('loginError'); try { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const data = await res.json(); if (data.success) { errEl.textContent = ''; showDashboard(); } else { errEl.textContent = data.error || 'Invalid password'; } } catch { errEl.textContent = 'Connection error. Server running?'; } } // ── Dashboard Setup ───────────────────────────────────────────────────────── async function setupDashboard() { await loadMenuData(); setupNav(); setupModal(); setupForms(); showSection('menu'); document.getElementById('logoutBtn').addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); location.reload(); }); document.getElementById('addItemBtn').addEventListener('click', () => openModal()); } async function loadMenuData() { const res = await fetch('/api/menu'); menuData = await res.json(); } // ── Navigation ────────────────────────────────────────────────────────────── function setupNav() { document.getElementById('topbarNav').addEventListener('click', (e) => { const btn = e.target.closest('.tb-nav-item'); if (!btn) return; document.querySelectorAll('.tb-nav-item').forEach(b => b.classList.remove('active')); btn.classList.add('active'); showSection(btn.dataset.section); }); } function showSection(section) { document.querySelectorAll('.panel').forEach(p => p.style.display = 'none'); switch (section) { case 'menu': document.getElementById('menuSection').style.display = ''; renderTable(); break; case 'categories': document.getElementById('categoriesSection').style.display = ''; renderCategories(); break; case 'info': document.getElementById('infoSection').style.display = ''; renderInfo(); break; case 'tables': document.getElementById('tablesSection').style.display = ''; setupTablesSection(); break; case 'settings': document.getElementById('settingsSection').style.display = ''; break; } } // ── Table ─────────────────────────────────────────────────────────────────── function renderTable() { const tbody = document.getElementById('menuTableBody'); const empty = document.getElementById('menuEmpty'); const items = menuData?.items || []; if (items.length === 0) { tbody.innerHTML = ''; empty.style.display = ''; return; } empty.style.display = 'none'; const cats = {}; (menuData.categories || []).forEach(c => { cats[c.id] = c.name; }); tbody.innerHTML = items.map(item => { const hasImg = item.image && item.image.startsWith('http'); const thumbImg = hasImg ? `` : ''; const thumbSvg = adminSvgIcon(item.category); return `
${thumbSvg}${thumbImg}
${esc(item.name)}
${esc((item.description || '').slice(0, 50))}
${cats[item.category] || item.category} ₹${item.price.toFixed(0)} ${item.available ? 'Available' : 'Sold out'}
`}).join(''); tbody.addEventListener('click', (e) => { const btn = e.target.closest('.act'); if (!btn) return; const id = btn.dataset.id; const item = items.find(i => i.id === id); if (!item) return; if (btn.classList.contains('edit')) openModal(item); else if (btn.classList.contains('toggle')) toggleItem(item); else if (btn.classList.contains('danger')) deleteItem(id); }); } // ── Modal ─────────────────────────────────────────────────────────────────── function setupModal() { const modal = document.getElementById('itemModal'); document.getElementById('modalClose').addEventListener('click', closeModal); document.getElementById('modalCancel').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); }); document.getElementById('itemForm').addEventListener('submit', saveItem); } function openModal(item = null) { editingItemId = item?.id || null; document.getElementById('modalTitle').textContent = item ? 'Edit item' : 'Add item'; document.getElementById('modalSave').textContent = item ? 'Update' : 'Add'; const select = document.getElementById('itemCategory'); select.innerHTML = (menuData.categories || []) .filter(c => c.id !== 'all') .map(c => ``) .join(''); document.getElementById('itemId').value = item?.id || ''; document.getElementById('itemName').value = item?.name || ''; document.getElementById('itemPrice').value = item?.price || ''; document.getElementById('itemCategory').value = item?.category || ''; document.getElementById('itemDescription').value = item?.description || ''; document.getElementById('itemImage').value = (item?.image && item.image.startsWith('http')) ? item.image : ''; document.getElementById('itemAvailable').checked = item ? item.available : true; document.getElementById('itemFeatured').checked = item ? item.featured : false; document.getElementById('itemModal').classList.add('open'); setTimeout(() => document.getElementById('itemName').focus(), 150); } function closeModal() { document.getElementById('itemModal').classList.remove('open'); editingItemId = null; document.getElementById('itemForm').reset(); } async function saveItem(e) { e.preventDefault(); const id = document.getElementById('itemId').value; const item = { name: document.getElementById('itemName').value.trim(), price: parseFloat(document.getElementById('itemPrice').value), category: document.getElementById('itemCategory').value, description: document.getElementById('itemDescription').value.trim(), image: document.getElementById('itemImage').value.trim() || null, available: document.getElementById('itemAvailable').checked, featured: document.getElementById('itemFeatured').checked }; if (!item.name || isNaN(item.price) || !item.category) { return showToast('Fill in name, price, and category.', 'error'); } try { const url = id ? `/api/menu/${id}` : '/api/menu'; const method = id ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); if (!res.ok) throw new Error((await res.json()).error || 'Failed'); await loadMenuData(); renderTable(); closeModal(); showToast(id ? 'Item updated.' : 'Item added.', 'success'); } catch (err) { showToast(err.message || 'Error saving', 'error'); } } async function toggleItem(item) { try { const res = await fetch(`/api/menu/${item.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ available: !item.available }) }); if (!res.ok) throw new Error('Failed'); await loadMenuData(); renderTable(); showToast(`Marked as ${!item.available ? 'available' : 'sold out'}.`, 'success'); } catch { showToast('Error updating', 'error'); } } async function deleteItem(id) { if (!confirm('Delete this item? This cannot be undone.')) return; try { const res = await fetch(`/api/menu/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed'); await loadMenuData(); renderTable(); showToast('Item deleted.', 'success'); } catch { showToast('Error deleting', 'error'); } } // ── Categories ────────────────────────────────────────────────────────────── function renderCategories() { const container = document.getElementById('categoriesEditor'); const cats = menuData?.categories || []; container.innerHTML = cats.map((cat, i) => `
`).join(''); container.querySelectorAll('.cat-remove').forEach(btn => { btn.addEventListener('click', () => { menuData.categories.splice(parseInt(btn.dataset.index), 1); renderCategories(); }); }); document.getElementById('addCategoryBtn').onclick = () => { menuData.categories.push({ id: '', name: '', icon: '📌' }); renderCategories(); }; document.getElementById('saveCategoriesBtn').onclick = async () => { const rows = container.querySelectorAll('.cat-row'); const categories = []; rows.forEach(row => { const icon = row.querySelector('.cat-icon').value.trim(); const name = row.querySelector('.cat-name').value.trim(); if (name) categories.push({ id: name.toLowerCase().replace(/\s+/g, '-'), name, icon: icon || '📌' }); }); try { const res = await fetch('/api/categories', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ categories }) }); if (!res.ok) throw new Error('Failed'); await loadMenuData(); renderCategories(); showToast('Categories saved.', 'success'); } catch { showToast('Error saving', 'error'); } }; } // ── Cafe Info ─────────────────────────────────────────────────────────────── function renderInfo() { const info = menuData?.info || {}; document.getElementById('infoName').value = info.name || ''; document.getElementById('infoTagline').value = info.tagline || ''; document.getElementById('infoAddress').value = info.address || ''; document.getElementById('infoPhone').value = info.phone || ''; document.getElementById('infoEmail').value = info.email || ''; const hours = info.hours || []; const editor = document.getElementById('hoursEditor'); editor.innerHTML = hours.map((h, i) => `
`).join(''); editor.querySelectorAll('.hours-del').forEach(btn => { btn.addEventListener('click', () => { menuData.info.hours.splice(parseInt(btn.dataset.index), 1); renderInfo(); }); }); document.getElementById('addHoursBtn').onclick = () => { if (!menuData.info.hours) menuData.info.hours = []; menuData.info.hours.push({ days: '', time: '' }); renderInfo(); }; } // ── Forms ─────────────────────────────────────────────────────────────────── function setupForms() { document.getElementById('infoForm').addEventListener('submit', async (e) => { e.preventDefault(); const rows = document.querySelectorAll('#hoursEditor .hours-row'); const hours = []; rows.forEach(row => { const days = row.querySelector('.hours-days').value.trim(); const time = row.querySelector('.hours-time').value.trim(); if (days) hours.push({ days, time }); }); const info = { name: document.getElementById('infoName').value.trim(), tagline: document.getElementById('infoTagline').value.trim(), address: document.getElementById('infoAddress').value.trim(), phone: document.getElementById('infoPhone').value.trim(), email: document.getElementById('infoEmail').value.trim(), hours }; try { const res = await fetch('/api/info', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(info) }); if (!res.ok) throw new Error('Failed'); await loadMenuData(); showToast('Cafe info saved.', 'success'); } catch { showToast('Error saving', 'error'); } }); document.getElementById('passwordForm').addEventListener('submit', async (e) => { e.preventDefault(); const errEl = document.getElementById('passwordError'); const okEl = document.getElementById('passwordSuccess'); errEl.textContent = ''; okEl.textContent = ''; const current = document.getElementById('currentPassword').value; const newPw = document.getElementById('newPassword').value; const confirm = document.getElementById('confirmPassword').value; if (newPw !== confirm) { errEl.textContent = 'Passwords do not match.'; return; } if (newPw.length < 6) { errEl.textContent = 'At least 6 characters.'; return; } try { const check = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: current }) }); if (!(await check.json()).success) { errEl.textContent = 'Current password is incorrect.'; return; } const res = await fetch('/api/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ newPassword: newPw }) }); const data = await res.json(); if (data.success) { okEl.textContent = 'Password updated.'; document.getElementById('passwordForm').reset(); } else { errEl.textContent = data.error || 'Error updating.'; } } catch { errEl.textContent = 'Connection error.'; } }); } // ── Tables & Orders ──────────────────────────────────────────────────────── let tablesSetup = false; function setupTablesSection() { if (tablesSetup) { renderOrders(); return; } tablesSetup = true; document.getElementById('saveTablesBtn').addEventListener('click', saveTableCount); document.getElementById('loadQrBtn').addEventListener('click', loadQrCodes); document.getElementById('refreshOrdersBtn').addEventListener('click', () => { renderOrders(); }); // Set current table count document.getElementById('tableCount').value = menuData.tables || 0; renderOrders(); } async function saveTableCount() { const count = parseInt(document.getElementById('tableCount').value); if (isNaN(count) || count < 0 || count > 100) { return showToast('Enter 0–100', 'error'); } try { const res = await fetch('/api/tables', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count }) }); if (!res.ok) throw new Error('Failed'); menuData.tables = count; showToast(`Saved: ${count} tables`, 'success'); } catch { showToast('Error saving', 'error'); } } async function loadQrCodes() { const grid = document.getElementById('qrGrid'); grid.innerHTML = '

Generating QR codes…

'; try { const res = await fetch('/api/qr'); if (!res.ok) throw new Error('Failed'); const qrs = await res.json(); if (qrs.length === 0) { grid.innerHTML = '

No tables configured. Set the number above first.

'; return; } grid.innerHTML = qrs.map(q => `
QR for Table ${q.table}
Table ${q.table}
${esc(q.url)}
`).join(''); showToast(`${qrs.length} QR codes ready for print`, 'success'); } catch (err) { console.error(err); grid.innerHTML = '

Failed to generate QR codes.

'; } } async function renderOrders() { const container = document.getElementById('ordersContainer'); try { const res = await fetch('/api/orders'); if (!res.ok) throw new Error('Failed'); const orders = await res.json(); // Only show active orders (hide cancelled & paid) const activeOrders = orders.filter(o => o.status === 'pending' || o.status === 'served'); if (activeOrders.length === 0) { container.innerHTML = '

No active orders.

'; return; } const statusLabel = { pending: 'New', served: 'Served', paid: 'Paid', cancelled: 'Cancelled' }; const statusClass = { pending: 'badge-order-new', served: 'badge-order-served', paid: 'badge-order-paid', cancelled: 'badge-order-cancelled' }; container.innerHTML = activeOrders.map(o => `
Table ${o.table} ${timeAgo(o.createdAt)}
${statusLabel[o.status] || o.status}
${o.note ? `

📝 ${esc(o.note)}

` : ''}

Total: ₹${o.total.toFixed(0)}

${`
${o.status === 'pending' ? `` : ''} ${o.status === 'served' ? `` : ''} ${o.status !== 'paid' ? `` : ''}
`}
`).join(''); // Wire up action buttons container.querySelectorAll('.act-order').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; const status = btn.dataset.status; try { const res = await fetch(`/api/orders/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!res.ok) throw new Error('Failed'); showToast(`Order ${status}`, 'success'); renderOrders(); } catch { showToast('Error updating', 'error'); } }); }); } catch (err) { console.error(err); container.innerHTML = '

Failed to load orders.

'; } } function timeAgo(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`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; const d = new Date(iso); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } // ── Toast ─────────────────────────────────────────────────────────────────── function showToast(msg, type = 'success') { const container = document.getElementById('toastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `${type === 'success' ? '✓' : '✗'} ${msg}`; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.25s'; setTimeout(() => toast.remove(), 250); }, 2800); } // ── Utils ─────────────────────────────────────────────────────────────────── function esc(str) { const div = document.createElement('div'); div.textContent = str || ''; return div.innerHTML; } function adminSvgIcon(category) { const svgs = { 'coffee': '', 'tea': '', 'pastries': '', 'food': '' }; return svgs[category] || svgs['coffee']; }