// Dashboard + Properties list pages function Dashboard({ data, setRoute }) { const { properties, transactions, reminders, tenants, store, settings } = data; const [showPropertyForm, setShowPropertyForm] = React.useState(false); const [showTxForm, setShowTxForm] = React.useState(false); const [showImport, setShowImport] = React.useState(false); const [catJob, setCatJob] = React.useState(null); // { jobId, total, done, categorized, finished, error } const uncategorizedCount = transactions.filter(t => !t.category).length; const categorizing = catJob && !catJob.finished; const runCategorize = async () => { try { const r = await window.categorizeTransactions(); setCatJob(r); } catch (e) { setCatJob({ error: e.message, finished: true }); } }; // Poll while job is running React.useEffect(() => { if (!catJob?.jobId || catJob.finished) return; let cancelled = false; const tick = async () => { try { const s = await window.getCategorizeStatus(catJob.jobId); if (cancelled) return; setCatJob(s); if (!s.finished) setTimeout(tick, 1500); else await store.reload(); } catch (e) { if (!cancelled) setCatJob(p => ({ ...(p || {}), error: e.message, finished: true })); } }; setTimeout(tick, 1000); return () => { cancelled = true; }; }, [catJob?.jobId, catJob?.finished, store]); const ownerFirstName = (settings?.ownerName || '').trim().split(/\s+/)[0] || ''; const greeting = (() => { const h = new Date().getHours(); if (h < 11) return 'Guten Morgen'; if (h < 18) return 'Guten Tag'; return 'Guten Abend'; })(); const totalUnits = properties.reduce((s, p) => s + p.units.length, 0); const occupiedUnits = properties.reduce((s, p) => s + p.units.filter(u => u.status === 'vermietet').length, 0); const occupancyRate = totalUnits > 0 ? Math.round((occupiedUnits / totalUnits) * 100) : 0; // Onboarding empty state — no properties yet if (properties.length === 0) { return (

Willkommen bei Hausgut.

Lege dein erstes Objekt an, um zu starten.
🏠
Noch keine Immobilien angelegt
Trage deine Immobilien ein, dann kannst du Wohnungen, Mieter, Buchungen und Dokumente verwalten.
setShowPropertyForm(false)} store={store} />
); } // Last 12 months const now = new Date(2026, 4, 1); const monthKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; const months = []; for (let i = 11; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); months.push({ key: monthKey(d), label: d.toLocaleDateString('de-DE', { month: 'short' }), date: d }); } const monthData = months.map(m => { const inc = transactions.filter(t => t.date.startsWith(m.key) && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const exp = transactions.filter(t => t.date.startsWith(m.key) && t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); return { ...m, income: inc, expense: exp, net: inc - exp }; }); const ytdIncome = transactions.filter(t => t.date.startsWith('2026') && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const ytdExpense = transactions.filter(t => t.date.startsWith('2026') && t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const ytdNet = ytdIncome - ytdExpense; const recentTxns = [...transactions].slice(0, 6); const upcoming = [...reminders].sort((a, b) => a.date.localeCompare(b.date)).slice(0, 4); return (

{greeting}{ownerFirstName ? `, ${ownerFirstName}` : ''}.

Mai 2026 · {properties.length} Objekte · {totalUnits} Einheiten
setShowPropertyForm(false)} store={store} /> setShowTxForm(false)} store={store} properties={properties} /> setShowImport(false)} store={store} properties={properties} />
Mieteinnahmen YTD
{fmtEUR(ytdIncome)}
{transactions.length === 0 ? 'noch keine Buchungen' : `${new Date().getFullYear()}`}
Ausgaben YTD
{fmtEUR(ytdExpense)}
{transactions.length === 0 ? '—' : `${new Date().getFullYear()}`}
Netto-Cashflow
{fmtEUR(ytdNet)}
{transactions.length === 0 ? '—' : 'lfd. Jahr'}
Auslastung
{totalUnits === 0 ? '—' : `${occupancyRate} %`}
{totalUnits === 0 ? 'keine Einheiten' : `${occupiedUnits} / ${totalUnits} Einheiten`}

Cashflow · letzte 12 Monate

{transactions.length === 0 ? (
Noch keine Buchungen
Erfasse deine erste Buchung — der Chart füllt sich mit der Zeit.
) : ( )}

Schnell-Upload

