Files
property-match/src/components/match-card/MatchCardExpanded.tsx
T
Benjamin Sutter ea221def74 feat: F011 match card system
- MatchCardViewModel: typed view model decoupling card from raw API data
- Sub-components: MatchCardHeader, MatchScoreDisplay, MatchReasonList, TradeoffList, MatchDataQualitySummary, MatchActionToolbar, MatchCardSkeleton, MatchCardRestrictedState
- 4 variants: MatchCardCompact, MatchCardExpanded, MatchCardReview, MatchCardCompareMini
- MatchCard: main entry point with variant prop
- matchCardAdapter: buildMatchCardViewModel() converts UnifiedMatchResult -> ViewModel
- UnifiedResultCard: now thin wrapper using MatchCardCompact via adapter
- FUTURE_AVAILABILITY always shows non-dismissable disclaimer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:39:35 +02:00

103 lines
3.1 KiB
TypeScript

import { Alert, Box, Card, Divider, Typography } from '@mui/material'
import { MapPin } from 'lucide-react'
import { MatchCardHeader } from './MatchCardHeader'
import { MatchReasonList } from './MatchReasonList'
import { TradeoffList } from './TradeoffList'
import { MatchDataQualitySummary } from './MatchDataQualitySummary'
import { MatchActionToolbar } from './MatchActionToolbar'
import { MatchCardRestrictedState } from './MatchCardRestrictedState'
import type { MatchCardViewModel } from './MatchCardViewModel'
interface Props {
vm: MatchCardViewModel
}
export function MatchCardExpanded({ vm }: Props) {
if (vm.isRestricted) return <MatchCardRestrictedState />
return (
<Card sx={{ p: 3, mb: 2 }}>
{/* FUTURE_AVAILABILITY disclaimer — mandatory */}
{vm.disclaimer && (
<Alert severity="warning" sx={{ mb: 2 }}>
{vm.disclaimer}
</Alert>
)}
{vm.isReviewRequired && (
<Alert severity="info" sx={{ mb: 2 }}>
Manuelle Überprüfung erforderlich
</Alert>
)}
<MatchCardHeader vm={vm} />
{/* Primary summary zone */}
<Box sx={{ mt: 2, mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>
{vm.title}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
<MapPin size={14} color="#64748b" />
<Typography variant="body2" color="text.secondary">{vm.locationLabel}</Typography>
</Box>
{vm.availabilityLabel && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
Verfügbar: {vm.availabilityLabel}
</Typography>
)}
{vm.explainabilitySummary && (
<Typography
variant="body2"
color="text.secondary"
sx={{ mt: 1, p: 1.5, bgcolor: '#f8fafc', borderRadius: 1, borderLeft: '3px solid #e2e8f0' }}
>
{vm.explainabilitySummary}
</Typography>
)}
</Box>
<Divider sx={{ mb: 2 }} />
{/* Why it matches — all 3 reasons */}
{vm.reasons.length > 0 && (
<Box sx={{ mb: 2 }}>
<MatchReasonList reasons={vm.reasons} maxItems={3} />
</Box>
)}
{/* Tradeoffs — up to 3 */}
{vm.tradeoffs.length > 0 && (
<>
<Divider sx={{ mb: 1.5 }} />
<Box sx={{ mb: 2 }}>
<TradeoffList tradeoffs={vm.tradeoffs} maxItems={3} />
</Box>
</>
)}
{/* Data quality — full view */}
<Divider sx={{ mb: 1.5 }} />
<Box sx={{ mb: 2 }}>
<MatchDataQualitySummary
dataQualityScore={vm.dataQualityScore}
missingData={vm.missingData}
/>
</Box>
{vm.sourceLabel && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.5 }}>
Quelle: {vm.sourceLabel}
{vm.externalUrl && (
<a href={vm.externalUrl} target="_blank" rel="noopener noreferrer" style={{ marginLeft: 4 }}>
</a>
)}
</Typography>
)}
<MatchActionToolbar actions={vm.actions} />
</Card>
)
}