Budget history by month, shared tab navigation, file imported statements into Statements/year/month and stop re-reading them
build-container / build (push) Successful in 40s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 15:52:05 -04:00
1 parent d7808a3925
commit 20317b14c9
7 files changed
+147 -12

No files matched your search

+25 -3
View File
@@ -25,11 +25,15 @@ input.amt{width:110px;text-align:right}
.pill{display:inline-block;padding:1px 9px;border-radius:99px;font-size:12px;font-weight:600}.pill.ok{background:var(--okbg);color:var(--ok)}.pill.bad{background:var(--badbg);color:var(--bad)}.pill.zero{background:var(--none);color:var(--mute)}
.alert{padding:10px 12px;border-radius:8px;margin:12px 0;background:var(--okbg);color:var(--ok)}.alert.bad{background:var(--badbg);color:var(--bad)}
#move{display:none;margin-top:12px}tr.empty td{color:var(--mute)}
.tabs{position:sticky;top:0;z-index:5;display:flex;gap:4px;margin:0 0 16px;padding:10px 0;background:var(--bg)}
.tabs a{flex:1;max-width:170px;text-align:center;padding:8px 14px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--mute);font-weight:600;font-size:14px;text-decoration:none}
.tabs a:hover{color:var(--ink)}.tabs a.on{background:var(--accent);border-color:var(--accent);color:#fff}
</style>
</head>
<body>
<main>
<div class="row"><h1>Envelope Budget</h1><div class="spacer"></div><a href="/dashboard">Dashboard</a><a href="/">Statement Loader</a></div>
<nav class="tabs"><a href="/">Statements</a><a href="/budget" class="on" aria-current="page">Budget</a><a href="/dashboard">Dashboard</a></nav>
<div class="row"><h1>Envelope Budget</h1><div class="spacer"></div></div>
<div class="row" style="margin:14px 0 0">
<button id="prev">‹</button><b id="mlabel" style="min-width:120px;text-align:center;font-size:16px"></b><button id="next">›</button>
<select id="mpick"></select>
@@ -58,6 +62,12 @@ input.amt{width:110px;text-align:right}
<table id="tbl"></table>
<p class="mute" style="margin:10px 0 0">Available = everything you’ve budgeted since the first budget month minus everything spent, so leftovers roll over and overspending carries forward. Ready to assign = income since then minus everything budgeted. Income paid in pieces (like the 15th and the last day)? Type the month’s full total in “Expecting”, and it counts as soon as you budget it. Aim for $0.</p>
</div>
<div class="card" id="histcard" style="margin-top:16px;display:none">
<div class="row" style="margin-bottom:6px"><h2 style="margin:0">History by month</h2><div class="spacer"></div>
<select id="hmetric"><option value="2">Left over (rolls forward)</option><option value="0">Budgeted</option><option value="1">Spent</option></select></div>
<div style="overflow-x:auto"><table id="hist"></table></div>
<p class="mute" style="margin:10px 0 0">Left over is what the envelope held at the end of that month: everything budgeted so far minus everything spent. It is worked out from the Budget tab and Bill Log, so it updates if you edit a past month.</p>
</div>
</main>
<script>
const TOKEN="__TOKEN__";
@@ -101,6 +111,18 @@ function softRender(active){ // update totals without losing focus in the input
const pos=active.selectionStart,i=active.dataset.i;render();
const n=document.querySelector(`input.amt[data-i="${i}"]`);if(n){n.focus();try{n.setSelectionRange(pos,pos)}catch(e){}}
}
let H=null;
async function loadHist(){try{H=await (await fetch("/api/budget_history")).json()}catch(e){H=null}renderHist()}
function renderHist(){
const c=$("#histcard");if(!H||!H.months.length){c.style.display="none";return}c.style.display="block";
const k=+$("#hmetric").value,ms=H.months.slice(-12),off=H.months.length-ms.length,cur=k===2;
const cell=v=>`<td class="n"><span style="${cur&&v<-0.005?"color:var(--bad)":cur&&v>0.005?"color:var(--ok)":""}">${Math.abs(v)<0.005?"–":money(v)}</span></td>`;
const tot=ms.map((_,i)=>H.envelopes.reduce((a,e)=>a+e.cells[off+i][k],0));
$("#hist").innerHTML="<tr><th>Envelope</th>"+ms.map(m=>`<th class="n">${MON[+m.slice(5)-1]} ${m.slice(2,4)}</th>`).join("")+"</tr>"+
H.envelopes.map(e=>`<tr><td>${esc(e.envelope)}</td>${ms.map((_,i)=>cell(e.cells[off+i][k])).join("")}</tr>`).join("")+
`<tr><td><b>Total</b></td>${tot.map(v=>`<td class="n"><b>${money(v)}</b></td>`).join("")}</tr>`;
}
$("#hmetric").onchange=renderHist;
function go(m){if(dirty&&!confirm("Discard unsaved changes?"))return;load(m)}
$("#prev").onclick=()=>go(add(month,-1));$("#next").onclick=()=>go(add(month,1));$("#mpick").onchange=e=>go(e.target.value);
$("#showall").onchange=render;
@@ -119,10 +141,10 @@ $("#mgo").onclick=()=>{const f=rows.find(r=>r.envelope==$("#mfrom").value),t=row
$("#save").onclick=async()=>{
const budgets={};rows.forEach(r=>{if(+r.edit)budgets[r.envelope]=+r.edit});if(exp>0)budgets["Expected income"]=exp;
try{const r=await fetch("/api/budget",{method:"POST",headers:{"X-Token":TOKEN},body:JSON.stringify({month,budgets})});const j=await r.json();if(!r.ok)throw new Error(j.error);
await load(month);$("#msg").innerHTML=`<div class="alert">${esc(j.message)}</div>`}catch(e){$("#msg").innerHTML=`<div class="alert bad">${esc(e.message)}</div>`}
await load(month);loadHist();$("#msg").innerHTML=`<div class="alert">${esc(j.message)}</div>`}catch(e){$("#msg").innerHTML=`<div class="alert bad">${esc(e.message)}</div>`}
};
addEventListener("beforeunload",e=>{if(dirty){e.preventDefault();e.returnValue=""}});
load(month);
load(month);loadHist();
</script>
</body>
</html>