feat: F012 match detail view

Full decision analysis page at /demand/results/:matchId:
- MatchDetailHeader: score, badges, CTA (compare/shortlist), back button
- ExecutiveSummaryPanel: 4-row summary (fit, top reason, tradeoff, next step)
- PropertyOverviewPanel: key facts grid; signal-aware for FUTURE_AVAILABILITY
- NeedAlignmentPanel: comparison table with fit indicators (MATCH/PARTIAL/NO_MATCH)
- ScoreBreakdownPanel: hard/soft scores + modifiers + total (sidebar)
- TradeoffPanel: severity-coded list with mitigation hints
- RiskPanel: risks sorted CRITICAL→LOW with category chips
- MissingInformationPanel: priority-grouped with per-item CTAs
- SourceProvenancePanel: source type, label, URL, freshness
- FutureAvailabilityContextPanel: mandatory disclaimer + signal metadata
- NextActionsPanel: engine-ranked actions + standard actions (sidebar)
- Details button on result cards now navigates to match detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-16 13:55:46 +02:00
parent ea221def74
commit 7cf8d8ba72
16 changed files with 1115 additions and 2 deletions
@@ -0,0 +1,67 @@
import { Box, Chip, Paper, Typography } from '@mui/material'
import { Banknote, Calendar, MapPin, Maximize2, Tag } from 'lucide-react'
import type { Match } from '../../domain/match'
import type { Property } from '../../domain/property'
import type { FutureSignal } from '../../domain/futureSignal'
interface FactRowProps {
icon: React.ReactNode
label: string
value: string
}
function FactRow({ icon, label, value }: FactRowProps) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, py: 1, borderBottom: '1px solid #f1f5f9' }}>
<Box sx={{ color: '#64748b', flexShrink: 0 }}>{icon}</Box>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 140 }}>{label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>{value}</Typography>
</Box>
)
}
interface Props {
match: Match
property: Property | null
signal: FutureSignal | null
}
export function PropertyOverviewPanel({ match, property, signal }: Props) {
if (!property && !signal) return null
const isFuture = match.resultType === 'FUTURE_AVAILABILITY'
const rows: FactRowProps[] = isFuture ? [
{ icon: <MapPin size={15} />, label: 'Standorthinweis', value: signal?.locationHint ?? '' },
{ icon: <Maximize2 size={15} />, label: 'Flächenschätzung', value: signal?.areaSqmEstimate ? `~${signal.areaSqmEstimate}` : 'unbekannt' },
{ icon: <Calendar size={15} />, label: 'Zeithorizont', value: signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : '' },
{ icon: <Tag size={15} />, label: 'Wahrscheinlichkeit', value: signal?.probability ? `${Math.round(signal.probability * 100)}%` : '' },
] : [
{ icon: <MapPin size={15} />, label: 'Standort', value: property ? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}` : '' },
{ icon: <Maximize2 size={15} />, label: 'Nutzfläche', value: property ? `${property.areaSqm}` : '' },
{ icon: <Banknote size={15} />, label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²` : '' },
{ icon: <Calendar size={15} />, label: 'Verfügbar ab', value: property?.availabilityDate ?? '' },
{ icon: <Tag size={15} />, label: 'Objekttyp', value: property?.assetType ?? '' },
]
return (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>
{isFuture ? 'Signal-Übersicht' : 'Objekt-Übersicht'}
</Typography>
{property?.status && (
<Chip label={property.status} size="small" variant="outlined" />
)}
</Box>
{rows.map((row, i) => (
<FactRow key={i} {...row} />
))}
{property?.description && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1.5, fontStyle: 'italic' }}>
{property.description}
</Typography>
)}
</Paper>
)
}