/* ── Bills management page ── */
const BillsPage = (() => {
let categories = [];
const CYCLE_LABELS = {
monthly: 'Monthly', quarterly: 'Quarterly',
annually: 'Annually', irregular: 'Irregular',
};
function fmt(amount) {
return '$' + Number(amount || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function escHtml(str) {
return String(str || '')
.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
async function init(container) {
container.innerHTML = `
Loading...
`;
try {
[categories] = await Promise.all([API.categories()]);
render(container);
} catch (e) {
container.innerHTML = `Failed to load: ${e.message}
`;
}
}
async function render(container) {
const bills = await API.allBills();
const active = bills.filter(b => b.active);
const inactive = bills.filter(b => !b.active);
container.innerHTML = `
${active.map(b => renderCard(b)).join('')}
${inactive.length ? `
INACTIVE
${inactive.map(b => renderCard(b, true)).join('')}
` : ''}
${bills.length === 0 ? `
No bills yet. Add your first bill!
` : ''}
`;
document.getElementById('add-bill-btn').onclick = () => openBillModal(null, () => render(container));
container.querySelectorAll('.btn-edit-bill').forEach(btn => {
btn.onclick = async () => {
const bill = await API.bill(btn.dataset.id);
openBillModal(bill, () => render(container));
};
});
container.querySelectorAll('.btn-toggle-bill').forEach(btn => {
btn.onclick = async () => {
const active = btn.dataset.active === '1';
if (!active || confirm('Deactivate this bill? It will be hidden from the tracker.')) {
await API.updateBill(btn.dataset.id, { active: active ? 0 : 1 });
render(container);
}
};
});
}
function renderCard(bill, inactive = false) {
const catName = categories.find(c => c.id === bill.category_id)?.name || '';
return `
${escHtml(bill.name)}
Day ${bill.due_day}
${catName ? ` · ${escHtml(catName)}` : ''}
· ${CYCLE_LABELS[bill.billing_cycle] || bill.billing_cycle}
${bill.autopay_enabled ? ' · Autopay' : ''}
${fmt(bill.expected_amount)}
`;
}
function openBillModal(bill, onSave) {
const modal = document.getElementById('bill-modal');
const isNew = !bill;
document.getElementById('bill-modal-title').textContent = isNew ? 'Add Bill' : 'Edit Bill';
document.getElementById('bill-id').value = bill?.id || '';
document.getElementById('bill-name').value = bill?.name || '';
document.getElementById('bill-due-day').value = bill?.due_day || '';
document.getElementById('bill-expected').value = bill?.expected_amount || '';
document.getElementById('bill-interest-rate').value = bill?.interest_rate ?? '';
document.getElementById('bill-cycle').value = bill?.billing_cycle || 'monthly';
document.getElementById('bill-autopay').checked = !!bill?.autopay_enabled;
document.getElementById('bill-2fa').checked = !!bill?.has_2fa;
document.getElementById('bill-website').value = bill?.website || '';
document.getElementById('bill-username').value = bill?.username || '';
document.getElementById('bill-account-info').value = bill?.account_info || '';
document.getElementById('bill-notes').value = bill?.notes || '';
// Populate category select
const catSelect = document.getElementById('bill-category');
catSelect.innerHTML = '' +
categories.map(c => ``).join('');
modal.classList.remove('hidden');
const close = () => modal.classList.add('hidden');
document.getElementById('bill-modal-close').onclick = close;
document.getElementById('bill-modal-cancel').onclick = close;
modal.querySelector('.modal-overlay').onclick = close;
document.getElementById('bill-form').onsubmit = async (e) => {
e.preventDefault();
const data = {
name: document.getElementById('bill-name').value.trim(),
category_id: document.getElementById('bill-category').value || null,
due_day: parseInt(document.getElementById('bill-due-day').value, 10),
expected_amount: parseFloat(document.getElementById('bill-expected').value) || 0,
interest_rate: document.getElementById('bill-interest-rate').value === '' ? null : parseFloat(document.getElementById('bill-interest-rate').value),
billing_cycle: document.getElementById('bill-cycle').value,
autopay_enabled: document.getElementById('bill-autopay').checked,
has_2fa: document.getElementById('bill-2fa').checked,
website: document.getElementById('bill-website').value || null,
username: document.getElementById('bill-username').value || null,
account_info: document.getElementById('bill-account-info').value || null,
notes: document.getElementById('bill-notes').value || null,
};
try {
if (isNew) {
await API.createBill(data);
showToast('Bill added', 'success');
} else {
await API.updateBill(bill.id, data);
showToast('Bill updated', 'success');
}
close();
onSave();
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
};
}
return { init };
})();