Add category/merchant breakdown accordion to Spending tab

This commit is contained in:
Kavi 2026-06-02 04:15:18 -04:00
parent a2cb7d3700
commit 948736c79a
2 changed files with 65 additions and 0 deletions

View File

@ -193,3 +193,23 @@
/* net pill */ /* net pill */
.net-pos { color: var(--c-income); } .net-pos { color: var(--c-income); }
.net-neg { color: var(--c-spend); } .net-neg { color: var(--c-spend); }
/* ---- category breakdown accordion ---- */
.bk-section { border-bottom: 1px solid var(--line); }
.bk-section:last-child { border-bottom: 0; }
.bk-header { display: flex; align-items: center; gap: 10px; padding: 11px 14px; cursor: pointer; user-select: none; }
.bk-header:hover { background: rgba(255,255,255,0.025); }
.bk-toggle { font-size: 9px; color: var(--ink-dim); width: 10px; flex-shrink: 0; }
.bk-dot { width: 9px; height: 9px; border-radius: 3px; flex-shrink: 0; }
.bk-name { flex: 1; font-size: 13.5px; color: var(--ink); }
.bk-meta { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); }
.bk-pct { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); width: 30px; text-align: right; }
.bk-amt { font-size: 13px; font-family: var(--font-num); color: var(--ink); min-width: 72px; text-align: right; }
.bk-rows { padding: 2px 14px 10px; border-top: 1px solid var(--line); background: rgba(0,0,0,0.18); }
.bk-row { display: grid; grid-template-columns: 1fr 34px 34px 80px; gap: 10px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
.bk-row:last-child { border-bottom: 0; }
.bk-row-desc { font-size: 12px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 19px; font-family: var(--font-num); }
.bk-row-cnt { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); text-align: right; }
.bk-row-pct { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); text-align: right; }
.bk-row-amt { font-size: 12px; color: var(--ink); font-family: var(--font-num); text-align: right; }
.bk-more { font-size: 11px; color: var(--ink-dim); padding: 7px 0 3px 19px; }

View File

@ -148,6 +148,7 @@
<div class="card"><h3>Top merchants<span class="pill">single payees</span></h3><div class="mlist" id="sp-merch"></div></div> <div class="card"><h3>Top merchants<span class="pill">single payees</span></h3><div class="mlist" id="sp-merch"></div></div>
</div> </div>
<div class="card" style="margin-top:16px;"><h3>Spending by month<span class="pill">CLP / month</span></h3><div class="chart-host" id="sp-month"></div></div> <div class="card" style="margin-top:16px;"><h3>Spending by month<span class="pill">CLP / month</span></h3><div class="chart-host" id="sp-month"></div></div>
<div class="card" style="margin-top:16px;"><h3>Categories &amp; merchants<span class="pill">click to expand</span></h3><div id="sp-breakdown"></div></div>
`; `;
C.hbars($('#sp-cats'), cats.map(([label, d], i) => ({ C.hbars($('#sp-cats'), cats.map(([label, d], i) => ({
label, value: d.value, color: catColor(i), sub: d.txns.length + ' txns · ' + Math.round(d.value / total * 100) + '%', label, value: d.value, color: catColor(i), sub: d.txns.length + ' txns · ' + Math.round(d.value / total * 100) + '%',
@ -163,6 +164,50 @@
ml.appendChild(row); ml.appendChild(row);
}); });
C.monthlyBars($('#sp-month'), months, [{ key: 'sp', label: 'Spending', color: col.spend(), values: spendByM }], { height: 200 }); C.monthlyBars($('#sp-month'), months, [{ key: 'sp', label: 'Spending', color: col.spend(), values: spendByM }], { height: 200 });
renderCategoryBreakdown($('#sp-breakdown'), cats, catColor, total);
}
function renderCategoryBreakdown(host, cats, catColor, total) {
const MT = window.MT;
host.innerHTML = '';
cats.forEach(([catName, catData], i) => {
const color = catColor(i);
const byDesc = groupSum(catData.txns, t => MT.cleanDesc(t.description) || t.description || '—');
const descs = sortEntries(byDesc);
const pct = Math.round(catData.value / total * 100);
const section = document.createElement('div');
section.className = 'bk-section';
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
const cpct = Math.round(d.value / catData.value * 100);
return `<div class="bk-row">
<span class="bk-row-desc">${esc(desc)}</span>
<span class="bk-row-cnt">${d.txns.length}×</span>
<span class="bk-row-pct">${cpct}%</span>
<span class="bk-row-amt">${MT.CLPk(d.value)}</span>
</div>`;
}).join('') + (descs.length > 30 ? `<div class="bk-more">+${descs.length - 30} more descriptions</div>` : '');
section.innerHTML = `
<div class="bk-header">
<span class="bk-toggle"></span>
<span class="bk-dot" style="background:${color}"></span>
<span class="bk-name">${esc(catName)}</span>
<span class="bk-meta">${catData.txns.length} txns</span>
<span class="bk-pct">${pct}%</span>
<span class="bk-amt">${MT.CLPk(catData.value)}</span>
</div>
<div class="bk-rows" hidden>${rowsHtml}</div>`;
const header = section.querySelector('.bk-header');
const rows = section.querySelector('.bk-rows');
const toggle = section.querySelector('.bk-toggle');
header.addEventListener('click', () => {
const open = !rows.hidden;
rows.hidden = open;
toggle.textContent = open ? '▶' : '▼';
});
// expand biggest category by default
if (i === 0) { rows.hidden = false; toggle.textContent = '▼'; }
host.appendChild(section);
});
} }
// ============================================================ TAB 3: INCOME // ============================================================ TAB 3: INCOME