← 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 }) %>