← back to AbramsOS
views/savings.ejs
140 lines
<%- include('partials/header', { title: 'Savings' }) %>
<section class="page-head">
<div>
<h1>Savings & 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 & 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 }) %>