/* ClubLedger – main SPA */ let cashierMember = null; let barMember = null; let editMemberId = null; // --------------------------------------------------------------------------- // Boot // --------------------------------------------------------------------------- (async function init() { await loadConfig(); await loadStaffInto('cashierStaff'); await loadStaffInto('barStaff'); try { const data = await apiFetch('/staff'); renderStaffChips(data.staff); } catch (e) { /* ignore */ } // Nav document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.querySelectorAll('.view').forEach(v => v.classList.add('hidden')); document.getElementById('view-' + btn.dataset.view).classList.remove('hidden'); }); }); document.getElementById('registerForm').addEventListener('submit', async e => { e.preventDefault(); await registerMember(); }); document.getElementById('editForm').addEventListener('submit', async e => { e.preventDefault(); await saveEdit(); }); document.getElementById('memberSearch').addEventListener('keydown', e => { if (e.key === 'Enter') searchMembers(); }); document.getElementById('cashierSearch').addEventListener('keydown', e => { if (e.key === 'Enter') cashierSearchMembers(); }); document.getElementById('barSearch').addEventListener('keydown', e => { if (e.key === 'Enter') barSearchMembers(); }); document.getElementById('staffNameInput').addEventListener('keydown',e => { if (e.key === 'Enter') addStaff(); }); searchMembers(); })(); // --------------------------------------------------------------------------- // Members view // --------------------------------------------------------------------------- async function registerMember() { const number = document.getElementById('reg-number').value.trim(); const name = document.getElementById('reg-name').value.trim(); const pin = document.getElementById('reg-pin').value; if (!number || !name || !pin) { setMsg('registerMsg', 'All fields required.', 'err'); return; } try { const m = await apiFetch('/members', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ member_number: number, name, pin }) }); setMsg('registerMsg', `Registered: ${m.name} (#${m.member_number})`, 'ok'); document.getElementById('registerForm').reset(); searchMembers(); } catch (e) { setMsg('registerMsg', e.message, 'err'); } } async function searchMembers() { const q = document.getElementById('memberSearch').value.trim(); const url = q ? `/members?q=${encodeURIComponent(q)}` : '/members'; try { const members = await apiFetch(url); renderMemberTable(members); } catch (e) { console.error(e); } } function renderMemberTable(members) { const tbody = document.querySelector('#memberTable tbody'); if (!members.length) { tbody.innerHTML = 'No members found'; return; } tbody.innerHTML = members.map(m => ` ${esc(m.member_number)} ${esc(m.name)} ${esc(m.balance_display)} ${m.created_at ? m.created_at.slice(0, 10) : ''} Statement ${m.balance === 0 ? `` : ''} `).join(''); } // --------------------------------------------------------------------------- // Edit member // --------------------------------------------------------------------------- function openEditModal(id, name, number) { editMemberId = id; document.getElementById('edit-number').value = number; document.getElementById('edit-name').value = name; document.getElementById('edit-pin').value = ''; setMsg('editMsg', '', ''); document.getElementById('editModal').classList.remove('hidden'); document.getElementById('edit-name').focus(); } function closeEditModal() { editMemberId = null; document.getElementById('editModal').classList.add('hidden'); } async function saveEdit() { if (!editMemberId) return; const number = document.getElementById('edit-number').value.trim(); const name = document.getElementById('edit-name').value.trim(); const pin = document.getElementById('edit-pin').value; const body = { member_number: number, name }; if (pin) body.pin = pin; try { await apiFetch(`/members/${editMemberId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); closeEditModal(); searchMembers(); } catch (e) { setMsg('editMsg', e.message, 'err'); } } // --------------------------------------------------------------------------- // Delete member // --------------------------------------------------------------------------- async function deleteMember(id, name) { if (!confirm(`Delete member "${name}"?\n\nThis will permanently remove their account and transaction history.`)) return; try { await apiFetch(`/members/${id}`, { method: 'DELETE' }); searchMembers(); } catch (e) { alert(e.message); } } // --------------------------------------------------------------------------- // Cashier view // --------------------------------------------------------------------------- async function cashierSearchMembers() { const q = document.getElementById('cashierSearch').value.trim(); const url = q ? `/members?q=${encodeURIComponent(q)}` : '/members'; try { const members = await apiFetch(url); const list = document.getElementById('cashierMemberList'); list.innerHTML = members.map(m => `
${esc(m.name)}
#${esc(m.member_number)}
${esc(m.balance_display)}
`).join(''); } catch (e) { console.error(e); } } function selectCashierMember(id, name, number, balance, balanceDisplay) { cashierMember = { id, name, number }; document.getElementById('cashierMemberList').innerHTML = ''; document.getElementById('cashierSelected').innerHTML = `${esc(name)}   #${esc(number)}   Balance: ${esc(balanceDisplay)}`; document.getElementById('cashierForm').classList.remove('hidden'); setMsg('cashierMsg', '', ''); } function clearCashierSelection() { cashierMember = null; document.getElementById('cashierForm').classList.add('hidden'); document.getElementById('cashierAmount').value = ''; document.getElementById('cashierNote').value = ''; setMsg('cashierMsg', '', ''); } async function doTopup() { if (!cashierMember) return; const amount = parseInt(document.getElementById('cashierAmount').value, 10); const staff = document.getElementById('cashierStaff').value; const note = document.getElementById('cashierNote').value.trim(); if (!amount || isNaN(amount) || amount <= 0) { setMsg('cashierMsg', 'Enter a valid amount.', 'err'); return; } if (!staff) { setMsg('cashierMsg', 'Select a staff member.', 'err'); return; } try { const r = await apiFetch('/topup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ member_id: cashierMember.id, amount, staff_name: staff, note: note || null }) }); window.open(`/receipt/${r.entry_id}`, '_blank'); setMsg('cashierMsg', `Top-up complete. New balance: ${r.new_balance_display}`, 'ok'); clearCashierSelection(); } catch (e) { setMsg('cashierMsg', e.message, 'err'); } } // --------------------------------------------------------------------------- // Bar view // --------------------------------------------------------------------------- async function barSearchMembers() { const q = document.getElementById('barSearch').value.trim(); const url = q ? `/members?q=${encodeURIComponent(q)}` : '/members'; try { const members = await apiFetch(url); const list = document.getElementById('barMemberList'); list.innerHTML = members.map(m => `
${esc(m.name)}
#${esc(m.member_number)}
${esc(m.balance_display)}
`).join(''); } catch (e) { console.error(e); } } function selectBarMember(id, name, number, balance, balanceDisplay) { barMember = { id, name, number }; document.getElementById('barMemberList').innerHTML = ''; document.getElementById('barSelected').innerHTML = `${esc(name)}   #${esc(number)}   Balance: ${esc(balanceDisplay)}`; document.getElementById('barForm').classList.remove('hidden'); document.getElementById('barProductSearch').value = ''; document.getElementById('barProductResults').innerHTML = ''; setMsg('barMsg', '', ''); } function clearBarSelection() { barMember = null; document.getElementById('barForm').classList.add('hidden'); document.getElementById('barAmount').value = ''; document.getElementById('barPin').value = ''; document.getElementById('barNote').value = ''; document.getElementById('barProductSearch').value = ''; document.getElementById('barProductResults').innerHTML = ''; setMsg('barMsg', '', ''); } let productTimer = null; async function barProductLookup() { clearTimeout(productTimer); productTimer = setTimeout(async () => { const q = document.getElementById('barProductSearch').value.trim(); if (!q) { document.getElementById('barProductResults').innerHTML = ''; return; } try { const products = await apiFetch(`/products?q=${encodeURIComponent(q)}`); const div = document.getElementById('barProductResults'); if (!products.length) { div.innerHTML = '
No products found
'; return; } div.innerHTML = products.map(p => `
${esc(p.name)}${p.brand ? ` – ${esc(p.brand)}` : ''} ${p.search_tags ? `
${esc(p.search_tags)}
` : ''}
${esc(p.price_display)} ${p.member_price_display ? `mbr: ${esc(p.member_price_display)}` : ''}
`).join(''); } catch (e) { console.error(e); } }, 250); } function selectProduct(price, memberPrice, label) { document.getElementById('barAmount').value = memberPrice; document.getElementById('barNote').value = label; document.getElementById('barProductResults').innerHTML = ''; document.getElementById('barProductSearch').value = ''; } async function doCharge() { if (!barMember) return; const amount = parseInt(document.getElementById('barAmount').value, 10); const pin = document.getElementById('barPin').value; const staff = document.getElementById('barStaff').value; const note = document.getElementById('barNote').value.trim(); if (!amount || isNaN(amount) || amount <= 0) { setMsg('barMsg', 'Enter a valid amount.', 'err'); return; } if (!pin) { setMsg('barMsg', 'PIN required.', 'err'); return; } if (!staff) { setMsg('barMsg', 'Select a staff member.', 'err'); return; } try { const r = await apiFetch('/charge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ member_id: barMember.id, amount, pin, staff_name: staff, note: note || null }) }); window.open(`/receipt/${r.entry_id}`, '_blank'); setMsg('barMsg', `Charge complete. New balance: ${r.new_balance_display}`, 'ok'); clearBarSelection(); } catch (e) { setMsg('barMsg', e.message, 'err'); } }