// Tenants, Calendar, Maintenance, Reports function TenantsPage({ data, route, setRoute }) { const { properties, tenants, store } = data; const [showAdd, setShowAdd] = React.useState(false); const [editing, setEditing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); // If route carries tenantId → render detail view if (route?.tenantId) { return ; } const allTenants = Object.values(tenants); // Build a map of tenant_id → assigned unit/property (if any) const assignmentByTenant = {}; for (const p of properties) { for (const u of p.units) { if (u.tenant) assignmentByTenant[u.tenant] = { unit: u, property: p }; } } return (

Mieter

{allTenants.length} {allTenants.length === 1 ? 'Mieter' : 'Mieter'} insgesamt
setShowAdd(false)} store={store} initial={editing} /> setDeleting(null)} onConfirm={async () => { await store.deleteTenant(deleting.id); }} title="Mieter löschen" message={`„${deleting?.name}" endgültig löschen? Wohnungen werden auf Leerstand gesetzt.`} confirmLabel="Endgültig löschen" /> {allTenants.length === 0 ? (
👥
Noch keine Mieter
Lege deinen ersten Mieter an. Du kannst ihn dann einer Wohnung zuordnen.
) : (
{allTenants.map(tenant => { const a = assignmentByTenant[tenant.id]; return ( setRoute({ page: 'tenants', tenantId: tenant.id })}> ); })}
Mieter Objekt / Einheit Mieter seit Vertragsende Kaution
{tenant.name.split(' ').map(s => s[0]).slice(0, 2).join('')}
{tenant.name}
{tenant.email || tenant.phone || ''}
e.stopPropagation()}> {a ? ( ) : — nicht zugeordnet —} {tenant.since ? fmtDate(tenant.since) : '—'} {tenant.until ? {fmtDate(tenant.until)} : {a ? 'unbefristet' : '—'}} {tenant.deposit ? fmtEUR(tenant.deposit) : '—'} e.stopPropagation()} style={{ whiteSpace: 'nowrap' }}>
)}
); } // ───── Tenant Detail Page ───── function TenantDetail({ data, tenantId, setRoute }) { const { tenants, properties, transactions, store } = data; const [showEdit, setShowEdit] = React.useState(false); const [showDelete, setShowDelete] = React.useState(false); const tenant = tenants[tenantId]; if (!tenant) return
Mieter nicht gefunden.
; // Find unit + property let unit = null, property = null; for (const p of properties) { const u = p.units.find(u => u.tenant === tenantId); if (u) { unit = u; property = p; break; } } // Cloud documents linked to this tenant's unit const cloudDocs = useDocuments(unit ? { propertyId: property.id, unitId: unit.id } : {}); // Transactions with this tenant: filter by unit_id (rent payments) OR counterparty matches name const nameNorm = tenant.name.toLowerCase(); const tenantTxns = transactions.filter(t => (unit && t.unitId === unit.id) || (t.counterparty || '').toLowerCase().includes(nameNorm) ); const incomeTxns = tenantTxns.filter(t => t.type === 'einnahme'); const expenseTxns = tenantTxns.filter(t => t.type === 'ausgabe'); const totalIncome = incomeTxns.reduce((s, t) => s + t.amount, 0); const totalExpense = expenseTxns.reduce((s, t) => s + Math.abs(t.amount), 0); const netRevenue = totalIncome - totalExpense; // Years as tenant const sinceDate = tenant.since ? new Date(tenant.since) : null; const untilDate = tenant.until ? new Date(tenant.until) : null; const refDate = untilDate || new Date(); const yearsAsTenant = sinceDate ? ((refDate - sinceDate) / (1000 * 60 * 60 * 24 * 365.25)).toFixed(1) : null; const isActive = !untilDate || untilDate > new Date(); // Rent saldo: expected rent (months × Kaltmiete) vs actual income let expectedRent = 0; if (unit && sinceDate) { const monthsActive = Math.max(0, Math.floor((refDate - sinceDate) / (1000 * 60 * 60 * 24 * 30.44))); expectedRent = monthsActive * (unit.rent || 0); } const rentSaldo = totalIncome - expectedRent; // Birthday gimmick: days until next birthday let bdayInfo = null; if (tenant.birthdate) { const bd = new Date(tenant.birthdate); const today = new Date(); today.setHours(0,0,0,0); const next = new Date(today.getFullYear(), bd.getMonth(), bd.getDate()); if (next < today) next.setFullYear(next.getFullYear() + 1); const days = Math.round((next - today) / (1000 * 60 * 60 * 24)); const ageNext = next.getFullYear() - bd.getFullYear(); bdayInfo = { days, ageNext, dateNext: next }; } // Contract end alert: <90 days remaining let contractAlert = null; if (untilDate && untilDate > new Date()) { const days = Math.round((untilDate - new Date()) / (1000 * 60 * 60 * 24)); if (days <= 90) contractAlert = { days }; } return (
setShowEdit(false)} store={store} initial={tenant} /> setShowDelete(false)} onConfirm={async () => { await store.deleteTenant(tenant.id); setRoute({ page: 'tenants' }); }} title="Mieter löschen" message={`„${tenant.name}" endgültig löschen? Wohnung wird auf Leerstand gesetzt.`} confirmLabel="Endgültig löschen" />
{tenant.name.split(/\s+/).map(s => s[0]).filter(Boolean).slice(0, 2).join('')}

