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:
@@ -0,0 +1,113 @@
|
||||
import { Alert, Box, Button, Chip, Paper, Typography } from '@mui/material'
|
||||
import { ArrowLeft, Bookmark, Columns2 } from 'lucide-react'
|
||||
import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay'
|
||||
import type { Match } from '../../domain/match'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { FutureSignal } from '../../domain/futureSignal'
|
||||
|
||||
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
|
||||
EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' },
|
||||
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
|
||||
}
|
||||
|
||||
function confColor(c: number): string {
|
||||
return c >= 0.75 ? '#1a7a4a' : c >= 0.55 ? '#d97706' : '#c0392b'
|
||||
}
|
||||
|
||||
function dqColor(q: number): string {
|
||||
return q >= 0.80 ? '#1a7a4a' : q >= 0.60 ? '#d97706' : '#c0392b'
|
||||
}
|
||||
|
||||
interface Props {
|
||||
match: Match
|
||||
property: Property | null
|
||||
signal: FutureSignal | null
|
||||
onBack: () => void
|
||||
onCompare: () => void
|
||||
onShortlist: () => void
|
||||
}
|
||||
|
||||
export function MatchDetailHeader({ match, property, signal, onBack, onCompare, onShortlist }: Props) {
|
||||
const rt = RESULT_TYPE_META[match.resultType ?? 'VERIFIED_PORTFOLIO'] ?? { label: '–', color: '#64748b' }
|
||||
const title = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–'
|
||||
const isFuture = match.resultType === 'FUTURE_AVAILABILITY'
|
||||
const dqScore = property?.dataQuality?.score ?? signal?.confidenceScore ?? 0.5
|
||||
const confPct = Math.round(match.confidenceLevel * 100)
|
||||
const dqPct = Math.round(dqScore * 100)
|
||||
const location = property?.location?.city
|
||||
? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}`
|
||||
: signal?.locationHint ?? '–'
|
||||
const source = property?.sourceLabel ?? property?.sourceMeta?.sourceLabel ?? '–'
|
||||
const availability = property?.availabilityDate ?? (signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined)
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3, mb: 2 }}>
|
||||
{isFuture && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Probabilistisches Signal – keine bestätigte Fläche. Alle Angaben sind Schätzungen.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
startIcon={<ArrowLeft size={16} />}
|
||||
onClick={onBack}
|
||||
size="small"
|
||||
sx={{ mb: 1.5, color: '#64748b' }}
|
||||
>
|
||||
Zurück zu Resultaten
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 0.5 }}>{title}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>{location}</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, alignItems: 'center' }}>
|
||||
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600 }} />
|
||||
{property?.assetType && (
|
||||
<Chip label={property.assetType} size="small" variant="outlined" />
|
||||
)}
|
||||
<Chip
|
||||
label={`${confPct}% Konfidenz`}
|
||||
size="small"
|
||||
sx={{ bgcolor: confColor(match.confidenceLevel), color: 'white' }}
|
||||
/>
|
||||
<Chip
|
||||
label={`DQ ${dqPct}%`}
|
||||
size="small"
|
||||
sx={{ bgcolor: dqColor(dqScore), color: 'white' }}
|
||||
/>
|
||||
{availability && <Chip label={availability} size="small" variant="outlined" />}
|
||||
{source !== '–' && (
|
||||
<Typography variant="caption" color="text.secondary">Quelle: {source}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 1.5 }}>
|
||||
<MatchScoreDisplay score={match.matchScore} size="lg" />
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<Bookmark size={14} />}
|
||||
onClick={onShortlist}
|
||||
>
|
||||
Shortlist
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<Columns2 size={14} />}
|
||||
onClick={onCompare}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Vergleichen
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user