// Analytik — KPIs, YoY trends, predictions function AnalyticsPage({ data, setRoute }) { const { properties, transactions, tenants } = data; // Empty state — analytics needs real data to render if (properties.length === 0 || transactions.length < 5) { return (

Analytik

KPIs, Trends, Prognosen
📈
Noch nicht genug Daten
Sobald du einige Objekte und Buchungen erfasst hast, zeigen wir hier Cashflow-Trends, Auslastung, Prognosen und YoY-Vergleiche.
); } // ───── Date helpers ───── const today = new Date(2026, 4, 1); const monthKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; // YTD 2026 (Jan–Apr) vs YTD 2025 (Jan–Apr) const inRange = (date, year, monthsCount) => { const d = new Date(date); return d.getFullYear() === year && d.getMonth() < monthsCount; }; const ytdMonths = 4; // Jan–Apr const cur = transactions.filter(t => inRange(t.date, 2026, ytdMonths)); const prev = transactions.filter(t => inRange(t.date, 2025, ytdMonths)); const sumIncome = (txns) => txns.filter(t => t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const sumExpense = (txns) => txns.filter(t => t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const curIncome = sumIncome(cur); const prevIncome = sumIncome(prev); const curExpense = sumExpense(cur); const prevExpense = sumExpense(prev); const curNet = curIncome - curExpense; const prevNet = prevIncome - prevExpense; const pct = (now, then) => then === 0 ? 0 : ((now - then) / then) * 100; // Occupancy stats 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 = (occupiedUnits / totalUnits) * 100; // Average rent / m² const totalRent = properties.reduce((s, p) => s + p.units.reduce((x, u) => x + u.rent, 0), 0); const totalArea = properties.reduce((s, p) => s + p.units.reduce((x, u) => x + u.size, 0), 0); const rentPerSqm = totalRent / totalArea; // Total portfolio value const totalValue = properties.reduce((s, p) => s + p.purchasePrice, 0); const grossYield = ((totalRent * 12) / totalValue) * 100; // 24-month income/expense for trend chart with prediction const histMonths = []; for (let i = 23; i >= -7; i--) { const d = new Date(today.getFullYear(), today.getMonth() - i, 1); const key = monthKey(d); const inc = transactions.filter(t => t.date.startsWith(key) && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const exp = transactions.filter(t => t.date.startsWith(key) && t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const isFuture = d > today; histMonths.push({ key, label: d.toLocaleDateString('de-DE', { month: 'short' }), year: d.getFullYear(), date: d, income: isFuture ? null : inc, expense: isFuture ? null : exp, net: isFuture ? null : inc - exp, isFuture, }); } // Predictions: simple linear extrapolation + occupancy assumption const predIncome = totalRent * 0.97; // accounting for vacancy const avgMonthlyExp = curExpense / ytdMonths; histMonths.forEach(m => { if (m.isFuture) { // assume rent stays + small seasonal variance for expense const seasonal = m.date.getMonth() === 11 ? 1.4 : m.date.getMonth() === 6 ? 1.2 : 0.9; m.predIncome = predIncome; m.predExpense = avgMonthlyExp * seasonal; m.predNet = m.predIncome - m.predExpense; } }); // Cashflow projection for full year 2026 const projAnnualIncome = curIncome + predIncome * (12 - ytdMonths); const projAnnualExpense = curExpense + (avgMonthlyExp * (12 - ytdMonths) * 1.05); const projAnnualNet = projAnnualIncome - projAnnualExpense; // Per-property comparison const propStats = properties.map(p => { const cur = transactions.filter(t => t.propertyId === p.id && t.date.startsWith('2026') && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const prev = transactions.filter(t => t.propertyId === p.id && t.date.startsWith('2025') && t.date < '2025-05-01' && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const occ = p.units.filter(u => u.status === 'vermietet').length / p.units.length; return { ...p, curIncome: cur, prevIncome: prev, change: pct(cur, prev), occupancy: occ }; }); return (
Stand 1. Mai 2026

Analytik

Trends, Vergleiche zum Vorjahr und Vorhersagen

Cashflow-Trend mit Prognose

Mai 2024 – Dez 2026 · ✦ Vorhersage

Jahresprognose 2026

Verteilungen

Objekte im Vergleich zum Vorjahr

Vorausschauende Hinweise

