// Finance data + chart components (ported & retuned for desktop) const NUMS = { fontVariantNumeric: 'tabular-nums', fontFeatureSettings: '"tnum"' }; const finPalette = (t) => ({ pos: t.green, neg: t.red, posBar: t.dark ? 'rgba(48,209,88,0.45)' : 'rgba(52,199,89,0.42)', negBar: t.dark ? 'rgba(255,69,58,0.42)' : 'rgba(255,59,48,0.36)', grid: t.dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.07)', }); // Monthly income/expense for the year (Mai 2026 = current) const FIN_BARS = [ { key: '2025-11', label: 'Nov', inc: 1640, exp: 720 }, { key: '2025-12', label: 'Dez', inc: 980, exp: 540 }, { key: '2026-01', label: 'Jan', inc: 1320, exp: 880 }, { key: '2026-02', label: 'Feb', inc: 1180, exp: 410 }, { key: '2026-03', label: 'Mär', inc: 2040, exp: 1260 }, { key: '2026-04', label: 'Apr', inc: 2260, exp: 690 }, { key: '2026-05', label: 'Mai', inc: 2480, exp: 760 }, ]; const SEED_TX = [ { id: 't1', kind: 'in', label: 'Hochzeit Müller', sub: 'JBL EON 712 · 4 Tage', amount: 180, date: '18. Mai 2026', dateISO: '2026-05-18' }, { id: 't2', kind: 'in', label: 'Gartenfest Bauer', sub: 'Shure SM58 ×4 · 2 Tage', amount: 60, date: '19. Mai 2026', dateISO: '2026-05-19' }, { id: 't3', kind: 'out', label: 'Ersatz-Speakon Kabel', sub: 'Zubehör Tontechnik', amount: 48, date: '16. Mai 2026', dateISO: '2026-05-16' }, { id: 't4', kind: 'in', label: 'Vereinsfest TSV', sub: 'Shure SM58 ×4 · 2 Tage', amount: 60, date: '27. Apr 2026', dateISO: '2026-04-27' }, { id: 't5', kind: 'in', label: 'Umzug Schmidt', sub: 'Anhänger 750kg · 1 Tag', amount: 40, date: '15. Mai 2026', dateISO: '2026-05-15' }, { id: 't6', kind: 'out', label: 'TÜV Anhänger 750kg', sub: 'Werkstatt · HU/AU', amount: 124, date: '12. Mai 2026', dateISO: '2026-05-12' }, { id: 't7', kind: 'in', label: 'Anzahlung Klein', sub: 'PA-Komplettset · Reservierung', amount: 80, date: '10. Mai 2026', dateISO: '2026-05-10' }, { id: 't8', kind: 'out', label: 'Versicherung Q2', sub: 'Geräte-Inhaltsversicherung', amount: 95, date: '04. Mai 2026', dateISO: '2026-05-04' }, ]; // Per-month historical detail (used in the Monatsverlauf drill-down) const CURRENT_MONTH_KEY = '2026-05'; const MONTHS_DETAIL = { '2026-01': { label: 'Januar 2026', short: 'Jan', income: 980, expenses: 320, bars: [{ label: 'W1', inc: 220, exp: 80 }, { label: 'W2', inc: 320, exp: 120 }, { label: 'W3', inc: 180, exp: 0 }, { label: 'W4', inc: 260, exp: 120 }], transactions: [ { id: 'jan1', kind: 'in', label: 'Silvesterparty', sub: 'PA-Anlage · 2 Tage', amount: 420, date: '02.01.' }, { id: 'jan2', kind: 'out', label: 'Anhänger Wartung', sub: 'TÜV + Reparatur', amount: 380, date: '14.01.' }, { id: 'jan3', kind: 'in', label: 'Firmenfeier Bauer', sub: 'Mikro-Set · 1 Tag', amount: 95, date: '17.01.' }, { id: 'jan4', kind: 'in', label: 'Geburtstag Klein', sub: 'JBL · 2 Tage', amount: 90, date: '24.01.' }, ], }, '2026-02': { label: 'Februar 2026', short: 'Feb', income: 1120, expenses: 480, bars: [{ label: 'W1', inc: 180, exp: 0 }, { label: 'W2', inc: 220, exp: 80 }, { label: 'W3', inc: 380, exp: 400 }, { label: 'W4', inc: 340, exp: 0 }], transactions: [ { id: 'feb1', kind: 'out', label: 'Sennheiser EW100', sub: 'Neues Equipment', amount: 680, date: '12.02.' }, { id: 'feb2', kind: 'in', label: 'Hochzeit Schmidt', sub: 'Komplett-Set · 3 Tage', amount: 380, date: '15.02.' }, { id: 'feb3', kind: 'in', label: 'Schulfest', sub: 'PA · 1 Tag', amount: 90, date: '22.02.' }, ], }, '2026-03': { label: 'März 2026', short: 'Mär', income: 1840, expenses: 220, bars: [{ label: 'W1', inc: 240, exp: 0 }, { label: 'W2', inc: 380, exp: 120 }, { label: 'W3', inc: 750, exp: 0 }, { label: 'W4', inc: 470, exp: 100 }], transactions: [ { id: 'mar1', kind: 'in', label: 'Messeveranstaltung', sub: 'Komplett-Set · 5 Tage', amount: 750, date: '14.03.' }, { id: 'mar2', kind: 'in', label: 'Vereinsjubiläum', sub: 'PA + Mikros', amount: 320, date: '21.03.' }, { id: 'mar3', kind: 'out', label: 'Kabel-Set', sub: 'Verbrauchsmaterial', amount: 120, date: '08.03.' }, { id: 'mar4', kind: 'in', label: 'Hochzeit Weber', sub: '2 Tage', amount: 280, date: '28.03.' }, ], }, '2026-04': { label: 'April 2026', short: 'Apr', income: 2100, expenses: 960, bars: [{ label: 'W1', inc: 380, exp: 0 }, { label: 'W2', inc: 580, exp: 880 }, { label: 'W3', inc: 620, exp: 0 }, { label: 'W4', inc: 520, exp: 80 }], transactions: [ { id: 'apr1', kind: 'out', label: 'JBL SRX835P', sub: 'Neues Equipment', amount: 880, date: '11.04.' }, { id: 'apr2', kind: 'in', label: 'Open Air Festival', sub: 'PA · 4 Tage', amount: 720, date: '18.04.' }, { id: 'apr3', kind: 'in', label: 'Firmenevent Audi', sub: 'Komplett · 2 Tage', amount: 580, date: '24.04.' }, { id: 'apr4', kind: 'out', label: 'Versicherung Q2', sub: 'Geräte-Inhalt', amount: 80, date: '28.04.' }, ], }, '2026-05': { label: 'Mai 2026', short: 'Mai', income: 2480, expenses: 680, bars: [ { label: 'Mo', inc: 0, exp: 0 }, { label: 'Di', inc: 180, exp: 0 }, { label: 'Mi', inc: 340, exp: 320 }, { label: 'Do', inc: 180, exp: 0 }, { label: 'Fr', inc: 580, exp: 240 }, { label: 'Sa', inc: 760, exp: 0 }, { label: 'So', inc: 440, exp: 120 }, ], transactions: [ { id: 'may1', kind: 'in', label: 'Hochzeit Müller', sub: 'JBL EON 712 · 4 Tage', amount: 180, date: '20.05.' }, { id: 'may2', kind: 'in', label: 'Vereinsfest TSV', sub: 'SM58 Set · 2 Tage', amount: 60, date: '19.05.' }, { id: 'may3', kind: 'out', label: 'Mikrofonständer ×4', sub: 'König & Meyer', amount: 240, date: '18.05.' }, { id: 'may4', kind: 'in', label: 'Open Air Konzert', sub: 'Komplett-Set · 3 Tage', amount: 320, date: '17.05.' }, { id: 'may5', kind: 'out', label: 'Kabelreparatur', sub: 'Werkstatt Müller', amount: 85, date: '16.05.' }, { id: 'may6', kind: 'in', label: 'Geburtstag Klein', sub: 'Anhänger · 1 Tag', amount: 40, date: '15.05.' }, { id: 'may7', kind: 'out', label: 'Versicherung Anhänger', sub: 'Halbjährlich', amount: 355, date: '14.05.' }, { id: 'may8', kind: 'in', label: 'Schulfest', sub: 'JBL EON 712 · 1 Tag', amount: 45, date: '12.05.' }, ], }, }; // Percent-change helper (returns null when previous = 0). const pctF = (cur, prev) => { if (!prev) return null; const v = Math.round((cur - prev) / Math.abs(prev) * 100); return { val: Math.abs(v), up: v >= 0 }; }; function buildFinanceSummary(bars) { const cur = bars[bars.length - 1]; const prev = bars[bars.length - 2] || { inc: 0, exp: 0 }; const ytdInc = bars.reduce((s, b) => s + b.inc, 0); const ytdExp = bars.reduce((s, b) => s + b.exp, 0); return { income: cur.inc, expenses: cur.exp, net: cur.inc - cur.exp, prevNet: prev.inc - prev.exp, ytdInc, ytdExp, ytdNet: ytdInc - ytdExp, }; } // ── Bar chart (hover tooltip) ── function ChartBars({ bars }) { const t = useTheme(); const p = finPalette(t); const max = Math.max(...bars.map((b) => Math.max(b.inc, b.exp)), 1); const H = 200; const [active, setActive] = useState(null); return (
{[0, 0.25, 0.5, 0.75, 1].map((g) => (
))}
{bars.map((bar, i) => { const isOn = active === i; const net = bar.inc - bar.exp; return (
setActive(i)} onMouseLeave={() => setActive((v) => v === i ? null : v)} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end', position: 'relative', cursor: 'default' }}>
{isOn && (
{bar.label} 2026
Einnahmen +{bar.inc} €
Ausgaben −{bar.exp} €
Netto = 0 ? p.pos : p.neg, fontWeight: 700 }}>{net >= 0 ? '+' : '−'}{Math.abs(net)} €
)}
); })}
{bars.map((bar, i) =>
{bar.label}
)}
); } // ── Line chart (income vs expense) ── function ChartLine({ bars }) { const t = useTheme(); const p = finPalette(t); const max = Math.max(...bars.map((b) => Math.max(b.inc, b.exp)), 1); const W = 560, H = 200, pad = 10; 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) => )} {bars.map((b, i) => {b.label})} ); } // ── Cumulative net area ── function ChartArea({ bars }) { const t = useTheme(); const p = finPalette(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 = 560, H = 200, pad = 10; 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) => )} {bars.map((b, i) => {b.label})} ); } // ── Donut (income / expense split) ── function ChartDonut({ income, expenses, net }) { const t = useTheme(); const p = finPalette(t); const total = income + expenses || 1; const incPct = income / total; const C = 100, R = 78, SW = 16, gap = 3; const circ = 2 * Math.PI * R; return (
Netto
{net >= 0 ? '+' : ''}{net.toLocaleString('de-DE')} €
{Math.round(incPct * 100)}% Ein · {Math.round((1 - incPct) * 100)}% Aus
); } Object.assign(window, { NUMS, finPalette, FIN_BARS, SEED_TX, MONTHS_DETAIL, CURRENT_MONTH_KEY, pctF, buildFinanceSummary, ChartBars, ChartLine, ChartArea, ChartDonut });