refactor: split PropertyDetailView (998→192 lines) and MatchDetail (464→236 lines)
PropertyDetailView.tsx extracted into 5 focused components: - PropertyDetailHelpers: Field, FieldGrid, SectionTitle, floorLabel - UnitStructurePanel: floor structure with unit matching - PreMarketPanel: schattenmarkt release controls - PropertyDetailOverview: overview tab content - MatchabilityTabPanel: need matches tab MatchDetail.tsx extracted into 2 focused components: - MatchDetailHero: image/map, header, key facts strip - MatchDetailPropertySections: Preis/Hauptangaben/Eigenschaften/etc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { Box, Divider, LinearProgress, TextField, Typography } from '@mui/material'
|
||||
import type { Property, UpdatePropertyInput } from '../../domain/property'
|
||||
import { PropertyMap } from '../shared'
|
||||
import { getAssetTypeLabel, qualityColor } from './propertyHelpers'
|
||||
import { Field, FieldGrid, SectionTitle } from './PropertyDetailHelpers'
|
||||
import { UnitStructurePanel } from './UnitStructurePanel'
|
||||
import { PreMarketPanel } from './PreMarketPanel'
|
||||
|
||||
interface PropertyDetailOverviewProps {
|
||||
p: Property
|
||||
editing: boolean
|
||||
draft: UpdatePropertyInput
|
||||
onDraftChange: (d: UpdatePropertyInput) => void
|
||||
}
|
||||
|
||||
export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: PropertyDetailOverviewProps) {
|
||||
const rentLabel = p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm.toLocaleString('de-CH')}` : undefined
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Key metrics */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 0.75 }}>
|
||||
{[
|
||||
{ label: 'Fläche', value: `${p.areaSqm.toLocaleString('de-CH')} m²` },
|
||||
{ label: 'CHF/m²/Jahr', value: rentLabel },
|
||||
{ label: 'Verfügbar ab', value: p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : undefined },
|
||||
].map(({ label, value }) => (
|
||||
<Box key={label} sx={{ p: 1.5, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 700, mt: 0.25 }}>
|
||||
{value ?? <span style={{ color: '#94a3b8' }}>k.A.</span>}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{p.propertyNumber && (
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', mb: 2, display: 'block' }}>
|
||||
Objekt-Nr. {p.propertyNumber}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{editing ? (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<SectionTitle title="Beschreibung" />
|
||||
<TextField
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
size="small"
|
||||
value={draft.description ?? p.description ?? ''}
|
||||
onChange={e => onDraftChange({ ...draft, description: e.target.value })}
|
||||
placeholder="Beschreibung des Objekts…"
|
||||
/>
|
||||
</Box>
|
||||
) : p.description ? (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<SectionTitle title="Beschreibung" />
|
||||
<Typography variant="body2" sx={{ lineHeight: 1.6, color: 'text.secondary' }}>
|
||||
{p.description}
|
||||
</Typography>
|
||||
</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>
|
||||
) : (
|
||||
<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 */}
|
||||
<UnitStructurePanel p={p} />
|
||||
|
||||
{/* Pre-Market Matching */}
|
||||
<PreMarketPanel p={p} />
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* Object details */}
|
||||
<SectionTitle title="Objekt & Lage" />
|
||||
<FieldGrid>
|
||||
{p.propertyNumber && <Field label="Objekt-Nr." value={p.propertyNumber} />}
|
||||
<Field label="Objekttyp" value={getAssetTypeLabel(p.assetType)} />
|
||||
<Field label="Etage" value={p.hardFacts?.floor ?? p.floorLevel} />
|
||||
<Field label="Ausbaustandard" value={p.hardFacts?.fitOut} />
|
||||
<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} />
|
||||
</FieldGrid>
|
||||
|
||||
{/* Map */}
|
||||
{p.location.coordinates && (
|
||||
<Box sx={{ mb: 2.5, mt: 1.5, borderRadius: 1, overflow: 'hidden', border: '1px solid #e2e8f0' }}>
|
||||
<Box sx={{ px: 1.5, pt: 1.25, pb: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
Standort
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{p.address.street} {p.address.houseNumber}, {p.address.postalCode} {p.address.city}
|
||||
</Typography>
|
||||
</Box>
|
||||
<PropertyMap
|
||||
lat={p.location.coordinates.lat}
|
||||
lng={p.location.coordinates.lng}
|
||||
label={p.title}
|
||||
height={160}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1.5 }} />
|
||||
|
||||
{/* Data quality */}
|
||||
<SectionTitle title="Datenqualität" />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="body2" color="text.secondary">Score</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{Math.round(p.dataQuality.score * 100)}%</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={p.dataQuality.score * 100}
|
||||
color={qualityColor(p.dataQuality.score)}
|
||||
sx={{ height: 8, borderRadius: 4, mb: 1 }}
|
||||
/>
|
||||
{p.dataQuality.warnings.length > 0 && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{p.dataQuality.warnings.map((w, i) => (
|
||||
<Typography key={i} variant="caption" sx={{ display: 'block', color: 'warning.main' }}>⚠ {w}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user