Modell-basiert · 60 Tage Horizont
); } // ───── KPI Grid ───── function KpiBigGrid({ items }) { return (
{items.map((k, i) => (
{k.label}
{k.value}
{k.prevLabel} {!k.hideDelta && k.delta !== 0 && ( )}
))}
); } function DeltaBadge({ delta, negativeIsGood }) { const positive = delta > 0; const isGood = negativeIsGood ? !positive : positive; const arrow = positive ? '↑' : '↓'; return ( {arrow} {Math.abs(delta).toFixed(1)}% ); } // ───── Trend Chart with Prediction ───── function TrendChart({ data, today }) { const W = 1200, H = 320; const pad = { l: 50, r: 30, t: 16, b: 36 }; const cw = W - pad.l - pad.r, ch = H - pad.t - pad.b; const allVals = data.flatMap(d => [d.income, d.expense, d.predIncome, d.predExpense].filter(v => v != null)); const max = Math.max(...allVals) * 1.1; const niceMax = Math.ceil(max / 5000) * 5000; const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => Math.round(niceMax * t)); const xFor = (i) => pad.l + (cw / (data.length - 1)) * i; const yFor = (v) => pad.t + ch - (v / niceMax) * ch; const buildLine = (key) => data.map((d, i) => { if (d[key] == null) return null; return `${i === 0 || data[i - 1][key] == null ? 'M' : 'L'} ${xFor(i)} ${yFor(d[key])}`; }).filter(Boolean).join(' '); const incPath = buildLine('income'); const expPath = buildLine('expense'); const predIncPath = buildLine('predIncome'); const predExpPath = buildLine('predExpense'); // Find today line const todayIdx = data.findIndex(d => d.isFuture); const todayX = todayIdx > 0 ? xFor(todayIdx - 0.5) : null; return (
{/* gridlines */} {ticks.map((t, i) => { const y = yFor(t); return ; })} {/* prediction shaded zone */} {todayX != null && ( )} {/* axes */} {ticks.map((t, i) => {(t / 1000).toFixed(0)}k)} {data.map((d, i) => { if (i % 3 !== 0) return null; return ( {d.label}{d.date.getMonth() === 0 ? ` '${String(d.year).slice(2)}` : ''} ); })} {/* "Heute" marker */} {todayX != null && ( heute )} {/* historical lines */} {/* prediction lines (dashed) */} {/* dots on income line */} {data.map((d, i) => d.income != null && ( ))}
Einnahmen Ausgaben Prognose
); } // ───── Projection Card ───── function ProjectionCard({ curIncome, curExpense, projAnnualIncome, projAnnualExpense, projAnnualNet, ytdMonths }) { const completion = (ytdMonths / 12) * 100; return (
Erwarteter Jahresnetto 2026
{fmtEUR(projAnnualNet)}
Hochrechnung mit aktueller Auslastung
YTD ({completion.toFixed(0)} %) Prognose Rest
); } function KvLine({ k, v, sub, tone }) { return (
{k} {v}
{sub &&
{sub}
}
); } // ───── Distribution Panel ───── function DistributionPanel({ transactions }) { const cats = {}; transactions.filter(t => t.date.startsWith('2026') && t.type === 'ausgabe').forEach(t => { cats[t.category] = (cats[t.category] || 0) + Math.abs(t.amount); }); const total = Object.values(cats).reduce((s, v) => s + v, 0); const entries = Object.entries(cats).sort((a, b) => b[1] - a[1]); // Donut chart const R = 60, r = 36; let cumAngle = -Math.PI / 2; const colors = ['var(--accent)', 'var(--ink)', 'var(--ink-3)', 'var(--warn)', 'var(--ink-4)', 'var(--ink-5)']; const segs = entries.map(([cat, val], i) => { const angle = (val / total) * Math.PI * 2; const startA = cumAngle; cumAngle += angle; const endA = cumAngle; const x1 = 75 + R * Math.cos(startA), y1 = 75 + R * Math.sin(startA); const x2 = 75 + R * Math.cos(endA), y2 = 75 + R * Math.sin(endA); const x3 = 75 + r * Math.cos(endA), y3 = 75 + r * Math.sin(endA); const x4 = 75 + r * Math.cos(startA), y4 = 75 + r * Math.sin(startA); const large = angle > Math.PI ? 1 : 0; const path = `M ${x1} ${y1} A ${R} ${R} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${r} ${r} 0 ${large} 0 ${x4} ${y4} Z`; return { path, cat, val, color: colors[i % colors.length] }; }); return (
{segs.map((s, i) => )}
Ausgaben
{fmtEUR(total)}
{segs.map((s, i) => (
{s.cat} {fmtEUR(s.val)} {((s.val / total) * 100).toFixed(0)}%
))}
); } // ───── YoY Table ───── function YoYTable({ propStats, setRoute }) { const maxIncome = Math.max(...propStats.map(p => p.curIncome)); return (
{propStats.map(p => ( setRoute({ page: 'properties', propertyId: p.id })}> ))}
Objekt Auslastung Einn. YTD 2026 Einn. YTD 2025 Veränderung Trend
{p.name}
{p.units.length} Einheiten
{(p.occupancy * 100).toFixed(0)}%
{fmtEUR(p.curIncome)} {fmtEUR(p.prevIncome)}
); } function MiniSpark({ change }) { const positive = change > 0; const points = positive ? "0,16 8,14 16,12 24,10 32,6 40,4" : "0,4 8,5 16,8 24,11 32,13 40,15"; return ( ); } // ───── Predictions list ───── const PREDICTIONS = [ { title: 'Wohnung 4 Lindenstraße — Neuvermietung Q3', prob: 0.82, impact: '+1.080 €/Monat', horizon: '60 Tage', body: 'Modell schätzt Vermarktungsdauer auf 38 Tage basierend auf Vergleichsangeboten. Bei zügiger Renovierung Erstbezug Juli realistisch.', tone: 'pos', }, { title: 'Mieter Maier (Stadtpark 7 W3) — kein Anschlussvertrag', prob: 0.91, impact: '−1.290 €/Monat', horizon: '31. Juli', body: 'Kündigung bereits vorliegend. Neuvermietungsdauer in München Westend liegt bei 14–28 Tagen.', tone: 'neg', }, { title: 'Heizölpreis-Anstieg erwartet', prob: 0.66, impact: '+340 € Heizkosten Q4', horizon: '120 Tage', body: 'Marktdaten zeigen +12 % Großhandelspreis seit März. Frühbestellung im Juni statistisch günstigster Zeitpunkt.', tone: 'warn', }, { title: 'Café Mira Indexmiete-Anpassung', prob: 0.95, impact: '+55 €/Monat ab Sept.', horizon: 'Sept. 2026', body: 'VPI-Anpassung gemäß Mietvertrag automatisch. Anpassung ankündigen empfohlen 2 Monate vorab.', tone: 'pos', }, ]; function PredictionsList() { return (
{PREDICTIONS.map((p, i) => (
{p.impact}
Wahrscheinlichkeit {Math.round(p.prob * 100)} %
{p.title}
{p.body}
Horizont · {p.horizon}
))}
); } function ProbBar({ prob }) { return (
); } Object.assign(window, { AnalyticsPage });