b2530f9e20
Reale Jahresbelastung (Change 6): - FitOutCostPanel zeigt Jahresmiete + amortisierte Ausbaukosten für alle fitOut-Werte - FULL/PREMIUM: Ausbau CHF 0 (bezugsfertig), SHELL/BASIC: CRB/BKP-Richtwerte amortisiert über 5 J. - Match-Card-Chip zeigt geschätzte Investition (orange wenn >100k) - FitOutInvestment-Typ + calcFitOutInvestment() in fitOutUtils.ts - BackendAIService + MockAIService mit generateFitOutAdvice (IAIService-Interface) Must-have Kriterien (Nicht prüfbar Fix): - ÖV-Anbindung: Minutengrenze aus Freitext extrahiert, gegen publicTransportMinutes geprüft - Mindestfläche: m²-Wert aus Freitext extrahiert, gegen areaSqm geprüft - Ausbaugrad: neues Keyword-Rule für FULL/PREMIUM fitOut-Datenpflege: - fitOut-Werte zu 32 fehlenden Properties ergänzt (Logistik=SHELL, Standard=BASIC, Modern=FULL) - MAB-Werte (200/150/250 CHF/m²) zu 3 BASIC-Objekten hinzugefügt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
import { memo, useState } from 'react'
|
|
import { Box, Button, Paper, Typography } from '@mui/material'
|
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { PropertyMap } from '../shared'
|
|
import {
|
|
ExecutiveSummaryPanel,
|
|
NeedAlignmentPanel,
|
|
LocationIntelligencePanel,
|
|
TradeoffPanel,
|
|
RiskPanel,
|
|
MissingInformationPanel,
|
|
FutureAvailabilityContextPanel,
|
|
ScoreBreakdownPanel,
|
|
} from '.'
|
|
import { MatchDetailPropertySections } from './MatchDetailPropertySections'
|
|
import { useMatchDetail } from '../../hooks/useMatches'
|
|
import type { Property } from '../../domain/property'
|
|
import type { Need } from '../../domain/need'
|
|
import type { FutureSignal } from '../../domain/futureSignal'
|
|
import { DS_TEXT, DS_BORDER, DS_BG } from '../../lib/ds'
|
|
import { lookupCityCoords } from '../../lib/locationIntelligence'
|
|
|
|
type Match = NonNullable<ReturnType<typeof useMatchDetail>['data']>
|
|
|
|
interface Props {
|
|
match: Match
|
|
property: Property | null
|
|
need: Need | null
|
|
signal: FutureSignal | null
|
|
isFuture: boolean
|
|
taxCalculatorUrl?: string
|
|
}
|
|
|
|
export const MatchDetailScoreBreakdown = memo(function MatchDetailScoreBreakdown({
|
|
match,
|
|
property,
|
|
need,
|
|
signal,
|
|
isFuture,
|
|
taxCalculatorUrl,
|
|
}: Props) {
|
|
const [showFullAnalysis, setShowFullAnalysis] = useState(false)
|
|
|
|
const searchCenter = need?.preferredLocations?.[0] ? lookupCityCoords(need.preferredLocations[0]) : null
|
|
const searchLabel = need?.preferredLocations?.[0]
|
|
|
|
return (
|
|
<>
|
|
{/* ── Full analysis toggle ── */}
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => setShowFullAnalysis(v => !v)}
|
|
fullWidth
|
|
endIcon={showFullAnalysis ? <ChevronUp size={15} /> : <ChevronDown size={15} />}
|
|
sx={{
|
|
borderStyle: 'dashed', color: DS_TEXT.muted, borderColor: DS_BORDER.strong,
|
|
textTransform: 'none', fontWeight: 500,
|
|
'&:hover': { borderColor: DS_TEXT.muted, bgcolor: DS_BG.subtle },
|
|
}}
|
|
>
|
|
{showFullAnalysis ? 'Weniger anzeigen' : 'Vollständige Analyse anzeigen'}
|
|
</Button>
|
|
|
|
{/* ── Full analysis (hidden by default) ── */}
|
|
{showFullAnalysis && (
|
|
<>
|
|
<ExecutiveSummaryPanel match={match} />
|
|
<NeedAlignmentPanel match={match} need={need} property={property} />
|
|
{!isFuture && property && <MatchDetailPropertySections property={property} match={match} />}
|
|
<LocationIntelligencePanel property={property} />
|
|
{!isFuture && property?.images?.[0] && property?.location?.coordinates && (
|
|
<Paper sx={{ overflow: 'hidden', p: 0 }}>
|
|
<Box sx={{ px: 2.5, pt: 2, pb: 1 }}>
|
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>Standort</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
|
|
</Typography>
|
|
</Box>
|
|
<PropertyMap
|
|
lat={property.location.coordinates.lat}
|
|
lng={property.location.coordinates.lng}
|
|
label={property.title}
|
|
height={220}
|
|
searchCenterLat={searchCenter?.lat}
|
|
searchCenterLng={searchCenter?.lng}
|
|
searchRadiusKm={need?.searchRadius}
|
|
searchLabel={searchLabel}
|
|
/>
|
|
</Paper>
|
|
)}
|
|
<TradeoffPanel match={match} />
|
|
<RiskPanel match={match} />
|
|
<MissingInformationPanel match={match} />
|
|
{isFuture && <FutureAvailabilityContextPanel match={match} signal={signal} />}
|
|
<ScoreBreakdownPanel match={match} taxCalculatorUrl={taxCalculatorUrl} isFuture={isFuture} signal={signal} />
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
})
|