const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; const itemsBody = $('#items-body'); const rowTemplate = $('#row-template'); const form = $('#payment-form'); const resultBox = $('#result'); const submitBtn = $('#submit-btn'); const currencySelect = $('#currency'); const money = (n, currency = currencySelect?.value || 'AUD') => new Intl.NumberFormat('en-US', { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: 2, }).format(Number(n) || 0); // ---------- Items table ---------- function addRow(data = {}) { const frag = rowTemplate.content.cloneNode(true); const row = frag.querySelector('tr'); if (data.sku) row.querySelector('.cell-sku').value = data.sku; if (data.name) row.querySelector('.cell-name').value = data.name; if (data.price != null) row.querySelector('.cell-price').value = data.price; if (data.quantity != null) row.querySelector('.cell-qty').value = data.quantity; if (data.condition) row.querySelector('.cell-condition').value = data.condition; row.querySelector('.btn-del').addEventListener('click', () => { row.remove(); if (itemsBody.children.length === 0) addRow(); recalc(); }); row.addEventListener('input', recalc); itemsBody.appendChild(row); recalc(); } function recalc() { let total = 0; $$('.item-row', itemsBody).forEach((row) => { const price = parseFloat(row.querySelector('.cell-price').value) || 0; const qty = parseFloat(row.querySelector('.cell-qty').value) || 0; const sub = price * qty; total += sub; row.querySelector('.cell-subtotal').textContent = money(sub); }); $('#grand-total').textContent = money(total); } function collectItems() { return $$('.item-row', itemsBody) .map((row) => ({ sku: row.querySelector('.cell-sku').value.trim(), name: row.querySelector('.cell-name').value.trim(), price: parseFloat(row.querySelector('.cell-price').value) || 0, quantity: parseFloat(row.querySelector('.cell-qty').value) || 0, condition: row.querySelector('.cell-condition').value.trim(), })) .filter((it) => it.sku || it.name || it.price > 0); } $('#add-row').addEventListener('click', () => addRow()); // Re-render totals when the currency changes. currencySelect.addEventListener('change', recalc); // ---------- Submit ---------- function setLoading(on) { submitBtn.disabled = on; $('.spinner', submitBtn).hidden = !on; $('.btn-label', submitBtn).textContent = on ? 'Creating...' : 'Create payment link'; } function showResult(type, html) { resultBox.hidden = false; resultBox.className = `result ${type}`; resultBox.innerHTML = html; } form.addEventListener('submit', async (e) => { e.preventDefault(); const customer = { customerName: $('#customerName').value.trim(), email: $('#email').value.trim(), phone: $('#phone').value.trim(), address: $('#address').value.trim(), }; // All customer fields are required. const CUSTOMER_LABELS = { customerName: 'Customer name', email: 'Email', phone: 'Phone number', address: 'Address', }; const missingCustomer = Object.keys(CUSTOMER_LABELS).filter((k) => !customer[k]); if (missingCustomer.length) { showResult('error', `Please fill in: ${missingCustomer.map((k) => CUSTOMER_LABELS[k]).join(', ')}.`); return; } // At least one item, each with SKU, price and quantity. const items = collectItems(); if (items.length === 0) { showResult('error', 'Please add at least one item.'); return; } for (let i = 0; i < items.length; i++) { const it = items[i]; const missing = []; if (!it.sku) missing.push('SKU'); if (!(it.price > 0)) missing.push('Price'); if (!(it.quantity > 0)) missing.push('Qty'); if (!it.condition) missing.push('Condition'); if (missing.length) { showResult('error', `Item ${i + 1}: ${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`); return; } } const payload = { ...customer, currency: currencySelect.value, items, }; setLoading(true); resultBox.hidden = true; try { const res = await fetch('/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to create link.'); // Open in a new tab window.open(data.link, '_blank', 'noopener'); showResult( 'success', `✓ Link created successfully!
` ); $('#copy-link')?.addEventListener('click', () => { navigator.clipboard.writeText(data.link); $('#copy-link').textContent = 'Copied'; }); loadHistory(); } catch (err) { showResult('error', `Error: ${err.message}`); } finally { setLoading(false); } }); // ---------- History ---------- async function loadHistory() { const list = $('#history-list'); try { const res = await fetch('/api/payments'); const data = await res.json(); const payments = data.payments || []; if (payments.length === 0) { list.innerHTML = 'No payments yet.
'; return; } list.innerHTML = payments.map(renderHistItem).join(''); } catch { list.innerHTML = 'Could not load history.
'; } } function renderHistItem(p) { let itemCount = 0; try { itemCount = JSON.parse(p.items_json || '[]').length; } catch { } const when = (p.created_at || '').replace('T', ' ').slice(0, 16); const hasLink = !!p.payment_link; const hint = hasLink ? `