PDFs, Belege, Fotos — Objekt wird im nächsten Schritt zugeordnet
store.reload()} label="Datei hierher ziehen oder klicken zum Auswählen" subLabel="KI versucht, Kategorie + Objekt automatisch zu erkennen" />
{(uncategorizedCount > 0 || categorizing) && (
🤖 {categorizing ? `KI kategorisiert ${catJob.done} von ${catJob.total} Buchungen…` : catJob?.finished && !catJob?.error ? `✓ ${catJob.categorized} von ${catJob.total} Buchungen kategorisiert · ${uncategorizedCount} verbleibend` : `${uncategorizedCount} ${uncategorizedCount === 1 ? 'Buchung ohne Kategorie' : 'Buchungen ohne Kategorie'}`}
{categorizing ? 'Läuft im Hintergrund — das Dashboard aktualisiert sich automatisch wenn fertig.' : 'Lass die KI Kategorien vorschlagen anhand Empfänger + Verwendungszweck.'}
{catJob?.error &&
⚠ {catJob.error}
}
{!categorizing && uncategorizedCount > 0 && ( )}
{categorizing && (
)}
)}

Letzte Buchungen

{recentTxns.length === 0 ? (
Noch keine Buchungen erfasst.
) : (
{recentTxns.map(tx => { const prop = properties.find(p => p.id === tx.propertyId); return ( ); })}
DatumBeschreibungObjektBetrag
{fmtDateShort(tx.date)} {tx.description || tx.category || '—'} {prop ? prop.name : '—'} 0 ? 'var(--pos)' : 'var(--neg)' }}> {tx.amount > 0 ? '+' : ''}{fmtEUR(tx.amount)}
)}

Anstehende Fristen

{upcoming.length === 0 ? (
Keine anstehenden Termine.
) : (
{upcoming.map(r => { const d = new Date(r.date); const day = d.getDate(); const month = d.toLocaleDateString('de-DE', { month: 'short' }); return (
{day}
{month}
{r.title}
{r.category}
); })}
)}
); } function CashflowChart({ data }) { const W = 1100, H = 280; const pad = { l: 50, r: 20, t: 10, b: 30 }; const cw = W - pad.l - pad.r, ch = H - pad.t - pad.b; const max = Math.max(...data.map(d => Math.max(d.income, d.expense))) * 1.1; const niceMax = Math.ceil(max / 5000) * 5000; const bw = cw / data.length; const barW = bw * 0.32; const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => Math.round(niceMax * t)); const netPath = data.map((d, i) => { const x = pad.l + bw * i + bw / 2; const y = pad.t + ch - (d.net / niceMax) * ch; return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; }).join(' '); return (
{ticks.map((t, i) => { const y = pad.t + ch - (t / niceMax) * ch; return ; })} {ticks.map((t, i) => { const y = pad.t + ch - (t / niceMax) * ch; return {(t / 1000).toFixed(0)}k; })} {data.map((d, i) => ( {d.label} ))} {data.map((d, i) => { const xCenter = pad.l + bw * i + bw / 2; const incH = (d.income / niceMax) * ch; const expH = (d.expense / niceMax) * ch; return ( ); })} {data.map((d, i) => { const x = pad.l + bw * i + bw / 2; const y = pad.t + ch - (d.net / niceMax) * ch; return ; })}
Einnahmen Ausgaben Netto
); } function PropertiesList({ data, setRoute }) { const { properties, tenants, transactions, store } = data; const [showForm, setShowForm] = React.useState(false); return (

Objekte

{properties.length} {properties.length === 1 ? 'Hauptimmobilie' : 'Hauptimmobilien'} · {properties.reduce((s, p) => s + p.units.length, 0)} Einheiten
setShowForm(false)} store={store} /> {properties.length === 0 && (
🏠
Noch keine Objekte
Lege deine erste Immobilie an, um Wohnungen, Mieter und Buchungen zu verwalten.
)}
{properties.map(prop => { const occupied = prop.units.filter(u => u.status === 'vermietet').length; const total = prop.units.length; const monthlyRent = prop.units.reduce((s, u) => s + (u.rent || 0), 0); const totalSize = prop.units.reduce((s, u) => s + (u.size || 0), 0); const occRatio = total > 0 ? occupied / total : 0; return ( ); })}
); } Object.assign(window, { Dashboard, PropertiesList, CashflowChart });