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,97 @@
import { Alert, Box, Chip, Paper, Typography } from '@mui/material'
import type { Match } from '../../domain/match'
import type { FutureSignal } from '../../domain/futureSignal'
const SIGNAL_TYPE_LABELS: Record<string, string> = {
EXPANSION: 'Expansion',
POSSIBLE_MOVE_OUT: 'Möglicher Auszug',
CONSTRUCTION_PROJECT: 'Bauvorhaben',
RESTRUCTURING: 'Restrukturierung',
PROJECT_DEVELOPMENT: 'Projektentwicklung',
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
}
const SENSITIVITY_META: Record<string, { label: string; color: 'error' | 'warning' | 'default' }> = {
CONFIDENTIAL: { label: 'Vertraulich', color: 'error' },
INTERNAL: { label: 'Intern', color: 'warning' },
PUBLIC: { label: 'Öffentlich', color: 'default' },
}
const REVIEW_STATUS_META: Record<string, { label: string; color: 'warning' | 'success' | 'default' | 'error' }> = {
UNREVIEWED: { label: 'Ungeprüft', color: 'warning' },
IN_REVIEW: { label: 'In Prüfung', color: 'warning' },
APPROVED: { label: 'Genehmigt', color: 'success' },
REJECTED: { label: 'Abgelehnt', color: 'error' },
FLAGGED: { label: 'Markiert', color: 'warning' },
}
interface Props {
match: Match
signal: FutureSignal | null
}
export function FutureAvailabilityContextPanel({ match: _match, signal }: Props) {
if (!signal) return null
const sensitiveMeta = SENSITIVITY_META[signal.sensitivityLevel] ?? SENSITIVITY_META.PUBLIC
const reviewMeta = signal.reviewStatus ? (REVIEW_STATUS_META[signal.reviewStatus] ?? null) : null
return (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Zukunftssignal Kontext</Typography>
{/* Mandatory disclaimer */}
<Alert severity="warning" sx={{ mb: 2 }}>
{signal.disclaimer}
</Alert>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{signal.signalType && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Signaltyp</Typography>
<Typography variant="body2">{SIGNAL_TYPE_LABELS[signal.signalType] ?? signal.signalType}</Typography>
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Konfidenz</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Math.round(signal.confidenceScore * 100)}%
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Wahrscheinlichkeit</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Math.round(signal.probability * 100)}%
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Zeithorizont</Typography>
<Typography variant="body2">~{signal.timeHorizonMonths} Monate</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Vertraulichkeit</Typography>
<Chip label={sensitiveMeta.label} size="small" color={sensitiveMeta.color} variant="outlined" />
</Box>
{reviewMeta && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Prüfstatus</Typography>
<Chip label={reviewMeta.label} size="small" color={reviewMeta.color} />
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>Quellglaubwürdigkeit</Typography>
<Typography variant="body2">{signal.source.credibility}</Typography>
</Box>
</Box>
{signal.evidence?.summary && (
<Box sx={{ mt: 1.5, p: 1.5, bgcolor: '#f8fafc', borderRadius: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
Evidenz
</Typography>
<Typography variant="body2" color="text.secondary">{signal.evidence.summary}</Typography>
</Box>
)}
</Paper>
)
}