{tenant.name}

{isActive ? Aktiver Mieter : Ehemaliger Mieter} {yearsAsTenant && seit {yearsAsTenant} {Number(yearsAsTenant) === 1 ? 'Jahr' : 'Jahren'}} {!unit && Keiner Wohnung zugeordnet}
{tenant.email && ✉ E-Mail} {tenant.phone && 📞 Anrufen}
{/* Alerts */} {(bdayInfo?.days <= 30 || contractAlert) && (
{bdayInfo && bdayInfo.days <= 30 && (
🎂 Geburtstag {bdayInfo.days === 0 ? 'heute' : bdayInfo.days === 1 ? 'morgen' : `in ${bdayInfo.days} Tagen`} — {bdayInfo.ageNext}. Geburtstag am {bdayInfo.dateNext.toLocaleDateString('de-DE')}
)} {contractAlert && (
Vertragsende in {contractAlert.days} Tagen — Verlängerung oder Neuvermietung jetzt anstoßen
)}
)} {/* KPI cards */}
Mieteinnahmen gesamt
{fmtEUR(totalIncome)}
{incomeTxns.length} Buchungen
Ausgaben mit Bezug
{fmtEUR(totalExpense)}
{expenseTxns.length} Buchungen (z.B. Reparaturen)
Netto-Beitrag
= 0 ? 'var(--pos)' : 'var(--neg)' }}>{fmtEUR(netRevenue)}
seit Mietbeginn
Mietsaldo
= -10 ? 'var(--pos)' : 'var(--neg)', fontSize: 22 }}> {rentSaldo >= -10 ? '✓ ausgeglichen' : fmtEUR(rentSaldo)}
{unit ? `Soll: ${fmtEUR(expectedRent)}` : 'keine Wohnung'}
{/* Left column: Stammdaten + Wohnung */}

Stammdaten

E-Mail{tenant.email || '—'}
Telefon{tenant.phone || '—'}
Geburtsdatum{tenant.birthdate ? fmtDate(tenant.birthdate) : '—'}
Mietbeginn{tenant.since ? fmtDate(tenant.since) : '—'}
Mietende{tenant.until ? fmtDate(tenant.until) : 'unbefristet'}
Kaution{tenant.deposit ? fmtEUR(tenant.deposit) : '—'}
IBAN{tenant.iban || '—'}
Vorherige Adresse{tenant.addressPrev || '—'}
{tenant.notes && (

Notiz

„{tenant.notes}"
)}
{/* Right column: Wohnung-Karte */}

Wohnung

{unit && property ? (

{unit.label}

{property.name} · {property.address}
Etage / Lage{unit.floor || '—'}
Wohnfläche{unit.size ? `${unit.size} m²` : '—'}
Zimmer{unit.rooms || '—'}
Kaltmiete{unit.rent ? `${fmtEUR(unit.rent)}/Monat` : '—'}
) : (
Keiner Wohnung zugeordnet
Geh zu einem Objekt → Wohnung → Bearbeiten → Mieter wählen.
)}

