Files
property-match/src/components/match-card/MatchCardCompact.tsx
T
Benjamin Sutter 27c53f3af1 feat: score transparency on all cards + budget parser fix
- Single scoring system: MockupNeedProvider rewrites matches via calculateScore
  exclusively — allFactors always populated, no more dual-system (computeScore
  removed), weights from weightingProfile reflected in every breakdown
- ScoreInlineBreakdown: new component shows hard/soft criteria with importance
  labels (Entscheidend/Sehr wichtig/…) and formula on compact + expanded cards
- MatchCardAdapter: passes scoreBreakdown + allFactors to ViewModel
- MatchDetail: 'Zukunftssignal' label replaced with 'Future Availability'
- aiService budget parser: values < 100 treated as monthly and multiplied by 12
  to produce correct annual CHF/m²/year value — fixes 0-result searches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:41:46 +02:00

120 lines
3.6 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 { ScoreInlineBreakdown } from './ScoreInlineBreakdown'
import type { MatchCardViewModel } from './MatchCardViewModel'
interface Props {
vm: MatchCardViewModel
}
export function MatchCardCompact({ vm }: Props) {
if (vm.isRestricted) return <MatchCardRestrictedState />
const borderColor = vm.isCompareSelected
? '#7c3aed'
: vm.isSelected
? '#1e3a5f'
: 'transparent'
return (
<Card
sx={{
p: 2.5,
mb: 1.5,
border: `2px solid ${borderColor}`,
opacity: vm.isStaleData ? 0.75 : 1,
transition: 'border-color 0.15s',
}}
>
{/* FUTURE_AVAILABILITY disclaimer — mandatory, non-dismissable */}
{vm.disclaimer && (
<Alert severity="warning" sx={{ mb: 1.5, py: 0.5 }}>
{vm.disclaimer}
</Alert>
)}
{vm.isReviewRequired && (
<Alert severity="info" sx={{ mb: 1.5, py: 0.5 }}>
Manuelle Überprüfung erforderlich
</Alert>
)}
{/* Header: score → type → confidence → availability → risk */}
<MatchCardHeader vm={vm} compact />
{/* Title + location (max 2 lines) */}
<Box sx={{ mt: 1.25, mb: 1.25 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} noWrap>
{vm.title}
</Typography>
{vm.propertySubtitle && (
<Typography variant="caption" sx={{ display: 'block', color: '#64748b', fontWeight: 500, mt: 0.125 }} noWrap>
{vm.propertySubtitle}
</Typography>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
<MapPin size={13} color="#64748b" />
<Typography variant="body2" color="text.secondary">
{vm.locationLabel}
</Typography>
</Box>
{vm.explainabilitySummary && (
<Typography
variant="body2"
color="text.secondary"
sx={{
mt: 0.5,
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{vm.explainabilitySummary}
</Typography>
)}
</Box>
<Divider sx={{ mb: 1.25 }} />
{/* Score formula — always visible */}
{vm.scoreBreakdown && (
<Box sx={{ mb: 1.25 }}>
<ScoreInlineBreakdown scoreBreakdown={vm.scoreBreakdown} allFactors={vm.allFactors} compact />
</Box>
)}
{/* Top reason only */}
{vm.reasons.length > 0 && (
<Box sx={{ mb: 1 }}>
<MatchReasonList reasons={vm.reasons.slice(0, 1)} />
</Box>
)}
{/* Top tradeoff only (compact) */}
{vm.tradeoffs.length > 0 && (
<Box sx={{ mb: 1 }}>
<TradeoffList tradeoffs={vm.tradeoffs.slice(0, 1)} compact />
</Box>
)}
{/* Data quality — only critical warning in compact mode */}
<MatchDataQualitySummary
dataQualityScore={vm.dataQualityScore}
missingData={vm.missingData}
compact
/>
<Box sx={{ mt: 1.25 }}>
<MatchActionToolbar actions={vm.actions} compact />
</Box>
</Card>
)
}