// Tax + Rent + Tasks pages — neue Module für Steuer, Mieten-Status, Aufgaben // ──────────────────────────────────────────────────────────────────── // AfA-Sektion: wird auf PropertyDetail eingebunden + auf TaxPage // ──────────────────────────────────────────────────────────────────── function AfaSection({ data, propertyId }) { const { assets, properties, store } = data; const [year, setYear] = React.useState(new Date().getFullYear()); const [showForm, setShowForm] = React.useState(false); const [editing, setEditing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); const [summary, setSummary] = React.useState(null); React.useEffect(() => { let cancelled = false; window.getAfaSummary({ year, propertyId }) .then(s => { if (!cancelled) setSummary(s); }) .catch(() => {}); return () => { cancelled = true; }; }, [year, propertyId, assets]); const propAssets = (assets || []).filter(a => !propertyId || a.propertyId === propertyId); const openAdd = () => { setEditing(null); setShowForm(true); }; return (

Anlagenverzeichnis & AfA

setShowForm(false)} store={store} properties={properties} initial={editing} defaultPropertyId={propertyId} /> setDeleting(null)} onConfirm={async () => { await store.deleteAsset(deleting.id); }} title="Anlage löschen" message={`„${deleting?.label}" endgültig löschen?`} confirmLabel="Endgültig löschen" /> {propAssets.length === 0 ? (
Noch keine Anlagen erfasst
Lege dein Gebäude (Anschaffungskosten × 80 % Gebäudeanteil) und Modernisierungen über 4.000 € als Anlagen an — Hausgut berechnet automatisch die jährliche AfA für die Steuererklärung.
) : ( <> {summary && (
AfA {year}
{fmtEUR(summary.total.afaThisYear)}
linear, abziehbare Werbungskosten
Bisher abgeschrieben
{fmtEUR(summary.total.afaAccumulated)}
kumuliert bis Ende {year}
Buchwert
{fmtEUR(summary.total.bookValue)}
verbleibender Wert {year}
Anschaffung
{fmtEUR(summary.total.acquisitionCost)}
{propAssets.length} Anlagen
)}
{(summary?.assets || []).filter(a => !propertyId || a.propertyId === propertyId).map(a => { const orig = propAssets.find(x => x.id === a.id) || a; const typeLabel = (window.ASSET_TYPES || []).find(t => t.value === a.type)?.label || a.type; return ( { setEditing(orig); setShowForm(true); }}> ); })}
BezeichnungTypAnschaffungKostenAfA-SatzAfA {year}Buchwert
{a.label} {typeLabel} {fmtDateShort(a.acquisitionDate)} {fmtEUR(a.acquisitionCost)} {a.afaRate.toFixed(1)} % {fmtEUR(a.afaThisYear)} {fmtEUR(a.bookValue)} e.stopPropagation()} style={{ whiteSpace: 'nowrap' }}>
)}
); } // ──────────────────────────────────────────────────────────────────── // TaxPage: Anlage V Vorbereitung // ──────────────────────────────────────────────────────────────────── function TaxPage({ data, setRoute }) { const { properties, store } = data; const [year, setYear] = React.useState(new Date().getFullYear() - 1); const [propertyId, setPropertyId] = React.useState(''); const [anlageV, setAnlageV] = React.useState(null); const [loading, setLoading] = React.useState(true); const [err, setErr] = React.useState(null); const load = React.useCallback(async () => { setLoading(true); setErr(null); try { const r = await window.getAnlageV({ year, propertyId: propertyId || undefined }); setAnlageV(r); } catch (e) { setErr(e.message); } finally { setLoading(false); } }, [year, propertyId]); React.useEffect(() => { load(); }, [load]); const datevUrl = window.datevExportUrl({ year, propertyId: propertyId || undefined }); const printAnlageV = () => { window.print(); }; return (

Steuer

Anlagenverzeichnis (AfA), Anlage V Vorbereitung & DATEV-Export
📊 DATEV-CSV
{/* AfA-Sektion */} {/* Anlage V */}

Anlage V — Einkünfte aus Vermietung und Verpachtung {year}

