// Desktop Finanzen — overview, chart switcher, transactions ledger. function ScreenFinance({ go, rentals, equipment }) { const t = useTheme(); const toast = useToast(); const [tx, setTx] = useState(SEED_TX); const [chart, setChart] = useState('bars'); const [txFilter, setTxFilter] = useState('alle'); const [sheet, setSheet] = useState(null); // {} new | tx edit const [monthDetail, setMonthDetail] = useState(null); // month key const fin = buildFinanceSummary(FIN_BARS); const CURRENT_BALANCE = 4820; const depositsHeld = rentals.filter((r) => r.depositStatus === 'erhalten').reduce((s, r) => s + (Number(r.deposit) || 0), 0); const openInvoices = rentals.filter((r) => r.paymentStatus === 'offen').reduce((s, r) => s + rentalTotal(r), 0); const visibleTx = tx.filter((x) => txFilter === 'alle' || x.kind === txFilter); const saveTx = (x) => { setTx((prev) => { const ex = prev.find((p) => p.id === x.id); return ex ? prev.map((p) => p.id === x.id ? x : p) : [{ ...x, id: 't-' + Date.now() }, ...prev]; }); toast('Buchung gespeichert'); }; const delTx = (id) => { setTx((prev) => prev.filter((p) => p.id !== id)); toast('Buchung gelöscht'); }; const CHART_TYPES = [['bars', 'Balken'], ['line', 'Verlauf'], ['area', 'Gewinn'], ['donut', 'Anteil']]; const card = { background: t.card, borderRadius: 16, boxShadow: t.shadowCard }; return ( <>
{/* LEFT */}
{/* balance */}
Kontostand
{CURRENT_BALANCE.toLocaleString('de-DE')}
{[['Einnahmen · Mai', `+${fin.income} €`, t.green], ['Ausgaben · Mai', `−${fin.expenses} €`, t.red], ['Netto · Mai', `${fin.net >= 0 ? '+' : ''}${fin.net} €`, t.text]].map(([l, v, c]) => (
{l}
{v}
))}
{/* chart */}
Einnahmen & Ausgaben
{chart === 'bars' && } {chart === 'line' && } {chart === 'area' && } {chart === 'donut' && }
{/* transactions */}
Buchungen
{visibleTx.map((x, i) => ( setSheet(x)} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10 }}>
{x.label}
{x.sub} · {x.date}
{x.kind === 'in' ? '+' : '−'}{x.amount} €
))}
{/* monatsverlauf — drill into past months */}
Monatsverlauf
Klick für Analyse
{Object.entries(MONTHS_DETAIL).reverse().map(([key, md], i, arr) => { const prof = md.income - md.expenses; const isCurrent = key === CURRENT_MONTH_KEY; return ( setMonthDetail(key)} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10 }}>
{md.short}
{key.slice(2, 4)}
{md.label} {isCurrent && JETZT}
+{md.income.toLocaleString('de-DE')} € −{md.expenses.toLocaleString('de-DE')} €
= 0 ? t.green : t.red, ...NUMS }}>{prof >= 0 ? '+' : '−'}{Math.abs(prof).toLocaleString('de-DE')} €
Netto
); })}
{/* RIGHT */}
{/* outstanding */}
Offene Posten
Offene Rechnungen
{rentals.filter((r) => r.paymentStatus === 'offen').length} Vorgänge
{openInvoices} €
Gehaltene Kautionen
Treuhand
{depositsHeld} €
{/* YTD summary */}
Jahresübersicht
Gewinn
+{fin.ytdNet.toLocaleString('de-DE')} €
{/* top earners */}
go('equipment')}>Top-Erlöse je Gerät
{[...equipment].map((e) => ({ e, sum: rentals.filter((r) => r.equipmentId === e.id && r.paymentStatus === 'bezahlt').reduce((s, r) => s + rentalTotal(r), 0) })).sort((a, b) => b.sum - a.sum).slice(0, 5).map(({ e, sum }) => (
{e.name}
{sum} €
))}
setSheet(null)} onSave={saveTx} onDelete={delTx}/> setMonthDetail(null)}/> ); } function TxSheet({ open, initial, defaultKind, onClose, onSave, onDelete }) { const t = useTheme(); const isEdit = !!(initial && initial.id); const blank = { kind: defaultKind || 'in', label: '', sub: '', amount: '', date: 'Heute' }; const [form, setForm] = useState(blank); useEffect(() => { if (open) setForm(initial ? { ...initial, amount: String(initial.amount) } : { ...blank, kind: defaultKind || 'in' }); }, [open, initial]); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const valid = form.label.trim() && parseFloat(String(form.amount).replace(',', '.')) > 0; const save = () => { if (!valid) return; onSave({ ...form, id: form.id, amount: Math.round(parseFloat(String(form.amount).replace(',', '.'))), sub: form.sub.trim() || (form.kind === 'in' ? 'Vermietung' : 'Ausgabe') }); onClose(); }; if (!open) return null; return (
{isEdit ? 'Buchung bearbeiten' : 'Neue Buchung'}
Abbrechen
{[['in', 'Einnahme', t.green], ['out', 'Ausgabe', t.red]].map(([k, l, c]) => ( set('kind', k)} scale={0.96} style={{ flex: 1 }}>
{l}
))}
set('amount', e.target.value)} placeholder="0" style={{ fontSize: 22, fontWeight: 600 }}/> set('label', e.target.value)} placeholder={form.kind === 'in' ? 'z.B. Hochzeit Schmidt' : 'z.B. Neuer Lautsprecher'}/> set('sub', e.target.value)} placeholder="optional"/>
{isEdit && { onDelete(form.id); onClose(); }} scale={0.97}>
Löschen
}
Speichern
); } Object.assign(window, { ScreenFinance, TxSheet, MonthDetailSheet }); // ── Modal: drill into a specific month for analysis ── function MonthDetailSheet({ open, monthKey, onClose }) { const t = useTheme(); const p = finPalette(t); const [chart, setChart] = useState('bars'); if (!open || !monthKey) return null; const md = MONTHS_DETAIL[monthKey]; if (!md) return null; const profit = md.income - md.expenses; const margin = md.income > 0 ? Math.round(profit / md.income * 100) : 0; const keys = Object.keys(MONTHS_DETAIL); const idx = keys.indexOf(monthKey); const prev = idx > 0 ? MONTHS_DETAIL[keys[idx - 1]] : null; const incTrend = prev ? pctF(md.income, prev.income) : null; const expTrend = prev ? pctF(md.expenses, prev.expenses) : null; const profTrend = prev ? pctF(profit, prev.income - prev.expenses) : null; const topIn = [...md.transactions].filter((x) => x.kind === 'in').sort((a, b) => b.amount - a.amount)[0]; const topOut = [...md.transactions].filter((x) => x.kind === 'out').sort((a, b) => b.amount - a.amount)[0]; const CHART_TYPES = [['bars', 'Balken'], ['line', 'Verlauf'], ['area', 'Gewinn'], ['donut', 'Anteil']]; return (
{/* Header */}
Monatsanalyse
{md.label}
Schließen
{/* Big stats card */}
Netto-Ergebnis
= 0 ? p.pos : p.neg, letterSpacing: -1.2, ...NUMS }}> {profit >= 0 ? '+' : ''}{profit.toLocaleString('de-DE')}
{profTrend && (
{profTrend.up ? '▲' : '▼'} {profTrend.val}%
)}
Marge {margin}% · {md.transactions.length} Buchungen
Einnahmen
{md.income.toLocaleString('de-DE')}
{incTrend &&
{incTrend.up ? '▲' : '▼'} {incTrend.val}% vs. Vormonat
}
Ausgaben
{md.expenses.toLocaleString('de-DE')}
{expTrend &&
{expTrend.up ? '▲' : '▼'} {expTrend.val}% vs. Vormonat
}
{/* Chart + switcher */}
Verlauf im Monat
{chart === 'bars' && } {chart === 'line' && } {chart === 'area' && } {chart === 'donut' && }
{/* Top positions */} {(topIn || topOut) && (
Top-Positionen
{topIn && (
Größte Einnahme
{topIn.label}
+{topIn.amount} €
)} {topOut && (
Größte Ausgabe
{topOut.label}
−{topOut.amount} €
)}
)} {/* All transactions */}
Alle Buchungen
{md.transactions.map((x, i, arr) => { const isIn = x.kind === 'in'; return (
{x.label}
{x.sub} · {x.date}
{isIn ? '+' : '−'}{x.amount} €
); })}
); }