// Finance helpers — pure, no React. // Lifted into window for use across screen-finanzen.jsx, screen-dashboard.jsx // and BackupView. const MONTHS_LONG_DE = [ 'Januar','Februar','März','April','Mai','Juni', 'Juli','August','September','Oktober','November','Dezember' ]; const MONTHS_SHORT_DE = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; const WEEKDAYS_SHORT_DE = ['So','Mo','Di','Mi','Do','Fr','Sa']; function _parseDate(s) { if (!s) return null; if (s instanceof Date) return isNaN(s.getTime()) ? null : s; // Tolerate plain 'YYYY-MM-DD' const d = new Date(s); return isNaN(d.getTime()) ? null : d; } function fin_todayISO() { const d = new Date(), p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; } function fin_monthKeyOf(date) { const d = _parseDate(date); if (!d) return null; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } function fin_fmtMonthLabel(monthKey) { if (!monthKey) return ''; const [y, m] = monthKey.split('-'); const mi = parseInt(m, 10) - 1; if (isNaN(mi) || mi < 0 || mi > 11) return monthKey; return `${MONTHS_LONG_DE[mi]} ${y}`; } function fin_fmtMonthShort(monthKey) { if (!monthKey) return ''; const [, m] = monthKey.split('-'); const mi = parseInt(m, 10) - 1; return (mi >= 0 && mi <= 11) ? MONTHS_SHORT_DE[mi] : ''; } // Human-friendly date label: Heute / Gestern / vor N Tagen / DD.MM. function fin_fmtDateLabel(iso) { const d = _parseDate(iso); if (!d) return iso || ''; const today = new Date(); today.setHours(0, 0, 0, 0); const dn = new Date(d); dn.setHours(0, 0, 0, 0); const diff = Math.round((today - dn) / 86400000); if (diff === 0) return 'Heute'; if (diff === 1) return 'Gestern'; if (diff > 1 && diff < 7) return `vor ${diff} Tagen`; if (diff === -1) return 'Morgen'; const p = (n) => String(n).padStart(2, '0'); return `${p(d.getDate())}.${p(d.getMonth() + 1)}.${d.getFullYear()}`; } // Build the month list to show — from earliest of {installedAt, oldest tx} // to max of {today, latest tx}. Future months never appear unless a tx is in them. function fin_buildMonthsRange(installedAt, transactions) { const today = new Date(); let earliest = _parseDate(installedAt) || today; let latest = today; for (const tx of (transactions || [])) { const d = _parseDate(tx.date); if (!d) continue; if (d < earliest) earliest = d; if (d > latest) latest = d; } // Clamp to start-of-month to avoid edge cases. earliest = new Date(earliest.getFullYear(), earliest.getMonth(), 1); latest = new Date(latest.getFullYear(), latest.getMonth(), 1); const months = []; const cur = new Date(earliest); let safety = 0; while (cur <= latest && safety++ < 600) { const m = cur.getMonth(), y = cur.getFullYear(); const key = `${y}-${String(m + 1).padStart(2, '0')}`; months.push({ key, year: y, month: m, label: `${MONTHS_LONG_DE[m]} ${y}`, short: MONTHS_SHORT_DE[m], }); cur.setMonth(cur.getMonth() + 1); } return months; } function fin_txInMonth(transactions, monthKey) { return (transactions || []).filter(t => fin_monthKeyOf(t.date) === monthKey); } function fin_txInYear(transactions, year) { return (transactions || []).filter(t => { const d = _parseDate(t.date); return d && d.getFullYear() === year; }); } function fin_sumAgg(txs) { let inc = 0, exp = 0; for (const t of (txs || [])) { if (t.kind === 'in') inc += Number(t.amount) || 0; else if (t.kind === 'out') exp += Number(t.amount) || 0; } return { income: inc, expenses: exp, profit: inc - exp }; } // Weekly bars W1..Wn (n = ceil(daysInMonth/7)) for current month function fin_weeklyBars(transactions, year, month) { const daysInMonth = new Date(year, month + 1, 0).getDate(); const numWeeks = Math.ceil(daysInMonth / 7); const bars = Array.from({ length: numWeeks }, (_, i) => ({ label: `W${i + 1}`, inc: 0, exp: 0 })); for (const tx of (transactions || [])) { const d = _parseDate(tx.date); if (!d || d.getFullYear() !== year || d.getMonth() !== month) continue; const day = d.getDate(); const wi = Math.min(numWeeks - 1, Math.floor((day - 1) / 7)); if (tx.kind === 'in') bars[wi].inc += Number(tx.amount) || 0; else if (tx.kind === 'out') bars[wi].exp += Number(tx.amount) || 0; } return bars; } // Year bars — one per active month. Only months that fall in the visible range. function fin_yearBars(transactions, year, installedAt) { const months = fin_buildMonthsRange(installedAt, transactions).filter(m => m.year === year); return months.map(m => { const txs = fin_txInMonth(transactions, m.key); const { income, expenses } = fin_sumAgg(txs); return { label: m.short, inc: income, exp: expenses, monthKey: m.key }; }); } // Last-7-days income sparkline (one value per day, oldest → newest) function fin_lastNDaysIncome(transactions, n = 7) { const out = []; const base = new Date(); base.setHours(0, 0, 0, 0); for (let i = n - 1; i >= 0; i--) { const d = new Date(base); d.setDate(base.getDate() - i); const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const sum = (transactions || []).reduce((s, tx) => { if (tx.kind !== 'in') return s; const td = _parseDate(tx.date); if (!td) return s; const tiso = `${td.getFullYear()}-${String(td.getMonth() + 1).padStart(2, '0')}-${String(td.getDate()).padStart(2, '0')}`; return tiso === iso ? s + (Number(tx.amount) || 0) : s; }, 0); out.push(sum); } return out; } // Income per month for the mini sparkline. Returns the last `maxBars` months // of the active range (since installation / oldest tx → today). When the user // has data in Mai + Juni, this returns 2 numbers — one bar per month. function fin_monthlyIncomeBars(transactions, installedAt, maxBars = 12) { const months = fin_buildMonthsRange(installedAt, transactions); const recent = months.slice(-maxBars); return recent.map(m => ({ key: m.key, short: m.short, income: fin_sumAgg(fin_txInMonth(transactions, m.key)).income, })); } Object.assign(window, { MONTHS_LONG_DE, MONTHS_SHORT_DE, WEEKDAYS_SHORT_DE, fin_todayISO, fin_monthKeyOf, fin_fmtMonthLabel, fin_fmtMonthShort, fin_fmtDateLabel, fin_buildMonthsRange, fin_txInMonth, fin_txInYear, fin_sumAgg, fin_weeklyBars, fin_yearBars, fin_lastNDaysIncome, fin_monthlyIncomeBars, });