Schnellaktionen

{unit && ( )} {unit && ( )} {tenant.email && ( ✉ E-Mail schreiben )}
{/* Buchungen */}

Buchungen mit diesem Mieter

{tenantTxns.length} {tenantTxns.length === 1 ? 'Eintrag' : 'Einträge'}
{tenantTxns.length === 0 ? (
Noch keine Buchungen mit Bezug zu diesem Mieter.
) : (
{tenantTxns.slice(0, 100).map(t => ( ))}
DatumBeschreibungKategorieEmpfängerBetrag
{fmtDateShort(t.date)} {t.description || '—'} {t.category ? {t.category} : } {t.counterparty || '—'} = 0 ? 'var(--pos)' : 'var(--neg)', whiteSpace: 'nowrap' }}> {t.amount >= 0 ? '+' : ''}{fmtEUR(t.amount)}
)}
{/* Cloud documents linked to the unit */} {unit && cloudDocs.items.length > 0 && (

Dokumente

{cloudDocs.items.length} aus Cloud
{cloudDocs.items.map(doc => (
{doc.name}
{(doc.sizeBytes / 1024).toFixed(0)} KB · {fmtDateShort(doc.uploadedAt)}
{doc.category || 'Cloud'}
))}
)}
); } function CalendarPage({ data }) { const { reminders, transactions, properties, store } = data; const today = new Date(); const [cursor, setCursor] = React.useState(new Date(today.getFullYear(), today.getMonth(), 1)); const [showForm, setShowForm] = React.useState(false); const [editing, setEditing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); const hasRentDue = properties?.some(p => p.units.some(u => u.rent && u.rent > 0)); const openAdd = () => { setEditing(null); setShowForm(true); }; const openEdit = (r) => { setEditing(r); setShowForm(true); }; const year = cursor.getFullYear(); const month = cursor.getMonth(); const firstDay = new Date(year, month, 1); const startWeekday = (firstDay.getDay() + 6) % 7; // Mon=0 const daysInMonth = new Date(year, month + 1, 0).getDate(); const cells = []; for (let i = 0; i < startWeekday; i++) { const d = new Date(year, month, -startWeekday + i + 1); cells.push({ date: d, muted: true }); } for (let i = 1; i <= daysInMonth; i++) { cells.push({ date: new Date(year, month, i), muted: false }); } while (cells.length % 7 !== 0) { const last = cells[cells.length - 1].date; cells.push({ date: new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1), muted: true }); } const monthName = cursor.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); const eventsForDate = (d) => { const key = d.toISOString().slice(0, 10); const evs = reminders.filter(r => r.date === key).map(r => ({ ...r, kind: 'reminder' })); if (d.getDate() === 1 && hasRentDue) evs.push({ id: `rent-${key}`, title: 'Mieteingänge', kind: 'rent' }); return evs; }; return (

Kalender

Fristen, Wartungen, Mietzahlungen
setShowForm(false)} store={store} properties={properties} initial={editing} /> setDeleting(null)} onConfirm={async () => { await store.deleteReminder(deleting.id); }} title="Termin löschen" message={`„${deleting?.title}" endgültig löschen?`} confirmLabel="Endgültig löschen" />
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map(d =>
{d}
)} {cells.map((c, i) => { const isToday = c.date.toDateString() === today.toDateString(); const evs = c.muted ? [] : eventsForDate(c.date); return (
{c.date.getDate()}
{evs.map((e, j) => (
{e.title}
))}
); })}

Anstehende Fristen

