// 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(true)}>
Erstes Objekt anlegen
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
setShowImport(true)}>📊 CSV importieren
setShowPropertyForm(true)}> Objekt
setShowTxForm(true)}> Buchung erfassen
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
setRoute({ page: 'reports' })}>Alle Berichte →
{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 && (
Jetzt kategorisieren
)}
{categorizing && (
)}
)}
Letzte Buchungen
Alle anzeigen →
{recentTxns.length === 0 ? (
Noch keine Buchungen erfasst.
) : (
Datum Beschreibung Objekt Betrag
{recentTxns.map(tx => {
const prop = properties.find(p => p.id === tx.propertyId);
return (
{fmtDateShort(tx.date)}
{tx.description || tx.category || '—'}
{prop ? prop.name : '—'}
0 ? 'var(--pos)' : 'var(--neg)' }}>
{tx.amount > 0 ? '+' : ''}{fmtEUR(tx.amount)}
);
})}
)}
Anstehende Fristen
setRoute({ page: 'calendar' })}>Kalender →
{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 (
);
})}
)}
);
}
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(true)}> Objekt anlegen
setShowForm(false)} store={store} />
{properties.length === 0 && (
🏠
Noch keine Objekte
Lege deine erste Immobilie an, um Wohnungen, Mieter und Buchungen zu verwalten.
setShowForm(true)}> Erstes Objekt anlegen
)}
{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 (
setRoute({ page: 'properties', propertyId: prop.id })}>
{prop.name}
{prop.address}
{occupied}/{total}
Einheiten
{fmtEUR(monthlyRent)}
Soll/Monat
{totalSize} m²
Wohnfläche
);
})}
);
}
Object.assign(window, { Dashboard, PropertiesList, CashflowChart });