← back to AbramsOS

views/savings.ejs

140 lines

<%- include('partials/header', { title: 'Savings' }) %>

<section class="page-head">
  <div>
    <h1>Savings &amp; Smart Buys</h1>
    <p class="subtle">
      <%= suggestions.length %> ideas ·
      <% if (totalSavings) { %>est. USD <%= totalSavings.toFixed(2) %> potential savings<% } else { %>run the advisor to find cheaper &amp; better swaps<% } %>
      · <span class="subtle">prices are AI estimates — verify before buying</span>
    </p>
  </div>
  <div class="grid-controls">
    <label>Sort
      <select data-sort-for="savings">
        <option value="savings-desc">Biggest savings</option>
        <option value="confidence-desc">Most confident</option>
        <option value="created-desc">Newest</option>
        <option value="merchant-asc">Merchant A→Z</option>
      </select>
    </label>
    <label>Density
      <input data-density-for="savings" type="range" min="260" max="480" step="10" value="340">
    </label>
    <button type="button" class="btn primary" id="runAdvisor">Find savings</button>
  </div>
</section>

<% if (!suggestions.length) { %>
  <section class="empty glass">
    <p>No suggestions yet. Click <strong>Find savings</strong> — the advisor reads what you
    buy a lot (your <a href="/reorders">Reorders</a>) and proposes cheaper or better swaps,
    using a local AI model ($0).</p>
  </section>
<% } %>

<section data-grid="savings" class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));">
  <% suggestions.forEach(s => {
       const conf = Math.round((Number(s.confidence)||0) * 100);
       const created = new Date(s.created_at);
       const createdStr = created.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
  %>
    <article class="purchase glass<%= s.status==='saved' ? ' saved' : '' %>"
             data-id="<%= s.id %>"
             data-merchant="<%= (s.merchant||'').toLowerCase() %>"
             data-savings="<%= Number(s.est_savings)||0 %>"
             data-confidence="<%= Number(s.confidence)||0 %>"
             data-created="<%= created.toISOString() %>">
      <header>
        <h3><%= s.suggested_item %></h3>
        <span class="chip <%= s.kind==='upgrade' ? 'upgrade' : 'save' %>"><%= s.kind %></span>
      </header>

      <div class="amount">
        <% if (s.est_savings && Number(s.est_savings) > 0) { %>
          <span class="save-amt">save ~$<%= Number(s.est_savings).toFixed(2) %></span>
          <span class="subtle"><%= s.savings_basis || 'per order' %></span>
        <% } else if (s.savings_basis && /%/.test(s.savings_basis)) { %>
          <span class="save-amt"><%= s.savings_basis %></span>
        <% } else { %><span class="subtle">smart buy</span><% } %>
      </div>

      <dl class="meta">
        <% if (s.current_item) { %>
          <dt>Instead of</dt>
          <dd><%= s.current_item %><% if (s.current_price) { %> <span class="subtle">($<%= Number(s.current_price).toFixed(2) %>)</span><% } %></dd>
        <% } %>
        <% if (s.merchant) { %><dt>Where</dt><dd><%= s.merchant %></dd><% } %>
        <% if (s.quality_note) { %><dt>Quality</dt><dd><%= s.quality_note %></dd><% } %>
        <dt>Confidence</dt><dd><span class="badge<%= conf>=70?' amber':'' %>"><%= conf %>%</span></dd>
      </dl>

      <% if (s.rationale) { %><p class="rationale subtle"><%= s.rationale %></p><% } %>

      <footer class="card-foot">
        <span class="when" title="<%= created.toISOString() %>">🕓 <%= createdStr %></span>
        <span class="actions">
          <% if (s.status !== 'saved') { %><button type="button" class="btn tiny" data-act="save" data-id="<%= s.id %>">Save</button><% } %>
          <button type="button" class="btn tiny ghost" data-act="dismiss" data-id="<%= s.id %>">Dismiss</button>
        </span>
      </footer>
    </article>
  <% }) %>
</section>

<% if (coupons.length) { %>
  <section class="page-head" style="margin-top:2rem"><div><h2>Coupons at places you shop</h2></div></section>
  <section class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));">
    <% coupons.forEach(c => {
         const cCreated = new Date(c.created_at);
         const cStr = cCreated.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
    %>
      <article class="purchase glass">
        <header><h3><%= c.merchant %></h3><% if (c.discount_text) { %><span class="chip save"><%= c.discount_text %></span><% } %></header>
        <div class="amount"><%= c.title %></div>
        <dl class="meta">
          <% if (c.code) { %><dt>Code</dt><dd><code><%= c.code %></code></dd><% } %>
          <% if (c.expires_on) { %><dt>Expires</dt><dd><%= c.expires_on %></dd><% } %>
          <% if (c.source) { %><dt>Source</dt><dd class="subtle"><%= c.source %></dd><% } %>
        </dl>
        <footer class="card-foot">
          <span class="when" title="<%= cCreated.toISOString() %>">🕓 <%= cStr %></span>
          <% if (c.url) { %><a class="btn tiny" href="<%= c.url %>" target="_blank" rel="noopener noreferrer">Open</a><% } %>
        </footer>
      </article>
    <% }) %>
  </section>
<% } %>

<script>
  window.__csrf = '<%= csrfToken %>';
  async function post(url, body) {
    const r = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(Object.assign({ _csrf: window.__csrf }, body || {})),
    });
    return r.json();
  }
  document.getElementById('runAdvisor')?.addEventListener('click', async (e) => {
    const btn = e.currentTarget; btn.disabled = true; btn.textContent = 'Thinking…';
    try {
      const out = await post('/api/savings/run');
      btn.textContent = out.ok ? `+${out.created} found` : 'Error';
      if (out.ok && out.created) setTimeout(() => location.reload(), 700);
      else btn.disabled = false;
    } catch (_) { btn.textContent = 'Error'; btn.disabled = false; }
  });
  document.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', async (e) => {
    const el = e.currentTarget; const id = el.dataset.id;
    const status = el.dataset.act === 'save' ? 'saved' : 'dismissed';
    el.disabled = true;
    await post('/api/savings/' + id + '/status', { status });
    const card = el.closest('article');
    if (status === 'dismissed') { card.style.opacity = 0.35; card.remove(); }
    else { card.classList.add('saved'); el.remove(); }
  }));
</script>

<%- include('partials/footer', { gridScript: true }) %>