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,70 @@
import { Box, Chip, Link, Paper, Typography } from '@mui/material'
import type { Property } from '../../domain/property'
const FRESHNESS_META: Record<string, { label: string; color: 'success' | 'warning' | 'error' }> = {
FRESH: { label: 'Aktuell (< 48h)', color: 'success' },
STALE: { label: 'Veraltet (214d)', color: 'warning' },
OUTDATED: { label: 'Outdated (> 14d)', color: 'error' },
}
interface Props {
property: Property | null
}
export function SourceProvenancePanel({ property }: Props) {
if (!property) return null
const freshness = property.dataQuality?.freshness
const freshnessMeta = freshness ? (FRESHNESS_META[freshness] ?? null) : null
const sourceUrl = property.sourceUrl ?? property.sourceMeta?.sourceUrl
const sourceLabel = property.sourceLabel ?? property.sourceMeta?.sourceLabel ?? property.sourceType ?? ''
const sourceUpdatedAt = property.sourceUpdatedAt ?? property.sourceMeta?.sourceUpdatedAt
const lastVerified = property.dataQuality?.lastVerifiedAt
const rows: { label: string; value: React.ReactNode }[] = [
{ label: 'Quelltyp', value: property.sourceType ?? '' },
{
label: 'Quelle',
value: sourceUrl ? (
<Link href={sourceUrl} target="_blank" rel="noopener noreferrer" variant="body2">
{sourceLabel}
</Link>
) : (
<Typography variant="body2">{sourceLabel}</Typography>
),
},
...(sourceUpdatedAt ? [{ label: 'Letzte Aktualisierung', value: <Typography variant="body2">{sourceUpdatedAt}</Typography> }] : []),
...(lastVerified ? [{ label: 'Letzte Verifikation', value: <Typography variant="body2">{lastVerified}</Typography> }] : []),
...(freshnessMeta ? [{
label: 'Aktualität',
value: <Chip label={freshnessMeta.label} size="small" color={freshnessMeta.color} />,
}] : []),
]
const warnings = property.dataQuality?.warnings ?? []
return (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Quelle & Herkunft</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{rows.map((row, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 160 }}>
{row.label}
</Typography>
{row.value}
</Box>
))}
</Box>
{warnings.length > 0 && (
<Box sx={{ mt: 1.5, p: 1, bgcolor: '#fffbeb', borderRadius: 1 }}>
{warnings.map((w, i) => (
<Typography key={i} variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{w}
</Typography>
))}
</Box>
)}
</Paper>
)
}