BillTracker/legacy/js/bills.js

162 lines
6.7 KiB
JavaScript

/* ── 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, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
async function init(container) {
container.innerHTML = `<div class="loading">Loading...</div>`;
try {
[categories] = await Promise.all([API.categories()]);
render(container);
} catch (e) {
container.innerHTML = `<div class="empty-state"><p>Failed to load: ${e.message}</p></div>`;
}
}
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 = `
<div class="page-header">
<h1 class="page-title">Bills</h1>
<button class="btn btn-primary" id="add-bill-btn">+ Add Bill</button>
</div>
<div class="bills-grid" id="bills-list">
${active.map(b => renderCard(b)).join('')}
${inactive.length ? `
<div style="margin-top:24px">
<div class="bucket-label" style="margin-bottom:12px">INACTIVE</div>
${inactive.map(b => renderCard(b, true)).join('')}
</div>` : ''}
${bills.length === 0 ? `<div class="empty-state"><p>No bills yet. Add your first bill!</p></div>` : ''}
</div>
`;
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 `
<div class="bill-card ${inactive ? 'inactive' : ''}">
<div class="bill-card-info">
<div class="bill-card-name">${escHtml(bill.name)}</div>
<div class="bill-card-meta">
Day ${bill.due_day}
${catName ? ` · ${escHtml(catName)}` : ''}
· ${CYCLE_LABELS[bill.billing_cycle] || bill.billing_cycle}
${bill.autopay_enabled ? ' · <span style="color:var(--warning)">Autopay</span>' : ''}
</div>
</div>
<div class="bill-card-amount">${fmt(bill.expected_amount)}</div>
<div class="bill-card-actions">
<button class="btn btn-ghost btn-sm btn-edit-bill" data-id="${bill.id}">Edit</button>
<button class="btn btn-ghost btn-sm btn-toggle-bill"
data-id="${bill.id}" data-active="${bill.active}">
${bill.active ? 'Deactivate' : 'Activate'}
</button>
</div>
</div>
`;
}
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 = '<option value="">— none —</option>' +
categories.map(c => `<option value="${c.id}" ${bill?.category_id == c.id ? 'selected' : ''}>${escHtml(c.name)}</option>`).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 };
})();