Add category/merchant breakdown accordion to Spending tab
This commit is contained in:
parent
a2cb7d3700
commit
948736c79a
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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 & 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue