// 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 ( <> setSheet({ kind: 'in' })}>Neue Buchung {/* 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 KautionenTreuhand {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} € ); })} ); }