import { Box, Button, Chip, CircularProgress, Divider, Paper, Typography } from '@mui/material' import { ArrowLeft, Bookmark, Building2, Clock, Columns2, ExternalLink, Info, Layers, ShieldCheck, Tag, Train, TrendingUp } from 'lucide-react' import { useNavigate, useParams } from 'react-router' import type { PropertyUnit } from '../../domain/property' import { useQuery } from '@tanstack/react-query' import { useMatchDetail } from '../../hooks/useMatches' import { propertyService } from '../../services/propertyService' import { needService } from '../../services/needService' import { futureSignalService } from '../../services/futureSignalService' import { useCompareStore } from '../../stores/compareStore' import { useShortlistStore } from '../../stores/shortlistStore' import { AddToShortlistDialog } from '../../components/shortlist' import { MatchReasonList } from '../../components/match-card/MatchReasonList' import { MatchScoreDisplay } from '../../components/match-card/MatchScoreDisplay' import { PropertyMap } from '../../components/shared' import { getCityIntelligence } from '../../lib/locationIntelligence' import { LocationIntelligencePanel, ExecutiveSummaryPanel, NeedAlignmentPanel, ScoreBreakdownPanel, TradeoffPanel, RiskPanel, MissingInformationPanel, SourceProvenancePanel, FutureAvailabilityContextPanel, NextActionsPanel, } from '../../components/match-detail' import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel' // ── Property detail helpers ──────────────────────────────────────────────────── const FLOOR_LABEL = (level: number) => level === 0 ? 'EG' : level < 0 ? `UG ${Math.abs(level)}` : `${level}.OG` const ASSET_LABELS: Record = { OFFICE: 'Büro', LOGISTICS: 'Lager / Logistik', RETAIL: 'Retail / Laden', PRODUCTION: 'Produktion', MIXED: 'Gewerbe (gemischt)', } const RISK_LABELS: Record = { LOW: 'Niedrig', MEDIUM: 'Mittel', HIGH: 'Hoch' } const SOURCE_LABELS: Record = { ERP_IMPORT: 'ERP-Import (intern)', IMMOSCOUT_SCRAPE: 'ImmoScout24', HOMEGATE_SCRAPE: 'Homegate', MATCHOFFICE_SCRAPE: 'MatchOffice', NEWHOME_SCRAPE: 'newhome.ch', AI_SIGNAL: 'KI-Signal', MANUAL: 'Manuell erfasst', } const PASSERBY_LABELS: Record = { LOW: 'Gering', MEDIUM: 'Mittel', HIGH: 'Hoch', VERY_HIGH: 'Sehr hoch', } function KeyFactRow({ label, value }: { label: string; value?: string | null }) { if (!value) return null return ( {label} {value} ) } function UnitStatusChip({ unit }: { unit: PropertyUnit }) { if (unit.schattenmarktRelease?.enabled) { return } label="PRE-MARKET" sx={{ bgcolor: '#f0fdf4', color: '#1a7a4a', border: '1px solid #86efac', fontWeight: 700, fontSize: '0.68rem', height: 20 }} /> } if (unit.available) { return } return } function UnitRow({ unit, highlighted }: { unit: PropertyUnit; highlighted: boolean }) { const availableFrom = unit.schattenmarktRelease?.availableFrom ?? unit.leaseEndDate const monthlyRent = unit.rentPricePerSqm ? Math.round(unit.rentPricePerSqm / 12) : undefined return ( {FLOOR_LABEL(unit.floorLevel)}{unit.unitLabel ? ` · ${unit.unitLabel}` : ''} {unit.currentTenant && {unit.currentTenant}} {unit.areaSqm.toLocaleString('de-CH')} m² {monthlyRent ? `CHF ${monthlyRent}/m²/Mt.` : '–'} {availableFrom ? new Date(availableFrom).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' }) : '–'} ) } // ── Match helpers ────────────────────────────────────────────────────────────── const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing']) const RESULT_TYPE_META: Record = { VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, EXTERNAL_MARKET: { label: 'Direktinserat', color: '#d97706' }, MAISON_WORK: { label: 'Maison Work', color: '#0369a1' }, FUTURE_AVAILABILITY: { label: 'Future Availability', color: '#7c3aed' }, } function buildReasons(match: NonNullable['data']>): MatchCardReason[] { return match.positiveFactors.slice(0, 3).map(f => ({ type: (HARD_CRITERIA.has(f.criterion) ? 'HARD_FACT' : 'SOFT_FACTOR') as MatchCardReason['type'], label: f.criterion.charAt(0).toUpperCase() + f.criterion.slice(1), explanation: f.explanation, score: f.score, })) } export default function MatchDetail() { const { matchId } = useParams<{ matchId: string }>() const navigate = useNavigate() const { addToCompare } = useCompareStore() const { openAddDialog } = useShortlistStore() const { data: match, isLoading } = useMatchDetail(matchId ?? '') const isFuture = match?.resultType === 'FUTURE_AVAILABILITY' const { data: property = null } = useQuery({ queryKey: ['property', match?.propertyId], queryFn: () => propertyService.getById(match!.propertyId), enabled: !!match && !isFuture, select: r => r.data ?? null, }) const { data: need = null } = useQuery({ queryKey: ['need', match?.needId], queryFn: () => needService.getById(match!.needId), enabled: !!match?.needId, select: r => r.data ?? null, }) const { data: signal = null } = useQuery({ queryKey: ['signal', match?.resultId], queryFn: () => futureSignalService.getById(match!.resultId!), enabled: !!match && isFuture && !!match.resultId, select: r => r.data ?? null, }) if (isLoading) { return ( ) } if (!match) { return ( Match nicht gefunden ) } const reasons = buildReasons(match) const rt = RESULT_TYPE_META[match.resultType ?? 'VERIFIED_PORTFOLIO'] ?? { label: '–', color: '#64748b' } const title = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–' const city = property?.location?.city const location = city ? `${city}${property?.location?.district ? `, ${property.location.district}` : ''}` : signal?.locationHint ?? '–' // Tax calculator — only shown when AI evaluated taxEnvironment const allFactors = [...match.positiveFactors, ...(match.negativeFactors ?? [])] const hasTaxFactor = allFactors.some(f => f.criterion === 'taxEnvironment') const cityIntel = city ? getCityIntelligence(city) : null const taxCalculatorUrl = hasTaxFactor ? (cityIntel?.taxCalculatorUrl ?? undefined) : undefined const handleCompare = () => { if (match && !isFuture && property) { addToCompare({ resultType: property.resultType === 'EXTERNAL_MARKET' ? 'EXTERNAL_MARKET' : 'VERIFIED_PORTFOLIO', matchId: match.id, needId: match.needId, matchScore: match.matchScore, match, property, }) } else if (match && isFuture && signal) { addToCompare({ resultType: 'FUTURE_AVAILABILITY', matchId: match.id, needId: match.needId, matchScore: match.matchScore, match, signal, }) } navigate('/demand/compare') } const handleShortlist = () => { openAddDialog({ resultId: match.id, resultType: match.resultType ?? 'VERIFIED_PORTFOLIO', title: property?.title ?? signal?.companyName ?? signal?.locationHint ?? match.id, matchScore: match.matchScore, confidenceScore: match.confidenceLevel, sourceLabel: property?.sourceLabel ?? match.resultType ?? 'VERIFIED_PORTFOLIO', addedBy: 'admin@ideal-sharing.ch', propertyId: property?.id, }) } // Key facts for the strip below the hero const keyFacts = isFuture ? [ { label: 'Flächenschätzung', value: signal?.areaSqmEstimate ? `~${signal.areaSqmEstimate.toLocaleString('de-CH')} m²` : '–' }, { label: 'Zeithorizont', value: signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : '–' }, { label: 'Wahrscheinlichkeit', value: signal?.probability ? `${Math.round(signal.probability * 100)}%` : '–' }, ] : [ { label: 'Nutzfläche', value: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : '–' }, { label: 'Miete/m²/Jahr', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}` : '–' }, { label: 'Verfügbar ab', value: property?.availabilityDate ?? '–' }, { label: 'Nutzungsart', value: property?.assetType ?? '–' }, ] return ( {/* Sticky back nav */} {isFuture && ( )} {/* Hero: image first, map fallback */} {!isFuture && ( property?.images?.[0] ? ( {property.title} ) : property?.location?.coordinates ? ( ) : null )} {/* Property header — white section below hero */} {/* Left: title + address + chips */} {title} {location} {property?.assetType && ( )} {/* Right: score + actions */} {/* Key facts strip */} {keyFacts.map((fact, i) => ( {fact.label} {fact.value} ))} {/* Main content */} {/* Main column */} {/* ── Property Details ── */} {!isFuture && property && (() => { const units = property.units ?? [] const matchedUnit = units.find(u => u.id === match.unitId) const flexibleUnits = units.filter(u => u.isFlexible && u.minLettableSqm != null) const preMarketUnits = units.filter(u => u.schattenmarktRelease?.enabled) const otherUnits = units.filter(u => !u.schattenmarktRelease?.enabled) const monthlyPerSqm = Math.round(property.rentPricePerSqm / 12) const totalMonthly = Math.round(property.areaSqm * property.rentPricePerSqm / 12) const minLettable = property.areaSqmMin ?? (flexibleUnits.length > 0 ? Math.min(...flexibleUnits.map(u => u.minLettableSqm!)) : undefined) const sourceLabel = property.sourceLabel ?? property.sourceMeta?.sourceLabel ?? SOURCE_LABELS[property.sourceType] ?? property.sourceType return ( <> {/* Preis */} Preis {property.ancillaryCosts != null && ( )} {/* Hauptangaben */} Hauptangaben {minLettable != null && } {property.contractDurationMonths != null && } {(property.floorLevel != null || matchedUnit) && ( )} {property.currentTenant && } {property.leaseEndDate && } {property.breakoutOption && } {property.riskLevel && } {property.expansionPotentialSqm != null && } {/* Eigenschaften */} {property.softFactors && ( Eigenschaften {property.softFactors.publicTransportMinutes != null && ( } label={`ÖV: ${property.softFactors.publicTransportMinutes} Min. zu Fuss`} sx={{ bgcolor: '#eff6ff', color: '#1d4ed8', border: '1px solid #bfdbfe', fontWeight: 500 }} /> )} {property.softFactors.parkingSpots != null && property.softFactors.parkingSpots > 0 && ( )} {property.softFactors.prestige != null && property.softFactors.prestige >= 80 && ( )} {property.softFactors.visibilityScore != null && property.softFactors.visibilityScore >= 80 && ( )} {property.softFactors.passerbyFrequency && ( )} {property.softFactors.talentAccess != null && property.softFactors.talentAccess >= 80 && ( )} )} {/* Wegzeit */} {property.softFactors?.publicTransportMinutes != null && ( Wegzeit {property.softFactors.publicTransportMinutes} Min. zu Fuss Nächster ÖV-Anschluss — {property.location.city} Die Zeiten beziehen sich auf die Strecke zu Fuss. )} {/* Einheiten */} {units.length > 0 && ( Einheiten {['Einheit', 'Fläche', 'Miete', 'Verfügbar ab', 'Status'].map(h => ( {h} ))} {preMarketUnits.map(u => )} {otherUnits.map(u => )} )} {/* Beschreibung */} {property.description && ( Beschreibung {property.description} )} {/* Quelle & Referenz */} Quelle & Referenz {property.propertyNumber && } {property.importedFrom && } {property.dataQuality.lastVerifiedAt && ( )} {property.sourceUrl && ( )} ) })()} {reasons.length > 0 && ( Warum dieses Match {match.negativeFactors.length > 0 && ( Schwächere Faktoren {match.negativeFactors.slice(0, 3).map((f, i) => ( · {f.criterion}: {f.explanation} ))} )} )} {/* Map — always show when image was the hero above */} {!isFuture && property?.images?.[0] && property?.location?.coordinates && ( Standort {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} )} {isFuture && } {/* Sidebar */} {}} onReject={() => {}} /> ) }