{reminders.length === 0 ? (
Noch keine Termine
Lege Erinnerungen an Wartungen, Vertragsfristen, Versicherungserneuerungen etc. an.
) : (
{[...reminders].sort((a, b) => a.date.localeCompare(b.date)).map(r => { const prop = properties.find(p => p.id === r.propertyId); return ( ); })}
DatumTitelKategoriePrioritätObjekt
{fmtDate(r.date)} {r.title} {r.category && {r.category}} {r.priority === 'hoch' ? Hoch : r.priority === 'mittel' ? Mittel : Niedrig} {prop ? prop.name : '—'}
)}
); } function MaintenancePage({ data, setRoute }) { const { maintenance, properties, contacts, store } = data; const [showForm, setShowForm] = React.useState(false); const [editing, setEditing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); const openAdd = () => { setEditing(null); setShowForm(true); }; const openEdit = (m) => { setEditing(m); setShowForm(true); }; const open = maintenance.filter(m => m.status !== 'erledigt'); return (

Wartung & Reparatur

{open.length} {open.length === 1 ? 'offener Vorgang' : 'offene Vorgänge'} · {maintenance.length} insgesamt
setShowForm(false)} store={store} properties={properties} contacts={contacts} initial={editing} /> setDeleting(null)} onConfirm={async () => { await store.deleteMaintenance(deleting.id); }} title="Wartungsvorgang löschen" message={`„${deleting?.title}" endgültig löschen?`} confirmLabel="Endgültig löschen" /> {maintenance.length === 0 ? (
🔧
Keine Wartungsvorgänge
Lege geplante oder akute Reparaturen an, weise sie einem Handwerker zu, verfolge den Status.
) : (
{maintenance.map(m => { const prop = properties.find(p => p.id === m.propertyId); const unit = prop?.units.find(u => u.id === m.unitId); const contact = contacts?.find(c => c.id === m.contactId); const tenantId = unit?.tenant; return ( ); })}
TitelObjektBeauftragt anStatusFälligKosten
{m.title} {prop ? prop.name : '—'}{unit ? ` · ${unit.label}` : ''} {contact ? contact.company : '—'} {m.status === 'erledigt' ? Erledigt : m.status === 'in-arbeit' ? In Arbeit : Geplant} {m.dueDate ? fmtDate(m.dueDate) : '—'} {m.cost ? fmtEUR(m.cost) : '—'} store.reload()} />
)}
); } // ─── Steuerberater-Export Section ──────────────────────────────────── // Wählt Monat + Objekt aus, lädt ZIP mit Buchungen-CSV + verknüpften Belegen. function SteuerExportSection({ properties, transactions }) { const today = new Date(); const [year, setYear] = React.useState(today.getFullYear()); const [month, setMonth] = React.useState(today.getMonth() === 0 ? 12 : today.getMonth()); // default: letzter Monat React.useEffect(() => { if (today.getMonth() === 0 && month === 12) setYear(today.getFullYear() - 1); }, []); const [propertyId, setPropertyId] = React.useState(''); const ym = `${year}-${String(month).padStart(2, '0')}`; const txInMonth = transactions.filter(t => t.date.startsWith(ym) && (!propertyId || t.propertyId === propertyId)); const withDocsCount = txInMonth.filter(t => (t.documentCount || 0) > 0).length; const totalIncome = txInMonth.filter(t => t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const totalExpense = txInMonth.filter(t => t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const monthLabels = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; const downloadUrl = window.monthlyExportZipUrl({ year, month, propertyId: propertyId || undefined }); return (

📦 Steuerberater-Mappe

