Auto-categorize with built-in merchant rules, payee category editor, income/net on dashboard
build-container / build (push) Successful in 35s
build-container / build (push) Successful in 35s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6073279a1b
commit
837f89e563
7 files changed
+191
-17
No files matched your search
+13
-2
@@ -33,6 +33,8 @@ td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}.scroll{max-height:
|
||||
<label class="mute">Year <select id="year"></select></label><a href="/">← Statement Loader</a></div>
|
||||
<div class="kpis" id="kpis"></div>
|
||||
<div class="grid">
|
||||
<div class="card"><h2 id="t0">Income vs spending</h2><div id="c0"></div><div class="legend" id="l0"></div></div>
|
||||
<div class="card"><h2 id="tsrc">Income sources</h2><div class="scroll" id="inc"></div></div>
|
||||
<div class="card"><h2 id="t1">Monthly total</h2><div id="c1"></div></div>
|
||||
<div class="card"><h2>Monthly spending by category</h2><div id="c2"></div><div class="legend" id="l2"></div></div>
|
||||
<div class="card"><h2>Year over year by month</h2><div id="c3"></div><div class="legend" id="l3"></div></div>
|
||||
@@ -50,7 +52,7 @@ td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}.scroll{max-height:
|
||||
<select id="fc"><option value="">All categories</option></select><select id="fa"><option value="">All accounts</option></select></div>
|
||||
<div class="mute" id="count" style="margin-bottom:6px"></div><div class="scroll" id="tx"></div>
|
||||
</div>
|
||||
<p class="mute">Read-only view of Bills_Tracker.xlsx. Card payments and transfers are not included (see Excluded Transfers in the workbook). Refunds are negative amounts.</p>
|
||||
<p class="mute">Read-only view of Bills_Tracker.xlsx. Income = paychecks and interest. Card payments and transfers between your accounts are in neither income nor spending (see Excluded Transfers in the workbook). Refunds are negative amounts.</p>
|
||||
</main>
|
||||
<div id="tip"></div>
|
||||
<script>
|
||||
@@ -111,13 +113,22 @@ function render(){
|
||||
const topc=Object.entries(cats).sort((a,b)=>b[1]-a[1]);
|
||||
const lastM=cur.length?Math.max(...cur.map(t=>t.m)):0, prev=all.filter(t=>t.y==Y-1&&t.m<=lastM), curYtd=total;
|
||||
const prevT=sum(prev,t=>t.amt), chg=prevT?(curYtd/prevT-1):null;
|
||||
const inc=(D.income||[]).map(i=>({d:i[0],y:+i[0].slice(0,4),m:+i[0].slice(5,7),src:i[1],kind:i[2],amt:i[3]})).filter(i=>i.y==Y);
|
||||
const incM=Array(12).fill(0);inc.forEach(i=>incM[i.m-1]+=i.amt);const incT=sum(incM,x=>x),net=incT-total,rate=incT>0?net/incT:null;
|
||||
$("#t0").textContent=`Income vs spending · ${Y}`;$("#tsrc").textContent=`Income sources · ${Y}`;
|
||||
$("#kpis").innerHTML=[
|
||||
["Total paid",money(total),`${cur.length} payments`],
|
||||
["Income",money(incT),`${inc.length} deposits`],
|
||||
["Total paid (spending)",money(total),`${cur.length} payments`],
|
||||
["Net (income − spending)",`<span class="${net<0?"up":"down"}">${net<0?"-":"+"}${money(Math.abs(net))}</span>`,rate==null?"":`${(rate*100).toFixed(0)}% of income ${net<0?"overspent":"saved"}`],
|
||||
["Average per month",money(active?total/active:0),`${active} month(s) with data`],
|
||||
["Highest month",hi>0?MON[byM.indexOf(hi)]:"-",hi>0?money(hi):""],
|
||||
["Largest category",topc[0]?topc[0][0]:"-",topc[0]?money(topc[0][1]):""],
|
||||
[`${Y} vs ${Y-1}, same months`,chg==null?"n/a":`<span class="${chg>0?"up":"down"}">${chg>0?"+":""}${(chg*100).toFixed(1)}%</span>`,prevT?`${money(curYtd)} vs ${money(prevT)} (Jan–${MON[lastM-1]})`:`no ${Y-1} data`]
|
||||
].map(k=>`<div class="card kpi"><div class="l">${k[0]}</div><div class="v">${k[1]}</div><div class="s">${k[2]}</div></div>`).join("");
|
||||
const iser=[{name:"Income",color:PAL[4],values:incM},{name:"Spending",color:PAL[2],values:byM.map(v=>Math.max(0,v))}];
|
||||
chart("#c0",MON,iser,"group");legend("#l0",iser);
|
||||
const sm={};inc.forEach(i=>{(sm[i.src]=sm[i.src]||{amt:0,n:0,kind:i.kind});sm[i.src].amt+=i.amt;sm[i.src].n++});
|
||||
$("#inc").innerHTML=inc.length?"<table><tr><th>Source</th><th>Type</th><th class='n'>Deposits</th><th class='n'>Total</th></tr>"+Object.entries(sm).sort((a,b)=>b[1].amt-a[1].amt).map(([k,v])=>`<tr><td>${esc(k)}</td><td class="mute">${v.kind}</td><td class="n">${v.n}</td><td class="n">${money(v.amt,2)}</td></tr>`).join("")+`<tr><td><b>Total</b></td><td></td><td class="n">${inc.length}</td><td class="n"><b>${money(incT,2)}</b></td></tr></table>`:'<div class="mute">No income recorded for this year. Paychecks and interest are read from the Excluded Transfers tab.</div>';
|
||||
chart("#c1",MON,[{name:"Total",color:PAL[0],values:byM}],"group");
|
||||
// categories: top 7 + Other
|
||||
const top=topc.slice(0,7).map(c=>c[0]);const col=c=>PAL[top.indexOf(c)>=0?top.indexOf(c):7];
|
||||
|
||||
Reference in new issue
Block a user