// Property detail page (with units list) + Unit detail page (with tabs) function PropertyDetail({ data, route, setRoute }) { const { properties, tenants, transactions, documents, maintenance, store } = data; const prop = properties.find(p => p.id === route.propertyId); if (!prop) return
Objekt nicht gefunden.
; const [showEdit, setShowEdit] = React.useState(false); const [showAddUnit, setShowAddUnit] = React.useState(false); const [showDelete, setShowDelete] = React.useState(false); const [showImport, setShowImport] = React.useState(false); const monthlyRent = prop.units.reduce((s, u) => s + (u.rent || 0), 0); const occupied = prop.units.filter(u => u.status === 'vermietet').length; const totalSize = prop.units.reduce((s, u) => s + (u.size || 0), 0); // P&L for this property const propTxns = transactions.filter(t => t.propertyId === prop.id); const yr = String(new Date().getFullYear()); const ytdIncome = propTxns.filter(t => t.date.startsWith(yr) && t.type === 'einnahme').reduce((s, t) => s + t.amount, 0); const ytdExpense = propTxns.filter(t => t.date.startsWith(yr) && t.type === 'ausgabe').reduce((s, t) => s + Math.abs(t.amount), 0); const propDocs = documents.filter(d => d.propertyId === prop.id && !d.unitId); const propMaint = maintenance.filter(m => m.propertyId === prop.id && m.status !== 'erledigt'); const cloudPropDocs = useDocuments({ propertyId: prop.id, scope: 'allgemein' }); const yearsHeld = prop.purchaseDate ? ((Date.now() - new Date(prop.purchaseDate)) / (1000 * 60 * 60 * 24 * 365)).toFixed(1) : '—'; const grossYield = prop.purchasePrice > 0 ? ((monthlyRent * 12) / prop.purchasePrice * 100).toFixed(2) : null; return (
{prop.type}

{prop.name}

{prop.address}
Baujahr
{prop.yearBuilt || '—'}
Im Bestand seit
{prop.purchaseDate ? `${fmtDate(prop.purchaseDate)} (${yearsHeld} Jahre)` : '—'}
Kaufpreis
{prop.purchasePrice ? fmtEUR(prop.purchasePrice) : '—'}
Bruttorendite
{grossYield ? `${grossYield} %` : '—'}
setShowEdit(false)} store={store} initial={prop} /> setShowAddUnit(false)} store={store} propertyId={prop.id} tenants={tenants} /> setShowImport(false)} store={store} properties={properties} defaultPropertyId={prop.id} /> setShowDelete(false)} onConfirm={async () => { await store.deleteProperty(prop.id); setRoute({ page: 'properties' }); }} title="Objekt löschen" message={`„${prop.name}" und alle ${prop.units.length} Einheiten endgültig löschen? Buchungen werden ebenfalls gelöscht. Hochgeladene Dokumente bleiben erhalten.`} confirmLabel="Endgültig löschen" />

Einheiten

{prop.units.length === 0 ? (
Keine Einheiten
Lege die erste Wohnung / Einheit dieses Objekts an.
) : (
{prop.units.map((unit, i) => { const tenant = unit.tenant ? tenants[unit.tenant] : null; return ( ); })}
)}

Objektdokumente

cloudPropDocs.reload()} />
cloudPropDocs.reload()} label="Dateien hierher ziehen oder klicken" subLabel={`Wird unter "${prop.name}" als Objekt-Dokument gespeichert`} />
{cloudPropDocs.items.length > 0 && (
{cloudPropDocs.items.map(doc => (
{doc.name}
{(doc.sizeBytes / 1024).toFixed(0)} KB · {fmtDateShort(doc.uploadedAt)}
{doc.category || 'Cloud'}
))}
)} {propDocs.length === 0 && cloudPropDocs.items.length === 0 ? (
Keine Dokumente
Lade Versicherungspolicen, Grundbuchauszüge oder Hausgeldabrechnungen hoch.
) : propDocs.length === 0 ? null : (
{propDocs.map(doc => (
{doc.name}
{doc.size}
{doc.category} {fmtDateShort(doc.date)}
))}
)}

Wartung & Reparatur