ZIP mit Buchungen + Belegen + Zusammenfassung — direkt zum Versand an deinen Steuerberater
Buchungen
{txInMonth.length}
Mit Beleg
{withDocsCount} / {txInMonth.length}
Einnahmen
{fmtEUR(totalIncome)}
Ausgaben
{fmtEUR(totalExpense)}
{txInMonth.length > 0 && withDocsCount < txInMonth.length && ( ⚠ {txInMonth.length - withDocsCount} ohne Beleg )} 📥 ZIP herunterladen
); } // ─── Berichte / Reports ───────────────────────────────────────────── // Filterleiste oben (Zeitraum-Presets + Custom + Objekt-Filter), KPI-Karten, // Cashflow-Chart, gefilterte Tx-Tabelle mit Klick-zu-Bearbeiten + Löschen, // Pro-Objekt + Kategorien-Auswertung, CSV-Export. const RANGE_PRESETS = [ { id: 'thisMonth', label: 'Aktueller Monat' }, { id: 'lastMonth', label: 'Letzter Monat' }, { id: 'thisQuarter',label: 'Aktuelles Quartal' }, { id: 'thisYear', label: 'Aktuelles Jahr' }, { id: 'lastYear', label: 'Letztes Jahr' }, { id: 'last12', label: 'Letzte 12 Monate' }, { id: 'all', label: 'Alles' }, { id: 'custom', label: 'Benutzerdefiniert' }, ]; function rangeForPreset(preset, custom) { const today = new Date(); const y = today.getFullYear(); const m = today.getMonth(); const fmt = (d) => d.toISOString().slice(0, 10); switch (preset) { case 'thisMonth': return { from: fmt(new Date(y, m, 1)), to: fmt(new Date(y, m + 1, 0)) }; case 'lastMonth': return { from: fmt(new Date(y, m - 1, 1)), to: fmt(new Date(y, m, 0)) }; case 'thisQuarter':{ const qs = Math.floor(m / 3) * 3; return { from: fmt(new Date(y, qs, 1)), to: fmt(new Date(y, qs + 3, 0)) }; } case 'thisYear': return { from: `${y}-01-01`, to: `${y}-12-31` }; case 'lastYear': return { from: `${y - 1}-01-01`, to: `${y - 1}-12-31` }; case 'last12': return { from: fmt(new Date(y, m - 11, 1)), to: fmt(today) }; case 'all': return { from: '1900-01-01', to: '2999-12-31' }; case 'custom': return custom || { from: '', to: '' }; default: return { from: `${y}-01-01`, to: `${y}-12-31` }; } } function downloadCsv(filename, rows) { const escape = (v) => { const s = v == null ? '' : String(v); return /[";,\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; const csv = rows.map(r => r.map(escape).join(';')).join('\r\n'); const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0); } function SmartBarChart({ months }) { // Empty / no data → nice empty state instead of broken axes if (months.length === 0) { return
Keine Buchungen im gewählten Zeitraum.
; } const W = 1100, H = 240; const pad = { l: 56, r: 16, t: 12, b: 28 }; const cw = W - pad.l - pad.r, ch = H - pad.t - pad.b; const maxRaw = Math.max(0, ...months.map(d => Math.max(d.income, d.expense))); if (maxRaw === 0) { return
Keine Werte > 0 im gewählten Zeitraum.
; } // Nice axis: auto-scale to a power-of-10 step const niceStep = (raw) => { const e = Math.pow(10, Math.floor(Math.log10(raw))); const m = raw / e; if (m < 1.5) return e * 0.2; if (m < 3) return e * 0.5; if (m < 7) return e; return e * 2; }; const step = niceStep(maxRaw); const niceMax = Math.ceil(maxRaw / step) * step; const nTicks = Math.round(niceMax / step); const ticks = Array.from({ length: nTicks + 1 }, (_, i) => i * step); const bw = cw / months.length; const barW = Math.min(28, bw * 0.32); const fmtAxis = (v) => v >= 1000 ? `${(v / 1000).toFixed(v >= 10000 ? 0 : 1)}k` : String(v); return (
{/* Grid lines */} {ticks.map((t, i) => { const y = pad.t + ch - (t / niceMax) * ch; return ; })} {/* Y-axis labels */} {ticks.map((t, i) => { const y = pad.t + ch - (t / niceMax) * ch; return {fmtAxis(t)}; })} {/* X-axis labels */} {months.map((d, i) => ( {d.label} ))} {/* Bars */} {months.map((d, i) => { const xc = pad.l + bw * i + bw / 2; const incH = (d.income / niceMax) * ch; const expH = (d.expense / niceMax) * ch; return ( ); })} {/* Net line */} { const x = pad.l + bw * i + bw / 2; const y = pad.t + ch - Math.max(0, d.net) / niceMax * ch; return `${x},${y}`; }).join(' ')} />
Einnahmen Ausgaben Netto
); } function ReportsPage({ data }) { const { properties, transactions, store } = data; const [preset, setPreset] = React.useState('thisYear'); const [custom, setCustom] = React.useState({ from: '', to: '' }); const [propertyFilter, setPropertyFilter] = React.useState('all'); const [typeFilter, setTypeFilter] = React.useState('all'); // all | einnahme | ausgabe const [search, setSearch] = React.useState(''); const [sortBy, setSortBy] = React.useState('date-desc'); const [editingTx, setEditingTx] = React.useState(null); const [showTxForm, setShowTxForm] = React.useState(false); const [deletingTx, setDeletingTx] = React.useState(null); const range = rangeForPreset(preset, custom); // Apply filters let filtered = transactions; filtered = filtered.filter(t => t.date >= range.from && t.date <= range.to); if (propertyFilter !== 'all') filtered = filtered.filter(t => t.propertyId === propertyFilter); if (typeFilter !== 'all') filtered = filtered.filter(t => t.type === typeFilter); if (search.trim()) { const q = search.trim().toLowerCase(); filtered = filtered.filter(t => (t.description || '').toLowerCase().includes(q) || (t.counterparty || '').toLowerCase().includes(q) || (t.category || '').toLowerCase().includes(q) ); } // Sort const sorted = [...filtered].sort((a, b) => { switch (sortBy) { case 'date-asc': return a.date.localeCompare(b.date); case 'date-desc': return b.date.localeCompare(a.date); case 'amount-asc': return Math.abs(a.amount) - Math.abs(b.amount); case 'amount-desc':return Math.abs(b.amount) - Math.abs(a.amount); default: return 0; } }); // Aggregate totals const totalIncome = filtered.filter(t => t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const totalExpense = filtered.filter(t => t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const totalNet = totalIncome - totalExpense; const txCount = filtered.length; const uncategorizedCount = filtered.filter(t => !t.category).length; // Monthly aggregation for chart const monthMap = new Map(); filtered.forEach(t => { const key = t.date.slice(0, 7); if (!monthMap.has(key)) monthMap.set(key, { key, income: 0, expense: 0 }); const e = monthMap.get(key); if (t.type === 'einnahme') e.income += t.amount; else e.expense += Math.abs(t.amount); }); const months = Array.from(monthMap.values()) .sort((a, b) => a.key.localeCompare(b.key)) .map(m => ({ ...m, net: m.income - m.expense, label: new Date(m.key + '-01').toLocaleDateString('de-DE', { month: 'short', year: '2-digit' }), })); // Per-property breakdown const propRows = properties.map(p => { const propTxns = filtered.filter(t => t.propertyId === p.id); const inc = propTxns.filter(t => t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const exp = propTxns.filter(t => t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const monthlyRent = p.units.reduce((s, u) => s + (u.rent || 0), 0); const grossYield = p.purchasePrice > 0 ? ((monthlyRent * 12) / p.purchasePrice * 100) : 0; return { ...p, income: inc, expense: exp, net: inc - exp, count: propTxns.length, grossYield }; }).filter(p => p.count > 0 || propertyFilter === p.id || propertyFilter === 'all'); const maxPropIncome = Math.max(1, ...propRows.map(p => p.income)); // Per-category breakdown (expenses only) const catMap = {}; filtered.filter(t => t.type === 'ausgabe').forEach(t => { const c = t.category || 'Ohne Kategorie'; catMap[c] = (catMap[c] || 0) + Math.abs(t.amount); }); const catRows = Object.entries(catMap).sort((a, b) => b[1] - a[1]); const maxCat = catRows.length ? catRows[0][1] : 1; const exportCsv = () => { const fname = `hausgut-buchungen_${range.from}_${range.to}.csv`; const rows = [['Datum', 'Art', 'Beschreibung', 'Empfänger', 'Kategorie', 'Objekt', 'Betrag']]; sorted.forEach(t => { const prop = properties.find(p => p.id === t.propertyId); rows.push([ t.date, t.type === 'einnahme' ? 'Einnahme' : 'Ausgabe', t.description || '', t.counterparty || '', t.category || '', prop ? prop.name : '', t.amount.toFixed(2).replace('.', ','), ]); }); downloadCsv(fname, rows); }; // Empty: no transactions at all if (transactions.length === 0) { return (

Berichte

Cashflow, Rendite, Steuerübersicht
📊
Noch keine Buchungen
Erfasse Buchungen oder importiere eine Bank-CSV — dann werden hier Auswertungen erscheinen.
); } return (

Berichte

{txCount} {txCount === 1 ? 'Buchung' : 'Buchungen'} im gewählten Zeitraum
{ setShowTxForm(false); setEditingTx(null); }} store={store} properties={properties} initial={editingTx} /> setDeletingTx(null)} onConfirm={async () => { await store.deleteTransaction(deletingTx.id); }} title="Buchung löschen" message={`„${deletingTx?.description || deletingTx?.category || 'Buchung'}" über ${deletingTx ? fmtEUR(deletingTx.amount) : ''} endgültig löschen?`} confirmLabel="Endgültig löschen" /> {/* Steuerberater-Export */} {/* Filterleiste */}
{RANGE_PRESETS.map(p => ( ))}
{preset === 'custom' && (
Von setCustom(c => ({ ...c, from: e.target.value }))} /> bis setCustom(c => ({ ...c, to: e.target.value }))} />
)}
setSearch(e.target.value)} />
Zeitraum: {range.from === '1900-01-01' ? '—' : range.from} bis {range.to === '2999-12-31' ? '—' : range.to}
{/* KPI-Karten */}
Einnahmen
{fmtEUR(totalIncome)}
{filtered.filter(t => t.type === 'einnahme').length} Buchungen
Ausgaben
{fmtEUR(totalExpense)}
{filtered.filter(t => t.type === 'ausgabe').length} Buchungen
Netto-Cashflow
= 0 ? 'var(--pos)' : 'var(--neg)' }}>{fmtEUR(totalNet)}
{totalIncome > 0 ? `Marge: ${((totalNet / totalIncome) * 100).toFixed(1)} %` : '—'}
Buchungen
{txCount}
{uncategorizedCount > 0 ? `${uncategorizedCount} ohne Kategorie` : 'alle kategorisiert'}
{/* Cashflow-Chart */}

