diff --git a/web/dashboard.css b/web/dashboard.css index 50f4f6e..61e1c66 100644 --- a/web/dashboard.css +++ b/web/dashboard.css @@ -193,3 +193,23 @@ /* net pill */ .net-pos { color: var(--c-income); } .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; } diff --git a/web/dashboard.js b/web/dashboard.js index cc5d147..a3ebe7a 100644 --- a/web/dashboard.js +++ b/web/dashboard.js @@ -148,6 +148,7 @@

Top merchantssingle payees

Spending by monthCLP / month

+

Categories & merchantsclick to expand

`; 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) + '%', @@ -163,6 +164,50 @@ ml.appendChild(row); }); 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 `
+ ${esc(desc)} + ${d.txns.length}× + ${cpct}% + ${MT.CLPk(d.value)} +
`; + }).join('') + (descs.length > 30 ? `
+${descs.length - 30} more descriptions
` : ''); + section.innerHTML = ` +
+ + + ${esc(catName)} + ${catData.txns.length} txns + ${pct}% + ${MT.CLPk(catData.value)} +
+ `; + 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