refactor: unit-centric lease view — Einheiten & Mietverträge

Replace building-level Miet- & Mieterinformationen section with
unit-level lease display. Each unit row now shows tenant name and
lease end date directly; expand reveals full contract details
(Mietbeginn, Mietende, Laufzeit, Breakout, source system, doc link).

Floor grouping groups consecutive same-floor units under one label.
Domain model extended with Lease and Tenant types; deprecated
property-level tenant fields preserved via @deprecated for compat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-10 22:52:18 +02:00
parent d771029633
commit ee229f8f4f
9 changed files with 518 additions and 343 deletions
+55 -101
View File
@@ -63,114 +63,66 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
</Box>
) : null}
{/* Lease & Tenant */}
<SectionTitle title="Miet- & Mieterinformationen" />
{editing ? (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5, mb: 1 }}>
<TextField
label="Aktueller Mieter"
size="small"
value={draft.currentTenant ?? p.currentTenant ?? ''}
onChange={e => onDraftChange({ ...draft, currentTenant: e.target.value })}
/>
<TextField
label="Mietlaufzeit"
size="small"
value={draft.leaseTerm ?? p.leaseTerm ?? ''}
onChange={e => onDraftChange({ ...draft, leaseTerm: e.target.value })}
placeholder="z.B. 5 Jahre"
/>
<TextField
label="Mietbeginn"
size="small"
type="date"
slotProps={{ inputLabel: { shrink: true } }}
value={draft.leaseStartDate ?? p.leaseStartDate ?? ''}
onChange={e => onDraftChange({ ...draft, leaseStartDate: e.target.value })}
/>
<TextField
label="Mietende"
size="small"
type="date"
slotProps={{ inputLabel: { shrink: true } }}
value={draft.leaseEndDate ?? p.leaseEndDate ?? ''}
onChange={e => onDraftChange({ ...draft, leaseEndDate: e.target.value })}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 0.5, mb: 0.5 }}>
<FileText size={13} color="#64748b" />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.4 }}>
Mietvertrag
</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
<TextField
label="Vertrag URL"
size="small"
value={draft.leaseContractUrl ?? p.leaseContractUrl ?? ''}
onChange={e => onDraftChange({ ...draft, leaseContractUrl: e.target.value })}
placeholder="https://…"
slotProps={{ input: { startAdornment: <ExternalLink size={12} style={{ marginRight: 6, color: '#94a3b8', flexShrink: 0 }} /> } }}
/>
<TextField
label="Bezeichnung (optional)"
size="small"
value={draft.leaseContractName ?? p.leaseContractName ?? ''}
onChange={e => onDraftChange({ ...draft, leaseContractName: e.target.value })}
placeholder="z.B. Mietvertrag 20212026"
/>
</Box>
</Box>
) : (
<>
<FieldGrid>
<Field label="Aktueller Mieter" value={p.currentTenant} />
<Field label="Mietlaufzeit" value={p.leaseTerm} />
<Field label="Mietbeginn" value={p.leaseStartDate ? new Date(p.leaseStartDate).toLocaleDateString('de-CH') : undefined} />
<Field label="Mietende" value={p.leaseEndDate ? new Date(p.leaseEndDate).toLocaleDateString('de-CH') : undefined} />
<Field label="Breakoutoption" value={p.breakoutOption} />
<Field label="Breakoutoption Zeitpunkt" value={p.breakoutOptionDate ? new Date(p.breakoutOptionDate).toLocaleDateString('de-CH') : undefined} />
<Field label="Importiert aus" value={p.importedFrom} />
<Field label="Zuletzt aktualisiert" value={p.lastUpdatedAt ? new Date(p.lastUpdatedAt).toLocaleDateString('de-CH') : undefined} />
</FieldGrid>
{/* Floor / unit structure — primary tenant/lease view */}
<UnitStructurePanel p={p} />
{/* Mietvertrag */}
<Box sx={{ mt: 1.5, p: 1.5, border: '1px solid #e2e8f0', borderRadius: 1.5, bgcolor: p.leaseContractUrl ? '#f8fafc' : '#fafafa' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: p.leaseContractUrl ? 0.75 : 0 }}>
<FileText size={13} color={p.leaseContractUrl ? '#152642' : '#94a3b8'} />
<Typography variant="caption" sx={{ fontWeight: 700, color: p.leaseContractUrl ? '#152642' : '#94a3b8', textTransform: 'uppercase', letterSpacing: 0.4 }}>
Mietvertrag
</Typography>
{/* Fallback: property-level contract doc only when no unit-level contracts exist */}
{(() => {
const hasUnitContracts = p.units?.some(u => u.leases?.some(l => l.contractDocumentUrl))
if (editing) {
return (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<FileText size={13} color="#64748b" />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.4 }}>
Gebäude-Mietvertrag (Fallback)
</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
<TextField
label="Vertrag URL"
size="small"
value={draft.leaseContractUrl ?? p.leaseContractUrl ?? ''}
onChange={e => onDraftChange({ ...draft, leaseContractUrl: e.target.value })}
placeholder="https://…"
slotProps={{ input: { startAdornment: <ExternalLink size={12} style={{ marginRight: 6, color: '#94a3b8', flexShrink: 0 }} /> } }}
/>
<TextField
label="Bezeichnung (optional)"
size="small"
value={draft.leaseContractName ?? p.leaseContractName ?? ''}
onChange={e => onDraftChange({ ...draft, leaseContractName: e.target.value })}
placeholder="z.B. Mietvertrag 20212026"
/>
</Box>
</Box>
{p.leaseContractUrl ? (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
)
}
if (!p.leaseContractUrl || hasUnitContracts) return null
return (
<Box sx={{ mt: 0.5, mb: 1.5, p: 1.5, border: '1px solid #e2e8f0', borderRadius: 1.5, bgcolor: '#f8fafc' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<FileText size={13} color="#152642" />
<Typography variant="caption" sx={{ color: '#475569', fontWeight: 500 }} noWrap>
{p.leaseContractName ?? 'Mietvertrag'}
</Typography>
<Button
size="small"
variant="outlined"
endIcon={<ExternalLink size={11} />}
href={p.leaseContractUrl}
target="_blank"
rel="noopener noreferrer"
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.25, px: 1, borderColor: '#cbd5e1', color: '#152642', whiteSpace: 'nowrap', flexShrink: 0 }}
>
Öffnen
</Button>
</Box>
) : (
<Typography variant="caption" sx={{ color: '#94a3b8' }}>
Noch kein Mietvertrag hinterlegt im Bearbeitungsmodus hinzufügen.
</Typography>
)}
<Button
size="small"
variant="outlined"
endIcon={<ExternalLink size={11} />}
href={p.leaseContractUrl}
target="_blank"
rel="noopener noreferrer"
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.25, px: 1, borderColor: '#cbd5e1', color: '#152642', whiteSpace: 'nowrap', flexShrink: 0 }}
>
Öffnen
</Button>
</Box>
</Box>
</>
)}
{/* Floor / unit structure */}
<UnitStructurePanel p={p} />
)
})()}
{/* Pre-Market Matching */}
<PreMarketPanel p={p} />
@@ -230,6 +182,8 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
<Field label="Parkplätze" value={p.hardFacts?.parking ?? p.softFactors?.parkingSpots} />
<Field label="ÖV (Min.)" value={p.softFactors?.publicTransportMinutes} />
<Field label="Nebenkosten/m²" value={p.ancillaryCosts ? `CHF ${p.ancillaryCosts}` : undefined} />
<Field label="Importiert aus" value={p.importedFrom} />
<Field label="Zuletzt aktualisiert" value={p.lastUpdatedAt ? new Date(p.lastUpdatedAt).toLocaleDateString('de-CH') : undefined} />
</FieldGrid>
)}