/* ── Tracker page ── */ const TrackerPage = (() => { let currentYear, currentMonth; let trackerData = null; const MONTH_NAMES = [ 'January','February','March','April','May','June', 'July','August','September','October','November','December', ]; const STATUS_META = { paid: { label: 'Paid', cls: 'badge-paid' }, upcoming: { label: 'Upcoming', cls: 'badge-upcoming' }, due_soon: { label: 'Due Soon', cls: 'badge-due-soon' }, late: { label: 'Late', cls: 'badge-late' }, missed: { label: 'Missed', cls: 'badge-missed' }, autodraft: { label: 'Autodraft', cls: 'badge-autodraft' }, }; function fmt(amount) { return '$' + Number(amount || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function fmtDate(dateStr) { if (!dateStr) return ''; const [y, m, d] = dateStr.split('-'); return `${parseInt(m)}/${parseInt(d)}/${y}`; } function todayStr() { return new Date().toISOString().slice(0, 10); } function init(container) { const now = new Date(); currentYear = now.getFullYear(); currentMonth = now.getMonth() + 1; render(container); } function render(container) { container.innerHTML = `
Loading...
`; document.getElementById('month-label').textContent = `${MONTH_NAMES[currentMonth - 1]} ${currentYear}`; document.getElementById('prev-month').onclick = () => navigate(-1, container); document.getElementById('next-month').onclick = () => navigate(1, container); document.getElementById('today-btn').onclick = () => { const now = new Date(); currentYear = now.getFullYear(); currentMonth = now.getMonth() + 1; loadData(container); }; loadData(container); } function navigate(delta, container) { currentMonth += delta; if (currentMonth > 12) { currentMonth = 1; currentYear++; } if (currentMonth < 1) { currentMonth = 12; currentYear--; } document.getElementById('month-label').textContent = `${MONTH_NAMES[currentMonth - 1]} ${currentYear}`; loadData(container); } async function loadData(container) { document.getElementById('tracker-body').innerHTML = '
Loading...
'; try { trackerData = await API.tracker(currentYear, currentMonth); renderSummary(trackerData.summary); renderRows(trackerData.rows, container); } catch (e) { document.getElementById('tracker-body').innerHTML = `

Failed to load tracker: ${e.message}

`; } } function renderSummary(s) { document.getElementById('summary-bar').innerHTML = `
Total Expected
${fmt(s.total_expected)}
Total Paid
${fmt(s.total_paid)}
Remaining
${fmt(s.remaining)}
Overdue
${fmt(s.overdue)}
`; } function renderRows(rows, container) { const body = document.getElementById('tracker-body'); if (!rows || rows.length === 0) { body.innerHTML = `

No bills this month. Add a bill

`; return; } const first = rows.filter(r => r.bucket === '1st'); const second = rows.filter(r => r.bucket === '15th'); body.innerHTML = ''; if (first.length) body.appendChild(renderBucket('1st–14th', first)); if (second.length) body.appendChild(renderBucket('15th–31st', second)); // Attach event listeners after render attachTableListeners(container); } function renderBucket(label, rows) { const totalExpected = rows.reduce((s, r) => s + r.expected_amount, 0); const totalPaid = rows.reduce((s, r) => s + r.total_paid, 0); const section = document.createElement('div'); section.className = 'bucket-section'; section.innerHTML = `
${label} ${fmt(totalPaid)} / ${fmt(totalExpected)}
${rows.map(renderRow).join('')}
Bill Due Expected Amount Paid Paid Date Status
`; return section; } function renderRow(row) { const meta = STATUS_META[row.status] || STATUS_META.upcoming; const rowCls = `row-${row.status}`; const paidDate = row.last_paid_date ? fmtDate(row.last_paid_date) : ''; const paidAmt = row.total_paid > 0 ? fmt(row.total_paid) : ''; const mismatch = row.total_paid > 0 && row.total_paid !== row.expected_amount; const isPaid = row.status === 'paid' || row.status === 'autodraft'; const autopayDot = row.autopay_enabled ? `` : ''; return `
${autopayDot}
${escHtml(row.name)}
${row.category_name ? `
${escHtml(row.category_name)}
` : ''}
${fmtDate(row.due_date)}
${fmt(row.expected_amount)}
${paidAmt || '—'}
${paidDate || '—'}
${meta.label}
${!isPaid ? `
` : ''} ${row.payments && row.payments.length > 0 ? `` : ''}
`; } function attachTableListeners(container) { // Quick pay buttons — read amount from the sibling input container.querySelectorAll('.btn-quick-pay').forEach(btn => { btn.onclick = async (e) => { e.stopPropagation(); const billId = btn.dataset.billId; const amtInput = btn.closest('.quick-pay-group')?.querySelector('.quick-pay-amount'); const amount = amtInput ? parseFloat(amtInput.value) || 0 : 0; if (amount <= 0) { showToast('Enter a payment amount', 'error'); return; } btn.disabled = true; try { await API.quickPay({ bill_id: billId, amount, paid_date: todayStr() }); showToast('Marked as paid', 'success'); loadData(container); } catch (err) { showToast('Error: ' + err.message, 'error'); btn.disabled = false; } }; }); // Edit payment buttons container.querySelectorAll('.btn-edit-payment').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const payment = JSON.parse(btn.dataset.payment); openPaymentModal(payment, () => loadData(container)); }; }); // Inline editable amount cells container.querySelectorAll('.amount-cell').forEach(cell => { cell.onclick = () => startInlineEdit(cell, 'number', container); }); // Inline editable date cells container.querySelectorAll('.date-cell').forEach(cell => { cell.onclick = () => startInlineEdit(cell, 'date', container); }); } function startInlineEdit(cell, type, container) { if (cell.querySelector('input')) return; // already editing const billId = cell.dataset.billId; const field = cell.dataset.field; const row = trackerData?.rows?.find(r => r.id == billId); if (!row) return; let currentVal = ''; if (field === 'amount') currentVal = row.total_paid > 0 ? String(row.total_paid) : ''; if (field === 'date') currentVal = row.last_paid_date || ''; const input = document.createElement('input'); input.type = type === 'date' ? 'date' : 'number'; if (type === 'number') { input.step = '0.01'; input.min = '0'; } input.value = currentVal; input.style.cssText = 'width:100%;min-width:80px;'; const origText = cell.textContent.trim(); cell.textContent = ''; cell.appendChild(input); cell.classList.remove('empty'); input.focus(); input.select(); async function commit() { const val = input.value.trim(); if (!val) { cell.textContent = origText || '—'; cell.classList.add('empty'); return; } try { if (row.payments && row.payments.length > 0) { const p = row.payments[0]; const update = {}; if (field === 'amount') update.amount = parseFloat(val); if (field === 'date') update.paid_date = val; await API.updatePayment(p.id, update); } else { // Create new payment const paidDate = field === 'date' ? val : todayStr(); const amount = field === 'amount' ? parseFloat(val) : row.expected_amount; await API.createPayment({ bill_id: billId, amount, paid_date: paidDate }); } showToast('Saved', 'success'); loadData(container); } catch (err) { showToast('Error: ' + err.message, 'error'); cell.textContent = origText || '—'; } } input.addEventListener('blur', commit); input.addEventListener('keydown', e => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { cell.textContent = origText || '—'; if (!origText) cell.classList.add('empty'); } }); } function openPaymentModal(payment, onSave) { const modal = document.getElementById('payment-modal'); document.getElementById('payment-bill-id').value = payment.bill_id; document.getElementById('payment-id').value = payment.id; document.getElementById('payment-amount').value = payment.amount; document.getElementById('payment-date').value = payment.paid_date; document.getElementById('payment-method').value = payment.method || ''; document.getElementById('payment-notes').value = payment.notes || ''; document.getElementById('payment-modal-title').textContent = 'Edit Payment'; modal.classList.remove('hidden'); const close = () => modal.classList.add('hidden'); document.getElementById('payment-modal-close').onclick = close; document.getElementById('payment-modal-cancel').onclick = close; modal.querySelector('.modal-overlay').onclick = close; // ✅ UPDATED DELETE LOGIC WITH CLEAR INTENT document.getElementById('payment-delete').onclick = async () => { const confirmDelete = confirm( 'Remove this payment?\n\n' + '- The BILL will NOT be deleted\n' + '- This will remove the payment record\n' + '- The bill will become UNPAID\n\n' + 'Continue?' ); if (!confirmDelete) return; try { await API.deletePayment(payment.id); close(); showToast( 'Payment removed. Bill is now marked as unpaid.', 'success' ); onSave(); // refresh tracker } catch (e) { showToast('Error: ' + e.message, 'error'); } }; // normal save logic untouched document.getElementById('payment-form').onsubmit = async (e) => { e.preventDefault(); const data = { amount: parseFloat(document.getElementById('payment-amount').value), paid_date: document.getElementById('payment-date').value, method: document.getElementById('payment-method').value || null, notes: document.getElementById('payment-notes').value || null, }; try { await API.updatePayment(payment.id, data); close(); showToast('Payment saved', 'success'); onSave(); } catch (err) { showToast('Error: ' + err.message, 'error'); } }; } function escHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } return { init }; })();