{propMaint.length === 0 ? (
Keine offenen Vorgänge.
) : (
{propMaint.map(m => (
{m.title}
{m.status === 'in-arbeit' ? 'In Arbeit' : 'Geplant'} fällig {fmtDateShort(m.due)} · {fmtEUR(m.cost)}
))}
)}
); } // ───── Unit Detail (Wohnung) with tabs ───── function UnitDetail({ data, route, setRoute }) { const { properties, tenants, transactions, documents, meterReadings, store } = data; const prop = properties.find(p => p.id === route.propertyId); const unit = prop?.units.find(u => u.id === route.unitId); if (!unit) return
Einheit nicht gefunden.
; const [tab, setTab] = React.useState('overview'); const [showEditUnit, setShowEditUnit] = React.useState(false); const [showDeleteUnit, setShowDeleteUnit] = React.useState(false); const [showAddTenant, setShowAddTenant] = React.useState(false); const [showTxForm, setShowTxForm] = React.useState(false); const [editingTx, setEditingTx] = React.useState(null); const [txDefaultType, setTxDefaultType] = React.useState('einnahme'); const tenant = unit.tenant ? tenants[unit.tenant] : null; const cloud = useDocuments({ propertyId: prop.id, unitId: unit.id, scope: 'einheit' }); const unitTxns = transactions.filter(t => t.unitId === unit.id); const unitDocs = documents.filter(d => d.unitId === unit.id); const unitMeters = meterReadings.filter(m => m.unitId === unit.id); const incomeTxns = unitTxns.filter(t => t.type === 'einnahme'); const expenseTxns = unitTxns.filter(t => t.type === 'ausgabe'); return (
{prop.name}

{unit.label}

{unit.floor || '—'}{unit.rooms ? ` · ${unit.rooms} Zimmer` : ''}{unit.size ? ` · ${unit.size} m²` : ''} ·
setShowEditUnit(false)} store={store} propertyId={prop.id} tenants={tenants} initial={unit} /> setShowDeleteUnit(false)} onConfirm={async () => { await store.deleteUnit(unit.id); setRoute({ page: 'properties', propertyId: prop.id }); }} title="Einheit löschen" message={`„${unit.label}" endgültig löschen? Verknüpfte Buchungen werden vom Mieter gelöst (bleiben dem Objekt zugeordnet).`} confirmLabel="Endgültig löschen" /> setShowAddTenant(false)} store={store} /> { setShowTxForm(false); setEditingTx(null); }} store={store} properties={properties} defaultPropertyId={prop.id} defaultUnitId={unit.id} defaultType={txDefaultType} initial={editingTx} />
{tab === 'overview' && (

Aktuelle Buchungen

{unitTxns.slice(0, 8).map(tx => ( ))}
DatumBeschreibungKategorieBetrag
{fmtDateShort(tx.date)} {tx.description} {tx.category} 0 ? 'var(--pos)' : 'var(--neg)' }}> {tx.amount > 0 ? '+' : ''}{fmtEUR(tx.amount)}
{tenant ? (

Mieter

{tenant.name.split(' ').map(s => s[0]).slice(0, 2).join('')}
{tenant.name}
{tenant.email}
Telefon{tenant.phone}
Mieter seit{fmtDate(tenant.since)}
Vertragsende{tenant.until ? fmtDate(tenant.until) : 'unbefristet'}
Kaution{fmtEUR(tenant.deposit)}
Kaltmiete{fmtEUR(unit.rent)}
) : (

Leerstand

Diese Einheit ist aktuell nicht vermietet.
)}

Dokumente / Belege

cloud.reload()} label="Datei hierher ziehen" subLabel="oder klicken zum Auswählen" /> {cloud.items.length > 0 && (
{cloud.items.length} {cloud.items.length === 1 ? 'Dokument' : 'Dokumente'} hochgeladen ·{' '}
)}

Schnellaktionen

)} {(tab === 'income' || tab === 'expense') && ( { setEditingTx(null); setShowTxForm(true); }} onEdit={(tx) => { setEditingTx(tx); setShowTxForm(true); }} onDelete={async (tx) => { if (confirm('Buchung wirklich löschen?')) await store.deleteTransaction(tx.id); }} /> )} {tab === 'docs' && (
{cloud.items.length + unitDocs.length} Dokumente {cloud.loading && · lade…} {cloud.error && · {cloud.error}}
cloud.reload()} />
cloud.reload()} label="Dateien hierher ziehen oder klicken" subLabel={`Wird unter "${prop.name} · ${unit.label}" gespeichert`} />
{cloud.items.length > 0 && (
Hochgeladen · Cloud
{cloud.items.map(doc => (
{doc.name}
{(doc.sizeBytes / 1024).toFixed(0)} KB · {fmtDateShort(doc.uploadedAt)}
{doc.category || 'Dokument'}
))}
)} {unitDocs.length === 0 && cloud.items.length === 0 ? (
Noch keine Dokumente
Mietvertrag, Übergabeprotokoll, Nebenkostenabrechnungen…
) : unitDocs.length === 0 ? null : (
{unitDocs.map(doc => (
{doc.name}
{doc.size}
{doc.category} {fmtDateShort(doc.date)}
))}
)}
)} {tab === 'meters' && (
Letzte Ablesung: {unitMeters[0] ? fmtDate(unitMeters[0].date) : '—'}
{unitMeters.length === 0 ? (
Keine Zählerstände
Erfasse Strom, Wasser und Gas für die jährliche Abrechnung.
) : (
{unitMeters.map(m => ( ))}
ZählerDatumWertEinheit
{m.type} {fmtDateShort(m.date)} {fmtNum(m.value)} {m.unit}
)}
)}
); } function TxnTab({ txns, kind, onAdd, onEdit, onDelete }) { const total = txns.reduce((s, t) => s + Math.abs(t.amount), 0); return (
Summe {kind === 'income' ? 'Einnahmen' : 'Ausgaben'}
{kind === 'income' ? '+' : '−'}{fmtEUR(total)}
{txns.length === 0 ? (
Noch keine {kind === 'income' ? 'Einnahmen' : 'Ausgaben'}
Erfasse die erste Buchung oben rechts.
) : (
{txns.map(tx => ( ))}
DatumBeschreibungKategorieBetrag
{fmtDateShort(tx.date)} {tx.description || '—'} {tx.category && {tx.category}} 0 ? 'var(--pos)' : 'var(--neg)' }}> {tx.amount > 0 ? '+' : ''}{fmtEUR(tx.amount)}
)}
); } Object.assign(window, { PropertyDetail, UnitDetail });