Auto-categorize with built-in merchant rules, payee category editor, income/net on dashboard
build-container / build (push) Successful in 35s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 13:17:42 -04:00
1 parent 6073279a1b
commit 837f89e563
7 files changed
+191 -17

No files matched your search

+21 -2
View File
@@ -53,6 +53,12 @@ th{font-size:12px;color:var(--mute);font-weight:600}td.num{text-align:right;font
<div id="summary" class="mute" style="margin:10px 0"></div>
<div class="grid"><table id="preview"></table></div>
</div>
<div class="card" id="catcard">
<div class="row"><h2 style="margin:0">4 · Categorize payees</h2><div class="spacer"></div>
<button id="auto">Auto-categorize</button><button class="primary" id="savecats" disabled>Save changes</button></div>
<div id="catsum" class="mute" style="margin:10px 0"></div>
<div class="grid" style="max-height:420px;overflow:auto"><table id="cattbl"></table></div>
</div>
</main>
<script>
const TOKEN="__TOKEN__";
@@ -63,11 +69,12 @@ const mon=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","De
async function post(path,body,extra){const r=await fetch(path,{method:"POST",headers:{"X-Token":TOKEN,...(extra||{})},body});const j=await r.json();if(!r.ok)throw new Error(j.error||"Request failed");return j}
function alertBox(kind,html){$("#alerts").insertAdjacentHTML("beforeend",`<div class="alert ${kind}">${html}</div>`)}
async function refresh(msg){
if($("#catcard"))$("#catcard").style.display="";
$("#alerts").innerHTML=msg?`<div class="alert good">${msg}</div>`:"";
$("#coverage").innerHTML="Reading statements…";
const r=await fetch("/api/state");const s=await r.json();
if(s.error){alertBox("bad",esc(s.error));return}
if(s.setup){$("#coverage").innerHTML="";$("#preview").innerHTML="";$("#import").disabled=true;
if(s.setup){$("#catcard").style.display="none";$("#coverage").innerHTML="";$("#preview").innerHTML="";$("#import").disabled=true;
$("#summary").innerHTML="";alertBox("warn",`<b>First-time setup:</b> <code>Bills_Tracker.xlsx</code> isn’t in the data folder (<code>${esc(s.data_dir)}</code>). Drop your existing one into the box above, or <button id="blank" style="margin-left:6px">Create a blank workbook</button>`+(s.rules?"":" Also add <code>loader_rules.json</code> (see loader_rules.example.json: your name, so transfers between your own accounts aren’t counted as spending)."));
const b=$("#blank");if(b)b.onclick=async()=>{try{const j=await post("/api/create_blank","");await refresh(esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};return}
s.bad.forEach(b=>alertBox("bad",`<b>${esc(b.file)}</b>: the transactions I read add up to ${fmt(b.parsed)} but the statement says ${fmt(b.stated)}. It will <b>not</b> be imported until that matches.`));
@@ -101,7 +108,19 @@ $("#import").onclick=async()=>{
};
$("#undo").onclick=async()=>{if(!confirm("Restore the workbook from the most recent backup? This discards the last import."))return;try{const j=await post("/api/undo","");await refresh(esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
$("#open").onclick=async()=>{try{await post("/api/open","")}catch(e){alertBox("bad",esc(e.message))}};
refresh();
let CATS=null,EDITS={};
async function loadPayees(){
const r=await fetch("/api/payees");const j=await r.json();if(j.error){$("#catsum").textContent=j.error;return}
CATS=j;EDITS={};$("#savecats").disabled=true;
$("#catsum").textContent=j.payees.length?`${j.payees.length} payee(s) are still “Other / Unsorted”. Pick a category (and Bill or Spending) for each, or try Auto-categorize first.`:"Every payee has a category.";
const opt=(list,sel)=>list.map(c=>`<option${c==sel?" selected":""}>${esc(c)}</option>`).join("");
$("#cattbl").innerHTML=j.payees.length?"<tr><th>Payee</th><th class='num'>Payments</th><th class='num'>Total</th><th>Category</th><th>Type</th></tr>"+j.payees.map((p,i)=>`<tr><td>${esc(p.payee)}</td><td class="num">${p.n}</td><td class="num">${fmt(p.total)}</td><td><select data-i="${i}" data-k="category">${opt(j.categories,p.category)}</select></td><td><select data-i="${i}" data-k="type">${opt(j.types,p.type)}</select></td></tr>`).join(""):"";
document.querySelectorAll("#cattbl select").forEach(sel=>sel.onchange=()=>{const p=j.payees[+sel.dataset.i];(EDITS[p.payee]=EDITS[p.payee]||{payee:p.payee,category:p.category,type:p.type})[sel.dataset.k]=sel.value;$("#savecats").disabled=false});
}
$("#auto").onclick=async()=>{try{const j=await post("/api/autocategorize","");await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
$("#savecats").onclick=async()=>{try{const j=await post("/api/set_categories",JSON.stringify(Object.values(EDITS)));await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
refresh();loadPayees();
</script>
</body>
</html>