diff --git a/BACKLOG.md b/BACKLOG.md index 6133be4..1ca7fca 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -23,7 +23,7 @@ Das Datenmodell speichert `recurrence_rule` (iCal RRULE) für Kalender-Events. D ### BL-02 — Budget: Monatsvergleich (aktuell vs. Vormonat) -**Status:** Offen +**Status:** Erledigt (v0.3.0) **Aufwand:** S (1–2 Tage) SPEC: „Monatsvergleich (aktuell vs. Vormonat)". Derzeit zeigt die Budget-Seite nur den aktuellen Monat. Es fehlen API-Endpunkt und UI-Komponente für den Vergleich. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2bf7e..ce8ed2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Budget: month-over-month comparison in summary cards — each card (Einnahmen, Ausgaben, Saldo) shows a trend line (▲/▼ + delta amount vs. previous month); previous month summary is fetched in parallel with current month + ## [0.2.1] - 2026-03-30 ### Fixed diff --git a/public/pages/budget.js b/public/pages/budget.js index 86af7ae..0d677ee 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -26,9 +26,10 @@ const MONTH_NAMES = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', // -------------------------------------------------------- let state = { - month: '', // YYYY-MM - entries: [], - summary: null, + month: '', // YYYY-MM + entries: [], + summary: null, + prevSummary: null, // Vormonat für Monatsvergleich }; let _container = null; @@ -56,19 +57,23 @@ function addMonths(ym, n) { // -------------------------------------------------------- async function loadMonth(month) { + const prevMonth = addMonths(month, -1); try { - const [entriesRes, summaryRes] = await Promise.all([ + const [entriesRes, summaryRes, prevSummaryRes] = await Promise.all([ api.get(`/budget?month=${month}`), api.get(`/budget/summary?month=${month}`), + api.get(`/budget/summary?month=${prevMonth}`), ]); - state.month = month; - state.entries = entriesRes.data; - state.summary = summaryRes.data; + state.month = month; + state.entries = entriesRes.data; + state.summary = summaryRes.data; + state.prevSummary = prevSummaryRes.data; } catch (err) { console.error('[Budget] loadMonth Fehler:', err); - state.month = month; - state.entries = []; - state.summary = { income: 0, expenses: 0, balance: 0, by_category: [] }; + state.month = month; + state.entries = []; + state.summary = { income: 0, expenses: 0, balance: 0, byCategory: [] }; + state.prevSummary = null; window.oikos?.showToast('Budget konnte nicht geladen werden.', 'danger'); } } @@ -157,8 +162,10 @@ function renderBody() { if (!body) return; updateLabel(); - const s = state.summary; + const s = state.summary; + const p = state.prevSummary; const balanceClass = s.balance >= 0 ? 'budget-summary-card--balance-positive' : 'budget-summary-card--balance-negative'; + const prevLabel = p ? formatMonthLabel(p.month).split(' ')[0].slice(0, 3) : ''; body.innerHTML = ` @@ -166,14 +173,17 @@ function renderBody() {
Einnahmen
${formatAmount(s.income)}
+ ${p ? renderTrend(s.income, p.income, prevLabel) : ''}
Ausgaben
${formatAmount(Math.abs(s.expenses))}
+ ${p ? renderTrend(s.expenses, p.expenses, prevLabel) : ''}
Saldo
${formatAmount(s.balance)}
+ ${p ? renderTrend(s.balance, p.balance, prevLabel) : ''}
@@ -274,6 +284,28 @@ function renderEntries() { }).join(''); } +/** + * Rendert eine Trend-Zeile im Vergleich zum Vormonat. + * Alle drei Metriken (income, expenses, balance) nutzen dieselbe Logik: + * delta > 0 → positiver Trend (▲ grün), delta < 0 → negativer Trend (▼ rot). + * Ausgaben werden als negative Zahlen übergeben, daher gilt: + * weniger Ausgaben ↔ delta > 0 ↔ gut. + * @param {number} current Aktueller Wert + * @param {number} prev Vormonatswert + * @param {string} prevLabel Kurzname des Vormonats (z.B. "Mär") + */ +function renderTrend(current, prev, prevLabel) { + const delta = current - prev; + if (Math.abs(delta) < 0.005) { + return `
— wie ${prevLabel}
`; + } + const positive = delta > 0; + const arrow = positive ? '▲' : '▼'; + const sign = positive ? '+' : ''; + const cls = positive ? 'budget-summary-card__trend--positive' : 'budget-summary-card__trend--negative'; + return `
${arrow} ${sign}${formatAmount(delta)} vs. ${prevLabel}
`; +} + function formatEntryDate(dateStr) { const d = new Date(dateStr + 'T00:00:00'); return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.`; diff --git a/public/styles/budget.css b/public/styles/budget.css index bcca4ac..2f09836 100644 --- a/public/styles/budget.css +++ b/public/styles/budget.css @@ -101,6 +101,14 @@ .budget-summary-card--balance-positive .budget-summary-card__amount { color: var(--color-success); } .budget-summary-card--balance-negative .budget-summary-card__amount { color: var(--color-danger); } +.budget-summary-card__trend { + font-size: var(--text-xs); + margin-top: var(--space-1); +} +.budget-summary-card__trend--positive { color: var(--color-success); } +.budget-summary-card__trend--negative { color: var(--color-danger); } +.budget-summary-card__trend--neutral { color: var(--color-text-secondary); } + /* -------------------------------------------------------- * Kategorien-Diagramm (Canvas) * -------------------------------------------------------- */