feat: Verwaltungszugang refactor — lease data, 3-tab detail, market signals

- Dashboard: remove QuickActionPanel, StrongMatchOverview, FutureSignalWidget,
  DataQualityWidget, header buttons, Marktchancen nav; KpiGrid → 4 cards
- Property domain: 9 new fields (leaseTerm, leaseStartDate/End, breakoutOption,
  currentTenant, importedFrom, importedAt, lastUpdatedAt)
- Mock data: annual CHF/m²/Jahr prices (×12), Swiss images, tenant/lease data
  for all 10 VERIFIED_PORTFOLIO properties; need budgets updated to annual
- PropertyTable: swap columns — add Aktueller Mieter, Mietlaufzeit,
  Breakoutoption, Breakoutoption Zeitpunkt; remove Verfügbarkeit, Konfidenz, Quelle
- PropertyDetailView: 3 tabs (Übersicht, Matchability, Marktsignale) with
  inline edit mode, NeedMatchCard list, PropertyMarketSignalsTab
- New: marketReport domain + mock data + service, NeedMatchCard,
  PropertyMarketSignalsTab with 4 sections + PDF button
- matchService: getNeedMatchesForProperty(propertyId, {minScore})

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-19 15:43:52 +02:00
parent 52a2693cad
commit 920ce8eb09
17 changed files with 1036 additions and 476 deletions
+231 -223
View File
@@ -1,32 +1,35 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
Alert,
Box,
Button,
Chip,
CircularProgress,
Divider,
IconButton,
LinearProgress,
Tab,
Tabs,
TextField,
Typography,
} from '@mui/material'
import { X } from 'lucide-react'
import { NegotiationInsightsPanel } from './NegotiationInsightsPanel'
import { DataQualityPanel as DQPanel, ProvenancePanel } from '../data-quality'
import { useQueryClient } from '@tanstack/react-query'
import { Edit2, Save, X } from 'lucide-react'
import { PropertyMap } from '../shared'
import type { Property } from '../../domain/property'
import type { Match } from '../../domain/match'
import type { FutureSignal } from '../../domain/futureSignal'
import { usePropertyById, usePropertyMatches, usePropertySignals } from '../../hooks/useProperties'
import { PropertyActivityLogPanel } from './PropertyActivityLogPanel'
import { NeedMatchCard } from './NeedMatchCard'
import { PropertyMarketSignalsTab } from './PropertyMarketSignalsTab'
import type { Property, UpdatePropertyInput } from '../../domain/property'
import type { PropertyNeedMatch } from '../../domain/match'
import { usePropertyById } from '../../hooks/useProperties'
import { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
import { matchService } from '../../services/matchService'
import { propertyService } from '../../services/propertyService'
import { useToastStore } from '../../stores/toastStore'
import {
getAssetTypeColor,
getAssetTypeLabel,
getAvailabilityChipColor,
getAvailabilityLabel,
getResultTypeColor,
getResultTypeLabel,
qualityColor,
} from './propertyHelpers'
@@ -35,7 +38,7 @@ interface PropertyDetailViewProps {
onClose?: () => void
}
// ── Sub-components ────────────────────────────────────────────────────────────
// ── Helpers ────────────────────────────────────────────────────────────────────
function Field({ label, value }: { label: string; value?: string | number | boolean | null }) {
return (
@@ -70,44 +73,25 @@ function SectionTitle({ title }: { title: string }) {
)
}
function ScoreRow({ label, value }: { label: string; value?: number }) {
if (value === undefined || value === null) {
return (
<Box sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="caption" color="text.disabled"></Typography>
</Box>
<LinearProgress variant="determinate" value={0} sx={{ height: 4, borderRadius: 2, opacity: 0.3 }} />
</Box>
)
}
const pct = value > 1 ? value : value * 100
return (
<Box sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="caption" sx={{ fontWeight: 600 }}>{Math.round(pct)}</Typography>
</Box>
<LinearProgress
variant="determinate"
value={Math.min(100, pct)}
color={pct >= 70 ? 'success' : pct >= 40 ? 'warning' : 'error'}
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
)
// ── Übersicht tab ─────────────────────────────────────────────────────────────
interface OverviewPanelProps {
p: Property
editing: boolean
draft: UpdatePropertyInput
onDraftChange: (d: UpdatePropertyInput) => void
}
// ── Tab panels ────────────────────────────────────────────────────────────────
function OverviewPanel({ p, editing, draft, onDraftChange }: OverviewPanelProps) {
const rentLabel = p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm.toLocaleString('de-CH')}` : undefined
function OverviewPanel({ p }: { p: Property }) {
return (
<Box>
{/* Key metrics */}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 2.5 }}>
{[
{ label: 'Fläche', value: `${p.areaSqm.toLocaleString('de-CH')}` },
{ label: 'Miete/m²/Jahr', value: p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm}` : undefined },
{ 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 }}>
@@ -118,16 +102,95 @@ function OverviewPanel({ p }: { p: Property }) {
</Box>
))}
</Box>
{p.description && (
<>
{/* Description */}
{editing ? (
<Box sx={{ mb: 2 }}>
<SectionTitle title="Beschreibung" />
<Typography variant="body2" sx={{ lineHeight: 1.6, color: 'text.secondary', mb: 2 }}>
<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>
)}
<Divider sx={{ my: 2 }} />
{/* Object details */}
<SectionTitle title="Objekt & Lage" />
<FieldGrid>
<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, borderRadius: 1, overflow: 'hidden', border: '1px solid #e2e8f0' }}>
<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
@@ -144,6 +207,10 @@ function OverviewPanel({ p }: { p: Property }) {
/>
</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>
@@ -166,176 +233,83 @@ function OverviewPanel({ p }: { p: Property }) {
)
}
function HardFactsPanel({ p }: { p: Property }) {
const hf = p.hardFacts
return (
<Box>
<SectionTitle title="Standort & Objekt" />
<FieldGrid>
<Field label="Titel" value={p.title} />
<Field label="Objekttyp" value={getAssetTypeLabel(p.assetType)} />
<Field label="Strasse" value={`${p.address.street} ${p.address.houseNumber}`} />
<Field label="PLZ / Stadt" value={`${p.address.postalCode} ${p.address.city}`} />
<Field label="Kanton" value={p.location.canton} />
<Field label="Region" value={p.location.region} />
<Field label="Land" value={p.address.country} />
<Field label="Nutzungsart" value={hf?.usageType} />
</FieldGrid>
<Divider sx={{ my: 2 }} />
<SectionTitle title="Fläche & Miete" />
<FieldGrid>
<Field label="Fläche (m²)" value={`${p.areaSqm.toLocaleString('de-CH')}`} />
<Field label="Fläche Min (m²)" value={p.areaSqmMin ? `${p.areaSqmMin.toLocaleString('de-CH')}` : undefined} />
<Field label="Fläche Max (m²)" value={p.areaSqmMax ? `${p.areaSqmMax.toLocaleString('de-CH')}` : undefined} />
<Field label="Miete/m²/Jahr" value={p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm}` : undefined} />
<Field label="Jahresmiete (CHF/m²)" value={p.rentChfSqmYear ? `CHF ${p.rentChfSqmYear}` : undefined} />
<Field label="Monatsmiete gesamt" value={p.totalRentMonthly ? `CHF ${p.totalRentMonthly.toLocaleString('de-CH')}` : undefined} />
<Field label="Nebenkosten/m²" value={p.ancillaryCosts ? `CHF ${p.ancillaryCosts}` : undefined} />
<Field label="Vertragsdauer" value={p.contractDurationMonths ? `${p.contractDurationMonths} Monate` : undefined} />
</FieldGrid>
<Divider sx={{ my: 2 }} />
<SectionTitle title="Verfügbarkeit" />
<FieldGrid>
<Field label="Status" value={getAvailabilityLabel(p.availabilityStatus)} />
<Field label="Verfügbar ab" value={p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : undefined} />
<Field label="Verfügbarkeitstyp" value={p.availabilityType} />
</FieldGrid>
{hf && (
<>
<Divider sx={{ my: 2 }} />
<SectionTitle title="Technische Details" />
<FieldGrid>
<Field label="Etage" value={hf.floor ?? p.floorLevel} />
<Field label="Ausbaustandard" value={hf.fitOut} />
<Field label="Parkplätze" value={hf.parking} />
<Field label="ÖV-Score" value={hf.publicTransportScore} />
<Field label="Deckenhöhe (m)" value={hf.ceilingHeightM} />
<Field label="Laderampen" value={hf.loadingDocksCount} />
<Field label="Stromanschluss (kVA)" value={hf.powerSupplyKva} />
<Field label="Serverraum" value={hf.hasServerRoom} />
<Field label="Barrierefrei" value={hf.isBarrierFree} />
</FieldGrid>
</>
)}
</Box>
)
}
// ── Matchability tab ──────────────────────────────────────────────────────────
function SoftFactorsPanel({ p }: { p: Property }) {
const sf = p.softFactors
if (!sf) {
return <Typography variant="body2" color="text.secondary">Keine Soft Factors vorhanden.</Typography>
}
const prestige = sf.prestigeScore ?? sf.prestige
const visibility = sf.visibilityScore
const footfall = sf.footfallScore
const commuter = sf.commuterAccessScore ?? sf.accessibility
const talent = sf.talentAccessScore ?? sf.talentAccess
const esg = sf.esgScore
const flex = sf.flexibilityScore
const expansion = sf.expansionPotentialScore
const tax = sf.taxEnvironmentScore
function MatchabilityTabPanel({ propertyId }: { propertyId: string }) {
const [matches, setMatches] = useState<PropertyNeedMatch[]>([])
const [loading, setLoading] = useState(true)
return (
<Box>
<SectionTitle title="Weiche Faktoren" />
<ScoreRow label="Prestige / Lage" value={prestige} />
<ScoreRow label="Sichtbarkeit" value={visibility} />
<ScoreRow label="Passantenfrequenz" value={footfall} />
<ScoreRow label="Pendlerzugang" value={commuter} />
<ScoreRow label="Talentpool-Zugang" value={talent} />
<ScoreRow label="ESG-Score" value={esg} />
<ScoreRow label="Flexibilität" value={flex} />
<ScoreRow label="Expansionspotenzial" value={expansion} />
<ScoreRow label="Steuerumfeld" value={tax} />
{sf.parkingSpots !== undefined && (
<Field label="Parkplätze" value={sf.parkingSpots} />
)}
{sf.publicTransportMinutes !== undefined && (
<Field label="ÖV-Erreichbarkeit (Min.)" value={sf.publicTransportMinutes} />
)}
{sf.infrastructureNotes && (
<Field label="Infrastruktur-Notizen" value={sf.infrastructureNotes} />
)}
</Box>
)
}
useEffect(() => {
let cancelled = false
setLoading(true)
matchService.getNeedMatchesForProperty(propertyId, { minScore: 80 }).then(result => {
if (!cancelled) { setMatches(result); setLoading(false) }
})
return () => { cancelled = true }
}, [propertyId])
function MatchabilityPanel({ matches }: { matches: Match[] }) {
if (matches.length === 0) {
if (loading) {
return (
<Typography variant="body2" color="text.secondary">
Keine Matches für dieses Objekt vorhanden.
</Typography>
<Box sx={{ p: 3, display: 'flex', justifyContent: 'center' }}>
<CircularProgress size={24} />
</Box>
)
}
return (
<Box>
<SectionTitle title={`${matches.length} Matches gefunden`} />
{matches.map(m => (
<Box key={m.id} sx={{ p: 1.5, border: '1px solid', borderColor: 'divider', borderRadius: 1, mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Bedarf: {m.needId}</Typography>
<Chip
label={`Score ${m.matchScore}`}
size="small"
sx={{
fontWeight: 700,
bgcolor: m.matchScore >= 80 ? '#1a7a4a' : m.matchScore >= 60 ? '#d97706' : '#c0392b',
color: 'white',
}}
/>
</Box>
{m.positiveFactors?.slice(0, 3).map((f, i) => (
<Typography key={i} variant="caption" sx={{ display: 'block', color: '#1a7a4a' }}> {f.explanation ?? f.criterion}</Typography>
))}
{m.missingData && m.missingData.length > 0 && (
<Typography variant="caption" sx={{ color: 'warning.main', display: 'block', mt: 0.5 }}>
{m.missingData.length} fehlende Datenfelder
</Typography>
)}
</Box>
))}
</Box>
)
}
function SignalsPanel({ signals }: { signals: FutureSignal[] }) {
if (signals.length === 0) {
return <Typography variant="body2" color="text.secondary">Keine Zukunftssignale für dieses Objekt.</Typography>
}
return (
<Box>
<SectionTitle title={`${signals.length} Zukunftssignale`} />
{signals.map(s => (
<Box key={s.id} sx={{ p: 1.5, border: '1px solid', borderColor: 'divider', borderRadius: 1, mb: 1.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.25 }}>
{s.title ?? s.signalType}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{s.locationHint} · Konfidenz {Math.round(s.confidenceScore * 100)}% · {s.timeHorizonMonths} Monate
</Typography>
{s.disclaimer && (
<Typography variant="caption" sx={{ color: 'text.disabled', display: 'block', mt: 0.5 }}>
{s.disclaimer}
</Typography>
)}
</Box>
))}
<Box sx={{ p: 2.5 }}>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: '#0f172a' }}>
Matchability
</Typography>
<Typography variant="caption" color="text.secondary">
Bedarfsprofile mit 80% Match-Score für dieses Objekt
</Typography>
</Box>
{matches.length === 0 ? (
<Alert severity="info" sx={{ fontSize: '0.8125rem' }}>
Keine Bedarfsprofile mit 80% Match-Score für dieses Objekt gefunden.
</Alert>
) : (
matches.map(m => <NeedMatchCard key={m.matchId} match={m} />)
)}
</Box>
)
}
// ── Main component ────────────────────────────────────────────────────────────
const TABS = ['Übersicht', 'Verhandlung', 'Hard Facts', 'Soft Factors', 'Matchability', 'Datenqualität', 'Quelle', 'Signale', 'Aktivitätslog'] as const
const TABS = ['Übersicht', 'Matchability', 'Marktsignale'] as const
export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewProps) {
const [tab, setTab] = useState(0)
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState<UpdatePropertyInput>({})
const [saving, setSaving] = useState(false)
const queryClient = useQueryClient()
const { data: property, isLoading } = usePropertyById(propertyId)
const { data: matches = [] } = usePropertyMatches(propertyId)
const { data: signals = [] } = usePropertySignals(propertyId)
const showToast = useToastStore(s => s.showToast)
function startEdit() {
setDraft({})
setEditing(true)
}
async function saveEdit() {
if (!property) return
setSaving(true)
try {
await propertyService.update(property.id, draft)
await queryClient.invalidateQueries({ queryKey: ['property', propertyId] })
setEditing(false)
setDraft({})
showToast('Objekt gespeichert.', 'success')
} catch {
showToast('Fehler beim Speichern.', 'error')
} finally {
setSaving(false)
}
}
if (isLoading) return <PropertyDetailSkeleton />
if (!property) {
@@ -355,16 +329,53 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
<Box sx={{ p: 2.5, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
<Chip label={getAssetTypeLabel(property.assetType)} size="small" sx={{ bgcolor: getAssetTypeColor(property.assetType), color: 'white', fontSize: '0.68rem' }} />
<Chip label={getResultTypeLabel(property.resultType)} size="small" sx={{ bgcolor: getResultTypeColor(property.resultType), color: 'white', fontSize: '0.68rem' }} />
<Chip label={getAvailabilityLabel(property.availabilityStatus)} size="small" color={getAvailabilityChipColor(property.availabilityStatus)} variant="outlined" />
<Chip
label={getAssetTypeLabel(property.assetType)}
size="small"
sx={{ bgcolor: getAssetTypeColor(property.assetType), color: 'white', fontSize: '0.68rem' }}
/>
<Chip
label={getAvailabilityLabel(property.availabilityStatus)}
size="small"
color={getAvailabilityChipColor(property.availabilityStatus)}
variant="outlined"
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 1 }}>
{tab === 0 && !editing && (
<IconButton size="small" onClick={startEdit} title="Bearbeiten">
<Edit2 size={15} />
</IconButton>
)}
{tab === 0 && editing && (
<>
<Button
size="small"
variant="contained"
startIcon={saving ? <CircularProgress size={12} sx={{ color: 'white' }} /> : <Save size={13} />}
onClick={saveEdit}
disabled={saving}
sx={{ textTransform: 'none', fontSize: '0.75rem', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
>
Speichern
</Button>
<Button
size="small"
onClick={() => { setEditing(false); setDraft({}) }}
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
>
Abbrechen
</Button>
</>
)}
{onClose && (
<IconButton size="small" onClick={onClose}>
<X size={16} />
</IconButton>
)}
</Box>
{onClose && (
<IconButton size="small" onClick={onClose} sx={{ flexShrink: 0, ml: 1 }}>
<X size={16} />
</IconButton>
)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3, mb: 0.25 }}>
{property.title}
</Typography>
@@ -384,7 +395,7 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
)}
</Box>
{/* Photo only — map moved into Übersicht tab */}
{/* Photo */}
{property.images?.[0] && (
<Box sx={{ flexShrink: 0, height: 160, overflow: 'hidden' }}>
<img
@@ -399,14 +410,10 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
<Tabs
value={tab}
onChange={(_, v) => setTab(v)}
variant="scrollable"
scrollButtons
allowScrollButtonsMobile
sx={{
borderBottom: '1px solid #e2e8f0',
flexShrink: 0,
'& .MuiTab-root': { textTransform: 'none', fontSize: '0.8rem', minWidth: 'auto', px: 1.5 },
'& .MuiTabScrollButton-root': { width: 28 },
'& .MuiTab-root': { textTransform: 'none', fontSize: '0.8rem', minWidth: 'auto', px: 2 },
}}
>
{TABS.map(label => (
@@ -415,16 +422,17 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
</Tabs>
{/* Tab content */}
<Box sx={{ flex: 1, overflowY: 'auto', p: 2.5 }}>
{tab === 0 && <OverviewPanel p={property} />}
{tab === 1 && <NegotiationInsightsPanel property={property} />}
{tab === 2 && <HardFactsPanel p={property} />}
{tab === 3 && <SoftFactorsPanel p={property} />}
{tab === 4 && <MatchabilityPanel matches={matches} />}
{tab === 5 && <DQPanel property={property} />}
{tab === 6 && <ProvenancePanel property={property} />}
{tab === 7 && <SignalsPanel signals={signals} />}
{tab === 8 && <PropertyActivityLogPanel propertyId={propertyId} />}
<Box sx={{ flex: 1, overflowY: 'auto', p: tab === 0 ? 2.5 : 0 }}>
{tab === 0 && (
<OverviewPanel
p={property}
editing={editing}
draft={draft}
onDraftChange={setDraft}
/>
)}
{tab === 1 && <MatchabilityTabPanel propertyId={propertyId} />}
{tab === 2 && <PropertyMarketSignalsTab propertyId={propertyId} />}
</Box>
</Box>
)