feat(budget): add month-over-month comparison to summary cards

Each summary card (Einnahmen, Ausgaben, Saldo) now shows a trend line
comparing the current month to the previous one. The previous month's
summary is fetched in parallel via the existing /budget/summary endpoint,
so there is no extra round-trip latency. Positive deltas render in green
(▲), negative in red (▼), unchanged in neutral grey (—).

Closes BL-02.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-03-30 22:54:04 +02:00
parent ca377e8441
commit 26d3d12a22
4 changed files with 55 additions and 12 deletions
+43 -11
View File
@@ -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 = `
<!-- Zusammenfassung -->
@@ -166,14 +173,17 @@ function renderBody() {
<div class="budget-summary-card budget-summary-card--income">
<div class="budget-summary-card__label">Einnahmen</div>
<div class="budget-summary-card__amount">${formatAmount(s.income)}</div>
${p ? renderTrend(s.income, p.income, prevLabel) : ''}
</div>
<div class="budget-summary-card budget-summary-card--expenses">
<div class="budget-summary-card__label">Ausgaben</div>
<div class="budget-summary-card__amount">${formatAmount(Math.abs(s.expenses))}</div>
${p ? renderTrend(s.expenses, p.expenses, prevLabel) : ''}
</div>
<div class="budget-summary-card ${balanceClass}">
<div class="budget-summary-card__label">Saldo</div>
<div class="budget-summary-card__amount">${formatAmount(s.balance)}</div>
${p ? renderTrend(s.balance, p.balance, prevLabel) : ''}
</div>
</div>
@@ -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 `<div class="budget-summary-card__trend budget-summary-card__trend--neutral">— wie ${prevLabel}</div>`;
}
const positive = delta > 0;
const arrow = positive ? '▲' : '▼';
const sign = positive ? '+' : '';
const cls = positive ? 'budget-summary-card__trend--positive' : 'budget-summary-card__trend--negative';
return `<div class="budget-summary-card__trend ${cls}">${arrow} ${sign}${formatAmount(delta)} vs. ${prevLabel}</div>`;
}
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')}.`;
+8
View File
@@ -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)
* -------------------------------------------------------- */