{loading &&
Lade…
} {err &&
⚠ {err}
} {!loading && anlageV && anlageV.properties.length === 0 && (
Keine Daten für diesen Filter.
)} {!loading && anlageV && anlageV.properties.map(p => (

{p.propertyName}

{p.propertyAddress}{p.legalOwnerName ? ` · ${p.legalOwnerName}` : ''}
Ergebnis {year}
= 0 ? 'var(--pos)' : 'var(--neg)' }}> {fmtEUR(p.result)}
{p.result >= 0 ? 'Überschuss' : 'Verlust'}
{/* Einnahmen */}
Einnahmen (Zeilen 8–14)
Kaltmiete{fmtEUR(p.income.kaltmiete)}
Nebenkostenvorauszahlungen{fmtEUR(p.income.nebenkosten)}
{p.income.kaution > 0 &&
Kaution (nicht steuerbar){fmtEUR(p.income.kaution)}
} {p.income.sonstiges > 0 &&
Sonstige Einnahmen{fmtEUR(p.income.sonstiges)}
}
Summe Einnahmen{fmtEUR(p.totalIncome)}
{/* Werbungskosten */}
Werbungskosten (Zeilen 33–49)
{p.expenseLines.map((l, i) => (
Z. {l.line} · {l.label} {fmtEUR(l.amount)}({l.count})
))} {p.afa.thisYear > 0 && (
Z. 33 · AfA Gebäude / Modernisierung {fmtEUR(p.afa.thisYear)}({p.afa.assets} Anlagen)
)}
Summe Werbungskosten{fmtEUR(p.totalExpense)}
))} {!loading && anlageV && anlageV.properties.length > 0 && (
Gesamtergebnis {year}
Summe aller Objekte · ohne Sonderfälle (Verluste <1 J, Liebhaberei etc.)
= 0 ? 'var(--pos)' : 'var(--neg)' }}> {fmtEUR(anlageV.total.result)}
Einnahmen gesamt: {fmtEUR(anlageV.total.totalIncome)}
Werbungskosten gesamt: {fmtEUR(anlageV.total.totalExpense)}
davon AfA: {fmtEUR(anlageV.total.afa)}
)}
⚠ Diese Aufstellung ist eine Vorbereitungshilfe für deine Steuererklärung. Lass die Werte vor Einreichung immer von deinem Steuerberater prüfen — Hausgut ersetzt keine steuerliche Beratung. Sonderfälle (zeitanteilige Vermietung, Werbungskostenüberschuss-Verteilung, Modernisierungsregelung §82b EStDV) musst du ggf. manuell anpassen.
); } // ──────────────────────────────────────────────────────────────────── // RentStatusPage: Soll vs. Ist + Mahn-Liste // ──────────────────────────────────────────────────────────────────── function RentStatusPage({ data, setRoute }) { const { properties } = data; const [year, setYear] = React.useState(new Date().getFullYear()); const [status, setStatus] = React.useState(null); const [loading, setLoading] = React.useState(true); const [err, setErr] = React.useState(null); const load = React.useCallback(async () => { setLoading(true); setErr(null); try { setStatus(await window.getRentStatus({ year })); } catch (e) { setErr(e.message); } finally { setLoading(false); } }, [year]); React.useEffect(() => { load(); }, [load]); const goMahnung = (t) => { setRoute({ page: 'contracts', contractWizard: 'mahnung', propertyId: t.propertyId, unitId: t.unitId, tenantId: t.tenantId }); }; return (

Mieten-Status

Soll vs. Ist · Schuldner · Mahn-Aktionen
{loading &&
Lade…
} {err &&
⚠ {err}
} {status && ( <>
Soll {year}
{fmtEUR(status.summary.expectedTotal)}
erwartete Mieteinnahmen
Ist
{fmtEUR(status.summary.receivedTotal)}
tatsächlich eingegangen
Saldo
= -50 ? 'var(--pos)' : 'var(--neg)' }}> {fmtEUR(status.summary.saldoTotal)}
{status.summary.saldoTotal >= -50 ? '✓ Portfolio ausgeglichen' : 'offene Forderungen'}
Schuldner
0 ? 'var(--neg)' : 'var(--pos)' }}> {status.summary.debtorsCount}
{status.summary.missedMonthsTotal} fehlende Monate

Pro Mieter

{status.tenants.length} aktive Mieter
{status.tenants.length === 0 ? (
Keine aktiven Mietverhältnisse im Jahr {year}.
) : (
{status.tenants.map(t => { const isDebtor = t.saldo < -50; return ( ); })}
MieterWohnungSollIstSaldoFehlende Monate
{fmtEUR(t.expectedAmount)} {fmtEUR(t.receivedAmount)} {t.saldo >= 0 ? '+' : ''}{fmtEUR(t.saldo)} {t.missedMonths.length === 0 ? ( vollständig ) : ( {t.missedMonths.length} ({t.missedMonths.slice(0, 3).join(', ')}{t.missedMonths.length > 3 ? '…' : ''}) )} {isDebtor && ( )}
)}
)}
); } // ──────────────────────────────────────────────────────────────────── // TasksPage: Aufgaben mit Done-Status // ──────────────────────────────────────────────────────────────────── function TasksPage({ data }) { const { tasks, properties, store } = data; const [showForm, setShowForm] = React.useState(false); const [editing, setEditing] = React.useState(null); const [filter, setFilter] = React.useState('open'); const filtered = (tasks || []).filter(t => { if (filter === 'open') return !t.completedAt; if (filter === 'done') return !!t.completedAt; return true; }); const toggleDone = async (t) => { await store.updateTask(t.id, { ...t, completed: !t.completedAt }); }; const openCount = (tasks || []).filter(t => !t.completedAt).length; const doneCount = (tasks || []).filter(t => t.completedAt).length; return (

Aufgaben

{openCount} offen · {doneCount} erledigt
setShowForm(false)} store={store} properties={properties} initial={editing} />
{['open', 'done', 'all'].map(f => ( ))}
{filtered.length === 0 ? (
{filter === 'done' ? '🎉' : '✓'}
{filter === 'done' ? 'Noch keine erledigten Aufgaben' : filter === 'open' ? 'Keine offenen Aufgaben' : 'Noch keine Aufgaben'}
) : (
{filtered.map(t => { const prop = properties.find(p => p.id === t.propertyId); const overdue = !t.completedAt && t.dueDate && t.dueDate < new Date().toISOString().slice(0, 10); return (
toggleDone(t)} style={{ marginTop: 4, width: 18, height: 18, cursor: 'pointer', flexShrink: 0 }} />
{ setEditing(t); setShowForm(true); }}>
{t.title}
{t.description && (
{t.description}
)}
{t.dueDate && {overdue ? '⚠ ' : '📅 '}{fmtDate(t.dueDate)}} {t.priority === 'hoch' && Hoch} {prop && 📍 {prop.name}}
); })}
)}
); } Object.assign(window, { AfaSection, TaxPage, RentStatusPage, TasksPage });