← back to AbramsOS
views/bills.ejs
130 lines
<%- include('partials/header', { title: 'Bills' }) %>
<section class="page-head">
<div>
<h1>Bills to Pay</h1>
<p class="subtle">
<%= bills.length %> tracked ·
<% if (monthlyBurden) { %>~USD <%= monthlyBurden.toFixed(2) %>/mo recurring<% } %>
· tax / government payments (e.g. CRA) live here — set Category to <em>tax</em> or <em>government</em>.
</p>
</div>
<div class="grid-controls">
<label>Sort
<select data-sort-for="bill">
<option value="due-asc">Due soonest</option>
<option value="due-desc">Due latest</option>
<option value="amount-desc">Amount ↓</option>
<option value="amount-asc">Amount ↑</option>
<option value="name-asc">Name A→Z</option>
<option value="category-asc">Category</option>
<option value="created-desc">Newest added</option>
</select>
</label>
<label>Density
<input data-density-for="bill" type="range" min="240" max="460" step="10" value="320">
</label>
<button type="button" class="btn" id="toggleAdd">+ Add bill</button>
</div>
</section>
<form id="addForm" class="add-form glass" hidden>
<div class="row">
<label>Name*<input name="name" required placeholder="Electric bill"></label>
<label>Payee<input name="payee" placeholder="LADWP / CRA / Landlord"></label>
<label>Category
<select name="category">
<% categories.forEach(c => { %><option value="<%= c %>"><%= c %></option><% }) %>
</select>
</label>
</div>
<div class="row">
<label>Amount<input name="amount" type="number" step="0.01" placeholder="0.00"></label>
<label>Currency<input name="currency" value="USD" size="4"></label>
<label>Cadence
<select name="cadence">
<% cadences.forEach(c => { %><option value="<%= c %>" <%= c==='monthly'?'selected':'' %>><%= c %></option><% }) %>
</select>
</label>
<label>Next due<input name="due_date" type="date"></label>
<label class="chk">Autopay<input name="autopay" type="checkbox"></label>
</div>
<div class="row">
<label>Account hint<input name="account_hint" placeholder="Visa ••1234 (never full number)"></label>
<label>Pay link<input name="url" type="url" placeholder="https://…"></label>
<label class="grow">Notes<input name="notes" placeholder="optional"></label>
</div>
<div class="row"><button type="submit" class="btn primary">Save bill</button></div>
</form>
<% if (!bills.length) { %>
<section class="empty glass"><p>No bills yet. Click <strong>+ Add bill</strong> to track your first one.</p></section>
<% } %>
<section data-grid="bill" class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));">
<% bills.forEach(b => { %>
<article class="purchase glass<%= b.is_overdue ? ' overdue' : '' %>"
data-name="<%= (b.name||'').toLowerCase() %>"
data-amount="<%= b.amount || 0 %>"
data-category="<%= b.category %>"
data-due="<%= b.due_date || '' %>"
data-created="<%= new Date(b.created_at).toISOString() %>">
<header>
<h3><%= b.name %></h3>
<span class="chip cat"><%= b.category %></span>
</header>
<div class="amount">
<% if (b.amount) { %><%= b.currency %> <%= Number(b.amount).toFixed(2) %><% } else { %><span class="subtle">amount unset</span><% } %>
<span class="subtle">/ <%= b.cadence %></span>
</div>
<dl class="meta">
<% if (b.payee) { %><dt>Payee</dt><dd><%= b.payee %></dd><% } %>
<dt>Due</dt>
<dd>
<% if (b.due_date) { %>
<%= b.due_date %>
<% if (b.is_overdue) { %><span class="badge red"><%= Math.abs(b.days_until) %>d overdue</span>
<% } else if (b.is_due_soon) { %><span class="badge amber">in <%= b.days_until %>d</span>
<% } else { %><span class="badge">in <%= b.days_until %>d</span><% } %>
<% } else { %><span class="subtle">no date</span><% } %>
</dd>
<% if (b.autopay) { %><dt>Autopay</dt><dd>✓ on</dd><% } %>
<% if (b.account_hint) { %><dt>Acct</dt><dd><%= b.account_hint %></dd><% } %>
<% if (b.last_paid_at) { %><dt>Last paid</dt><dd><%= b.last_paid_at %></dd><% } %>
<% if (b.url) { %><dt>Link</dt><dd><a href="<%= b.url %>" target="_blank" rel="noopener noreferrer">pay →</a></dd><% } %>
</dl>
<div class="when" title="<%= new Date(b.created_at).toISOString() %>">🕓 added <%= new Date(b.created_at).toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' }) %></div>
<div class="card-actions">
<button class="btn small" data-action="paid" data-id="<%= b.id %>">Mark paid</button>
<button class="btn small danger" data-action="delete" data-id="<%= b.id %>">Delete</button>
</div>
</article>
<% }) %>
</section>
<script>
const $ = (s, r = document) => r.querySelector(s);
$('#toggleAdd')?.addEventListener('click', () => { const f = $('#addForm'); f.hidden = !f.hidden; });
$('#addForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const body = Object.fromEntries(fd.entries());
body.autopay = fd.get('autopay') === 'on';
const res = await fetch('/api/bills', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) location.reload(); else alert('Save failed: ' + (await res.text()));
});
document.querySelectorAll('[data-action]').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id, action = btn.dataset.action;
if (action === 'delete' && !confirm('Delete this bill?')) return;
const url = action === 'paid' ? `/api/bills/${id}/paid` : `/api/bills/${id}/delete`;
const res = await fetch(url, { method: 'POST' });
if (res.ok) location.reload(); else alert('Action failed');
});
});
</script>
<%- include('partials/footer', { gridScript: true }) %>