Cashflow nach Monat

{/* Transaktionen-Tabelle */}

Buchungen

{sorted.length === 0 ? (
Keine Buchungen im Filter
Lockere Zeitraum, Objekt oder Suchbegriff.
) : (
{sorted.slice(0, 200).map(t => { const prop = properties.find(p => p.id === t.propertyId); const docCount = t.documentCount || 0; return ( { setEditingTx(t); setShowTxForm(true); }}> ); })}
Datum Beschreibung Empfänger Kategorie Objekt 📎 Betrag
{fmtDateShort(t.date)} {t.description || '—'} {t.counterparty || '—'} {t.category ? {t.category} : } {prop ? prop.name : '—'} 0 ? `${docCount} Beleg(e) verknüpft` : 'Kein Beleg verknüpft'} style={{ textAlign: 'center', color: docCount > 0 ? 'var(--accent)' : 'var(--ink-5)' }}> {docCount > 0 ? `📎 ${docCount}` : '—'} = 0 ? 'var(--pos)' : 'var(--neg)', whiteSpace: 'nowrap' }}> {t.amount >= 0 ? '+' : ''}{fmtEUR(t.amount)} e.stopPropagation()} style={{ whiteSpace: 'nowrap' }}>
{sorted.length > 200 && (
Zeige erste 200 von {sorted.length} Buchungen — engere Filter setzen für vollständige Übersicht oder CSV exportieren.
)}
)}
{/* Pro-Objekt + Kategorien */}

Auswertung pro Objekt

{propRows.length === 0 ? (
Keine Objekt-Daten im Zeitraum.
) : (
{propRows.map(p => (
{p.name}
Netto: = 0 ? 'var(--pos)' : 'var(--neg)' }}>{fmtEUR(p.net)} {p.grossYield > 0 && ` · ${p.grossYield.toFixed(2)} %`}
{fmtEUR(p.income)}
))}
)}

Ausgaben nach Kategorie

{catRows.length === 0 ? (
Keine Ausgaben im Zeitraum.
) : (
{catRows.map(([cat, amt]) => (
{cat}
{fmtEUR(amt)}
))}
Summe {fmtEUR(totalExpense)}
)}
); } Object.assign(window, { TenantsPage, TenantDetail, CalendarPage, MaintenancePage, ReportsPage });