// Finanzen — refined private-banking feel const { useState, useEffect } = React; // Empty bar templates (kept so charts still render an axis when there's no data yet) const EMPTY_WEEK_BARS = [ { label: 'Mo', inc: 0, exp: 0 }, { label: 'Di', inc: 0, exp: 0 }, { label: 'Mi', inc: 0, exp: 0 }, { label: 'Do', inc: 0, exp: 0 }, { label: 'Fr', inc: 0, exp: 0 }, { label: 'Sa', inc: 0, exp: 0 }, { label: 'So', inc: 0, exp: 0 }, ]; const EMPTY_MONTH_BARS = [ { label: 'W1', inc: 0, exp: 0 }, { label: 'W2', inc: 0, exp: 0 }, { label: 'W3', inc: 0, exp: 0 }, { label: 'W4', inc: 0, exp: 0 }, ]; const EMPTY_YEAR_BARS = [ { label: 'Jan', inc: 0, exp: 0 }, { label: 'Feb', inc: 0, exp: 0 }, { label: 'Mär', inc: 0, exp: 0 }, { label: 'Apr', inc: 0, exp: 0 }, { label: 'Mai', inc: 0, exp: 0 }, { label: 'Jun', inc: 0, exp: 0 }, { label: 'Jul', inc: 0, exp: 0 }, { label: 'Aug', inc: 0, exp: 0 }, { label: 'Sep', inc: 0, exp: 0 }, { label: 'Okt', inc: 0, exp: 0 }, { label: 'Nov', inc: 0, exp: 0 }, { label: 'Dez', inc: 0, exp: 0 }, ]; // All historical month detail is now derived from the live `transactions` array // via fin_buildMonthsRange / fin_txInMonth / fin_sumAgg (see finance-data.jsx). function pctF(a, b) {if (!b) return null;const p = Math.round((a - b) / b * 100);return { val: Math.abs(p), up: p >= 0 };} // System colors (matches calendar status bars, equipment utilization, etc.) const palette = (t) => ({ pos: t.green, posSft: t.greenSoft, posBar: t.green + (t.dark ? '85' : '6E'), // slightly more saturated but still soft neg: t.red, negSft: t.redSoft, negBar: t.red + (t.dark ? '85' : '6E'), ink: t.text, grid: t.dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)' }); const NUMS = { fontVariantNumeric: 'tabular-nums', fontFeatureSettings: '"tnum"' }; // ─── Chart components ─────────────────────────────────────── function ChartBars({ bars, t, onPick, monthKeys }) { const p = palette(t); const rawMax = Math.max(...bars.map((b) => Math.max(b.inc, b.exp)), 1); // Round max up to a "nice" number so the axis ticks read cleanly const niceMax = (v) => { const pow = Math.pow(10, Math.floor(Math.log10(v))); const n = v / pow; const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10; return step * pow; }; const max = niceMax(rawMax); const fmtAxis = (v) => v >= 1000 ? (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1).replace('.', ',') + 'k' : String(v); const AXIS_W = 28; const H = 110; const [active, setActive] = React.useState(null); // hovered bar index const [pinned, setPinned] = React.useState(null); // tap-pinned bar (touch) // Pointer interaction: on touch we capture and scrub; on mouse we use per-bar enter/leave. const rowRef = React.useRef(null); const updateActiveFromEvent = (e) => { const el = rowRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const x = (e.clientX || (e.touches && e.touches[0] && e.touches[0].clientX) || 0) - rect.left; const idx = Math.floor(x / (rect.width / bars.length)); if (idx >= 0 && idx < bars.length) setActive(idx); }; return (
{/* Y-axis labels */}
{[0, 0.25, 0.5, 0.75, 1].map((g) =>
{fmtAxis(Math.round(max * g))}{g === 1 ? ' €' : ''}
)}
{/* Gridlines */} {[0, 0.25, 0.5, 0.75, 1].map((g) =>
)}
{ if (e.pointerType === 'touch') { updateActiveFromEvent(e); try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {} } }} onPointerMove={(e) => { if (e.pointerType === 'touch' && (e.buttons || e.pressure)) updateActiveFromEvent(e); }} onPointerUp={(e) => { if (e.pointerType === 'touch') { updateActiveFromEvent(e); setPinned(active); setTimeout(() => { setActive(null); setPinned(null); }, 1600); } }} style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: H, position: 'relative', touchAction: 'pan-y' }}> {bars.map((bar, i) => { const isOn = active === i || pinned === i; const incBg = isOn ? p.pos : p.posBar; const expBg = isOn ? p.neg : p.negBar; const monthKey = monthKeys && monthKeys[i]; const net = bar.inc - bar.exp; return (
setActive(i)} onMouseLeave={() => setActive((v) => v === i ? null : v)} onClick={() => { if (monthKey && onPick) { onPick(monthKey); } else { setPinned(i); setTimeout(() => setPinned(null), 1600); } }} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, height: '100%', justifyContent: 'flex-end', cursor: monthKey ? 'pointer' : 'default', position: 'relative' }}>
{isOn &&
{bar.label}
Ein. +{bar.inc} €
Aus. −{bar.exp} €
Netto = 0 ? p.pos : p.neg, fontWeight: 700 }}>{net >= 0 ? '+' : '−'}{Math.abs(net)} €
{monthKey &&
Klick → Details
} {/* arrow */}
}
); })}
{bars.map((bar, i) =>
{bar.label}
)}
); } function ChartLine({ bars, t }) { const p = palette(t); const max = Math.max(...bars.map((b) => Math.max(b.inc, b.exp)), 1); const W = 280,H = 100,pad = 6; const xs = (i) => i / (bars.length - 1 || 1) * (W - pad * 2) + pad; const ys = (v) => H - v / max * (H - pad * 2) - pad; const linePath = (key) => bars.map((b, i) => `${i === 0 ? 'M' : 'L'}${xs(i)} ${ys(b[key])}`).join(' '); const areaPath = `M${pad} ${H} ${bars.map((b, i) => `L${xs(i)} ${ys(b.inc)}`).join(' ')} L${W - pad} ${H} Z`; return ( {[0.25, 0.5, 0.75].map((g) => )} {bars.map((b, i) => 0 ? 2 : 0} fill={p.pos} />)} {bars.map((b, i) => {b.label})} ); } function ChartArea({ bars, t }) { const p = palette(t); let cum = 0; const nets = bars.map((b) => (cum += b.inc - b.exp, cum)); const max = Math.max(...nets, 0),min = Math.min(...nets, 0); const range = max - min || 1; const W = 280,H = 100,pad = 6; const xs = (i) => i / (nets.length - 1 || 1) * (W - pad * 2) + pad; const ys = (v) => H - (v - min) / range * (H - pad * 2) - pad; const linePath = nets.map((v, i) => `${i === 0 ? 'M' : 'L'}${xs(i)} ${ys(v)}`).join(' '); const baseY = ys(0); const areaPath = `M${pad} ${baseY} ${nets.map((v, i) => `L${xs(i)} ${ys(v)}`).join(' ')} L${W - pad} ${baseY} Z`; return ( {[0.25, 0.5, 0.75].map((g) => )} {nets.map((v, i) => v !== 0 && )} {bars.map((b, i) => {b.label})} ); } function ChartDonut({ income, expenses, profit, t }) { const p = palette(t); const total = income + expenses || 1; const incPct = income / total; const C = 80,R = 58,SW = 10,gap = 2; const circ = 2 * Math.PI * R; return (
Netto
{profit >= 0 ? '+' : ''}{profit.toLocaleString('de-DE')} €
{Math.round(incPct * 100)}% Ein. · {Math.round((1 - incPct) * 100)}% Aus.
); } const CHART_TYPES = [ { id: 'bars', label: 'Balken', icon: 'M4 20V10M10 20V4M16 20V14M22 20H2' }, { id: 'line', label: 'Linie', icon: 'M3 17l5-5 4 3 8-9' }, { id: 'area', label: 'Gewinn', icon: 'M3 17l5-5 4 3 8-9M3 20h18' }, { id: 'donut', label: 'Anteil', icon: 'M21 12a9 9 0 11-9-9v9h9z' }]; // ─── Add Transaction Sheet ────────────────────────────────── function AddTxSheet({ open, onClose, onSave }) { const t = useTheme(); const p = palette(t); const [kind, setKind] = useState('in'); const [label, setLabel] = useState(''); const [sub, setSub] = useState(''); const [amount, setAmount] = useState(''); const [date, setDate] = useState(() => fin_todayISO()); // Whenever the sheet (re-)opens, default the date back to today useEffect(() => { if (open) setDate(fin_todayISO()); }, [open]); const reset = () => {setKind('in');setLabel('');setSub('');setAmount('');setDate(fin_todayISO());}; const close = () => {reset();onClose();}; const save = () => { const n = parseFloat(String(amount).replace(',', '.')); if (!label.trim() || !n || n <= 0) return; onSave({ kind, label: label.trim(), sub: sub.trim() || (kind === 'in' ? 'Vermietung' : 'Ausgabe'), amount: Math.round(n), date: date || fin_todayISO() }); reset();onClose(); }; const fInp = { width: '100%', padding: '12px 14px', borderRadius: 12, border: `0.5px solid ${t.inputBorder}`, background: t.card, fontSize: 15, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }; return (
Neue Buchung
Einnahme oder Ausgabe erfassen.
Abbrechen
{[['in', 'Einnahme', p.pos], ['out', 'Ausgabe', p.neg]].map(([k, l, c]) => setKind(k)} scale={0.96} style={{ flex: 1 }}>
{l}
)}
Betrag
setAmount(e.target.value)} placeholder="0" type="number" inputMode="decimal" style={{ ...fInp, padding: '14px 36px 14px 14px', fontSize: 26, fontWeight: 600, color: t.text, letterSpacing: -0.6, ...NUMS }} />
Bezeichnung
setLabel(e.target.value)} placeholder={kind === 'in' ? 'z.B. Hochzeit Schmidt' : 'z.B. Neuer Lautsprecher'} style={fInp} />
Notiz · optional
setSub(e.target.value)} placeholder={kind === 'in' ? 'Was wurde vermietet?' : 'Wofür?'} style={fInp} />
Datum
{date !== fin_todayISO() && setDate(fin_todayISO())} scale={0.96}>
Heute
}
setDate(e.target.value)} type="date" max={fin_todayISO()} style={{ ...fInp, color: t.text, fontVariantNumeric: 'tabular-nums' }} />
{date === fin_todayISO() ? 'Standard: heute. Antippen, um nachträglich einzutragen.' : `Wird am ${fin_fmtDateLabel(date)} eingetragen.`}
Speichern
); } // ─── Edit Transaction Sheet — pre-filled, with delete ─────── function EditTxSheet({ tx, onClose, onSave, onDelete }) { const t = useTheme(); const p = palette(t); const [kind, setKind] = useState('in'); const [label, setLabel] = useState(''); const [sub, setSub] = useState(''); const [amount, setAmount] = useState(''); const [date, setDate] = useState(''); const [confirmDel, setConfirmDel] = useState(false); useEffect(() => { if (tx) { setKind(tx.kind); setLabel(tx.label); setSub(tx.sub || ''); setAmount(String(tx.amount)); setDate(tx.date && /^\d{4}-\d{2}-\d{2}/.test(tx.date) ? tx.date.slice(0, 10) : fin_todayISO()); } }, [tx && tx.id]); if (!tx) return null; const save = () => { const n = parseFloat(String(amount).replace(',', '.')); if (!label.trim() || !n || n <= 0) return; onSave({ kind, label: label.trim(), sub: sub.trim(), amount: Math.round(n), date: date || fin_todayISO() }); }; const fInp = { width: '100%', padding: '12px 14px', borderRadius: 12, border: `0.5px solid ${t.inputBorder}`, background: t.card, fontSize: 15, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }; return (
Buchung bearbeiten
{fin_fmtDateLabel(tx.date)}
Abbrechen
{[['in', 'Einnahme', p.pos], ['out', 'Ausgabe', p.neg]].map(([k, l, c]) => setKind(k)} scale={0.96} style={{ flex: 1 }}>
{l}
)}
Betrag
setAmount(e.target.value)} placeholder="0" type="number" inputMode="decimal" style={{ ...fInp, padding: '14px 36px 14px 14px', fontSize: 26, fontWeight: 600, color: t.text, letterSpacing: -0.6, ...NUMS }} />
Bezeichnung
setLabel(e.target.value)} style={fInp} />
Notiz
setSub(e.target.value)} style={fInp} />
Datum
{date !== fin_todayISO() && setDate(fin_todayISO())} scale={0.96}>
Auf heute
}
setDate(e.target.value)} type="date" style={{ ...fInp, color: t.text, fontVariantNumeric: 'tabular-nums' }} />
setConfirmDel(true)} scale={0.97}>
Löschen
Speichern
setConfirmDel(false)} onConfirm={() => { setConfirmDel(false); onDelete(); }} /> ); } // ─── Month Detail Sheet — drill into a specific month for analysis ───────── function MonthDetailSheet({ open, monthKey, onClose, t, p, transactions, monthsRange }) { const [chartType, setChartType] = useState('bars'); if (!monthKey) return null; // Compute month figures on the fly from the shared transactions array. const [y, m] = monthKey.split('-').map(Number); const monthTxs = fin_txInMonth(transactions, monthKey).slice().sort((a, b) => (b.date || '').localeCompare(a.date || '')); const { income, expenses } = fin_sumAgg(monthTxs); const profit = income - expenses; const margin = income > 0 ? Math.round(profit / income * 100) : 0; const bars = fin_weeklyBars(transactions, y, m - 1); const label = fin_fmtMonthLabel(monthKey); // Compare with previous month from range const keys = (monthsRange || []).map(mo => mo.key); const idx = keys.indexOf(monthKey); let prevAgg = null; if (idx > 0) prevAgg = fin_sumAgg(fin_txInMonth(transactions, keys[idx - 1])); const incTrend = prevAgg ? pctF(income, prevAgg.income) : null; const expTrend = prevAgg ? pctF(expenses, prevAgg.expenses) : null; const profTrend = prevAgg ? pctF(profit, prevAgg.income - prevAgg.expenses) : null; // Top categories from transactions const topIn = [...monthTxs].filter((x) => x.kind === 'in').sort((a, b) => b.amount - a.amount).slice(0, 1)[0]; const topOut = [...monthTxs].filter((x) => x.kind === 'out').sort((a, b) => b.amount - a.amount).slice(0, 1)[0]; return (
Monatsanalyse
{label}
Schließen
{/* Big stats */}
Netto-Ergebnis
= 0 ? p.pos : p.neg, letterSpacing: -1.2, ...NUMS }}> {profit >= 0 ? '+' : ''}{profit.toLocaleString('de-DE')}
{profTrend &&
{profTrend.up ? '▲' : '▼'} {profTrend.val}%
}
Marge {margin}% · {monthTxs.length} Buchungen
Einnahmen
{income.toLocaleString('de-DE')}
{incTrend &&
{incTrend.up ? '▲' : '▼'} {incTrend.val}% vs. Vormonat
}
Ausgaben
{expenses.toLocaleString('de-DE')}
{expTrend &&
{expTrend.up ? '▲' : '▼'} {expTrend.val}% vs. Vormonat
}
{/* Chart switch */}
{CHART_TYPES.map((c) => setChartType(c.id)} scale={0.94} style={{ flex: 1 }}>
{c.label}
)}
{/* Chart */}
{chartType === 'bars' && } {chartType === 'line' && } {chartType === 'area' && } {chartType === 'donut' && }
{/* Highlights */} {(topIn || topOut) &&
Top-Positionen
{topIn &&
Größte Einnahme
{topIn.label}
+{topIn.amount} €
} {topOut &&
Größte Ausgabe
{topOut.label}
−{topOut.amount} €
}
} {/* Transactions */}
Alle Buchungen
{monthTxs.map((tx, i, arr) => { const isIn = tx.kind === 'in'; return (
{tx.label}
{tx.sub} · {fin_fmtDateLabel(tx.date)}
{isIn ? '+' : '−'}{tx.amount} €
); })}
); } // ─── Main Finanzen ─────────────────────────────────────────── function ScreenFinanzen({ toast, transactions, setTransactions, installedAt }) { const t = useTheme(); const p = palette(t); const [period, setPeriod] = useState('month'); const [tab, setTab] = useState('all'); const [addOpen, setAddOpen] = useState(false); const [editingTx, setEditingTx] = useState(null); const [monthDetailKey, setMonthDetailKey] = useState(null); const [defaultChart, setDefaultChart] = useState(() => { try { const raw = localStorage.getItem('rf-default-chart'); if (!raw) return 'bars'; // Tolerate legacy raw-string format AND the proper JSON-encoded form. try { const v = JSON.parse(raw); return typeof v === 'string' ? v : 'bars'; } catch { return raw; } } catch {return 'bars';} }); const [chartType, setChartType] = useState(defaultChart); const lpRef = React.useRef({ timer: null, fired: false }); const startPinPress = (id) => { lpRef.current.fired = false; clearTimeout(lpRef.current.timer); lpRef.current.timer = setTimeout(() => { lpRef.current.fired = true; pinAsDefault(id); if (navigator.vibrate) try { navigator.vibrate(10); } catch (_) {} }, 500); }; const cancelPinPress = () => { clearTimeout(lpRef.current.timer); }; const pinAsDefault = (id) => { try {localStorage.setItem('rf-default-chart', JSON.stringify(id));} catch {} setDefaultChart(id); toast(`„${CHART_TYPES.find((c) => c.id === id).label}“ als Standard gesetzt`); }; const baseD = { label: period === 'month' ? fin_fmtMonthLabel(fin_monthKeyOf(new Date())) : `Jahr ${new Date().getFullYear()}` }; // ── Live, dynamic data ── const today = new Date(); const curYear = today.getFullYear(); const curMonth = today.getMonth(); const CURRENT_MONTH_KEY = fin_monthKeyOf(today); const monthsRange = fin_buildMonthsRange(installedAt, transactions); const currentMonthLabel = fin_fmtMonthLabel(CURRENT_MONTH_KEY); const periodTxs = period === 'month' ? fin_txInMonth(transactions, CURRENT_MONTH_KEY) : fin_txInYear(transactions, curYear); const periodAgg = fin_sumAgg(periodTxs); const income = periodAgg.income; const expenses = periodAgg.expenses; const profit = periodAgg.profit; // Bars (weekly for month, monthly-of-active-months for year) const bars = period === 'month' ? fin_weeklyBars(transactions, curYear, curMonth) : fin_yearBars(transactions, curYear, installedAt); // Kontostand = net of ALL transactions (Home & Finanzen share this exact number) const liveBalance = fin_sumAgg(transactions).profit; // Hero-card month figures (always current month, independent of period toggle) const monthTxs = fin_txInMonth(transactions, CURRENT_MONTH_KEY); const monthAgg = fin_sumAgg(monthTxs); const monthIncome = monthAgg.income; const monthExpenses = monthAgg.expenses; const monthProfit = monthAgg.profit; // Mini-Bars: income per month across the active range (installation → today, // or oldest tx → today). Shows one bar per month — e.g. 2 bars for Mai + Juni. const monthBars = fin_monthlyIncomeBars(transactions, installedAt, 12); const sparkMax = Math.max(...monthBars.map(b => b.income)) || 1; // Transaction list (sorted newest-first) const filtered = periodTxs .filter((tx) => tab === 'all' || tx.kind === tab) .slice().sort((a, b) => (b.date || '').localeCompare(a.date || '')); const saveTxEdit = (id, patch) => { setTransactions((prev) => prev.map((x) => x.id === id ? { ...x, ...patch } : x)); toast('Buchung aktualisiert'); }; const deleteTx = (id) => { setTransactions((prev) => prev.filter((x) => x.id !== id)); toast('Buchung gelöscht'); }; const onSaveTx = (tx) => { setTransactions((prev) => [{ id: Date.now(), ...tx }, ...prev]); toast(`${tx.kind === 'in' ? 'Einnahme' : 'Ausgabe'} ${tx.amount} € gespeichert`); }; const fmt = (n) => Math.abs(n) >= 1000 ? (n / 1000).toFixed(1).replace('.', ',') + 'T' : String(n); return (
{/* Header — title row on top, period segmented control below for breathing room */}
{baseD.label}
Finanzen
setAddOpen(true)} scale={0.88}>
{[['month', 'Monat'], ['year', 'Jahr']].map(([id, label]) => setPeriod(id)} scale={0.96}>
{label}
)}
{/* Hero: balance — matches Home Kontostand card style, with Einnahmen + Ausgaben */}
{/* Row 1 — Kontostand + sparkline */}
Kontostand
{liveBalance.toLocaleString('de-DE')}
Verfügbares Guthaben
{monthBars.map((b, i) =>
)}
{/* Hairline */}
{currentMonthLabel}
{/* Einnahmen */}
Einnahmen
+{monthIncome.toLocaleString('de-DE')}
{/* Ausgaben */}
Ausgaben
−{monthExpenses.toLocaleString('de-DE')}
{/* Netto */}
= 0 ? p.pos : p.neg }} />
Netto
= 0 ? p.pos : p.neg, marginTop: 5, ...NUMS }}> {monthProfit >= 0 ? '+' : ''}{monthProfit.toLocaleString('de-DE')}
{/* Chart selector — always visible. Long-press a tile to make it the default. */}
{CHART_TYPES.map((c) =>
startPinPress(c.id)} onPointerUp={cancelPinPress} onPointerLeave={cancelPinPress} onPointerCancel={cancelPinPress} onContextMenu={(e) => { e.preventDefault(); pinAsDefault(c.id); }}> { if (!lpRef.current.fired) setChartType(c.id); }} scale={0.94} style={{ width: '100%' }}>
{c.label} {defaultChart === c.id &&
}
)}
Lang gedrückt halten, um als Standard festzulegen
{/* Chart card */}
{chartType === 'donut' ? 'Aufteilung' : chartType === 'area' ? 'Gewinn kumuliert' : chartType === 'line' ? 'Verlauf' : 'Einnahmen vs. Ausgaben'}
{chartType !== 'area' && <>
{chartType === 'donut' ? 'Ein.' : 'Einnahmen'}
{chartType === 'donut' ? 'Aus.' : 'Ausgaben'}
} {chartType === 'area' &&
Netto kumuliert
}
{chartType === 'bars' && setMonthDetailKey(key)} monthKeys={period === 'year' ? bars.map(b => b.monthKey) : null}/>} {chartType === 'line' && } {chartType === 'area' && } {chartType === 'donut' && }
{/* Transaction list */}
Buchungen
{[['all', 'Alle'], ['in', 'Ein'], ['out', 'Aus']].map(([id, label]) => setTab(id)} scale={0.92}>
{label}
)}
{filtered.length === 0 &&
Keine Buchungen.
} {filtered.map((tx, i, arr) => { const isIncome = tx.kind === 'in'; const col = isIncome ? p.pos : p.neg; return ( setEditingTx(tx)} onEdit={() => setEditingTx(tx)} onDelete={() => deleteTx(tx.id)}>
{tx.label}
{tx.sub}
{isIncome ? '+' : '−'}{tx.amount} €
{tx.date}
); })}
{/* Monatsverlauf — drill into past months */}
Monatsverlauf
Tippen für Analyse
{monthsRange.length === 0 &&
Noch keine Monate.
} {monthsRange.slice().reverse().map((mo, i, arr) => { const key = mo.key; const mAgg = fin_sumAgg(fin_txInMonth(transactions, key)); const prof = mAgg.profit; const isCurrent = key === CURRENT_MONTH_KEY; return ( setMonthDetailKey(key)} scale={0.99}>
{mo.short}
{key.slice(2, 4)}
{mo.label} {isCurrent && JETZT}
+{mAgg.income} −{mAgg.expenses}
= 0 ? p.pos : p.neg, ...NUMS }}> {prof >= 0 ? '+' : ''}{prof.toLocaleString('de-DE')} €
Netto
); })}
setAddOpen(false)} onSave={onSaveTx} /> setEditingTx(null)} onSave={(patch) => { saveTxEdit(editingTx.id, patch); setEditingTx(null); }} onDelete={() => { deleteTx(editingTx.id); setEditingTx(null); }} /> setMonthDetailKey(null)} t={t} p={p} transactions={transactions} monthsRange={monthsRange} />
); } Object.assign(window, { ScreenFinanzen });