← back to AbramsOS
views/assets.ejs
117 lines
<%- include('partials/header', { title: 'Assets' }) %>
<% const fmt = (d) => new Date(d).toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
const money = (n) => (n == null ? '—' : '$' + Number(n).toLocaleString(undefined, { maximumFractionDigits: 0 }));
const kindIcon = { property:'🏠', vehicle:'🚗', account:'🏦', valuable:'💎', other:'📦' }; %>
<section class="page-head">
<div>
<h1>Assets & Net Worth</h1>
<p class="subtle">
<% if (netWorth) { %>Net worth (entered): <strong><%= money(netWorth) %></strong> · <% } %>
<%= assets.length %> asset<%= assets.length===1?'':'s' %> · values you enter
</p>
</div>
<div class="grid-controls">
<button type="button" class="btn primary" id="toggleAdd">+ Add asset</button>
</div>
</section>
<% if (Object.keys(byCategory).length) { %>
<section class="stat-row">
<% Object.entries(byCategory).forEach(([c, v]) => { %>
<div class="stat glass"><div class="num" style="font-size:1.4rem"><%= money(v) %></div><div class="label"><%= c.replace(/_/g,' ') %></div></div>
<% }) %>
</section>
<% } %>
<form id="addForm" class="add-form glass" hidden>
<div class="row">
<label>Type
<select name="kind" id="kindSel">
<option value="property">🏠 Property</option>
<option value="vehicle">🚗 Vehicle</option>
<option value="account">🏦 Account</option>
<option value="valuable">💎 Valuable</option>
<option value="other">📦 Other</option>
</select>
</label>
<label class="grow">Name*<input name="name" required placeholder="Primary residence"></label>
<label>Category<input name="category" placeholder="real_estate" list="catList"></label>
<datalist id="catList"><option value="real_estate"><option value="vehicle"><option value="cash"><option value="investment"><option value="retirement"><option value="other"></datalist>
</div>
<div class="row addr-row">
<label class="grow">Address<input name="address" placeholder="123 Main St, City, ST 91436"></label>
</div>
<div class="row">
<label>Current value ($)<input name="current_value" type="number" step="0.01" placeholder="0"></label>
<label>As of<input name="valued_at" type="date"></label>
</div>
<div class="row">
<label class="grow">Details<input name="details" placeholder="4 bed / 3 bath, 2,400 sqft"></label>
</div>
<div class="row">
<label class="grow">Notes<input name="notes" placeholder="optional"></label>
<button type="submit" class="btn primary">Save asset</button>
</div>
</form>
<% if (!assets.length) { %>
<section class="empty glass">
<p>No assets yet. Click <strong>+ Add asset</strong> to enter your home (by address), a vehicle, or an account with its current value and a finance category. Property assets link to <a href="/neighborhood">Neighborhood Watch</a>.</p>
</section>
<% } %>
<section class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));">
<% assets.forEach(a => { %>
<article class="purchase glass" data-id="<%= a.id %>">
<header>
<h3><%= kindIcon[a.kind] || '📦' %> <%= a.name %></h3>
<% if (a.category) { %><span class="chip" style="background:rgba(128,128,128,.15)"><%= a.category.replace(/_/g,' ') %></span><% } %>
</header>
<div class="amount"><%= money(a.current_value) %><% if (a.valued_at) { %> <span class="subtle">as of <%= new Date(a.valued_at).toLocaleDateString() %></span><% } %></div>
<dl class="meta">
<% if (a.address) { %><dt>Address</dt><dd><%= a.address %></dd><% } %>
<% if (a.details) { %><dt>Details</dt><dd><%= a.details %></dd><% } %>
<% if (a.notes) { %><dt>Notes</dt><dd class="subtle"><%= a.notes %></dd><% } %>
</dl>
<footer class="card-foot">
<span class="when" title="<%= new Date(a.created_at).toISOString() %>">🕓 <%= fmt(a.created_at) %></span>
<span class="actions">
<% if (a.kind === 'property' && a.address) { %>
<button type="button" class="btn tiny" data-estimate="<%= a.id %>" title="<%= valuationConfigured ? 'Estimate value via RentCast' : 'Add RENTCAST_API_KEY to enable' %>">$ Estimate</button>
<a class="btn tiny" href="/neighborhood?asset=<%= a.id %>">🛡 Watch</a>
<% } %>
<button type="button" class="btn tiny ghost" data-del="<%= a.id %>">Remove</button>
</span>
</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(); }
const kindSel=document.getElementById('kindSel');
function syncAddr(){ document.querySelector('.addr-row').style.display = (kindSel.value==='property')?'':'none'; }
kindSel?.addEventListener('change', syncAddr); if(kindSel) syncAddr();
document.getElementById('toggleAdd')?.addEventListener('click', ()=>{ const f=document.getElementById('addForm'); f.hidden=!f.hidden; });
document.getElementById('addForm')?.addEventListener('submit', async (e)=>{
e.preventDefault(); const fd=Object.fromEntries(new FormData(e.target).entries());
const out=await post('/api/assets', fd); if(out.ok) location.reload(); else alert(out.error||'error');
});
document.querySelectorAll('[data-del]').forEach(b=>b.addEventListener('click', async (e)=>{
if(!confirm('Remove this asset?')) return;
await post('/api/assets/'+e.currentTarget.dataset.del+'/delete'); e.currentTarget.closest('article').remove();
}));
document.querySelectorAll('[data-estimate]').forEach(b=>b.addEventListener('click', async (e)=>{
const btn=e.currentTarget; btn.disabled=true; const t=btn.textContent; btn.textContent='…';
const out=await post('/api/assets/'+btn.dataset.estimate+'/estimate');
if(out.ok){ btn.textContent='$'+out.price.toLocaleString(); setTimeout(()=>location.reload(),900); }
else if(out.reason==='no_key'){ alert('Add RENTCAST_API_KEY (free at rentcast.io) via the secrets skill to enable estimates.'); btn.textContent=t; btn.disabled=false; }
else { alert('Estimate unavailable: '+(out.reason||out.error||'?')); btn.textContent=t; btn.disabled=false; }
}));
</script>
<%- include('partials/footer', {}) %>