da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
import { Box, Button, Chip, Typography } from '@mui/material'
|
|
import { MatchStatusBadge } from './MatchStatusBadge'
|
|
import type { Match } from '../../domain/match'
|
|
import type { Property } from '../../domain/property'
|
|
import type { Need } from '../../domain/need'
|
|
import { matchScoreHex } from '../../lib/utils'
|
|
|
|
const STRENGTH_META: Record<string, { label: string; bg: string; color: string }> = {
|
|
STRONG: { label: 'Stark', bg: '#f0fdf4', color: '#1a7a4a' },
|
|
MODERATE: { label: 'Mittel', bg: '#fefce8', color: '#d97706' },
|
|
WEAK: { label: 'Schwach', bg: '#fff1f2', color: '#c0392b' },
|
|
}
|
|
|
|
interface Props {
|
|
match: Match
|
|
property: Property | undefined
|
|
need: Need | undefined
|
|
onSelect: () => void
|
|
onApprove: () => void
|
|
}
|
|
|
|
export function MatchListCard({ match, property, need, onSelect, onApprove }: Props) {
|
|
const strength = STRENGTH_META[match.matchStrength] ?? { label: match.matchStrength, bg: '#f1f5f9', color: '#64748b' }
|
|
const summary = match.explainabilitySummary ?? ''
|
|
|
|
return (
|
|
<Box
|
|
onClick={onSelect}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 2,
|
|
px: 3,
|
|
py: 2,
|
|
borderBottom: '1px solid #e2e8f0',
|
|
bgcolor: 'white',
|
|
cursor: 'pointer',
|
|
'&:hover': { bgcolor: '#f8fafc' },
|
|
}}
|
|
>
|
|
{/* Score bubble */}
|
|
<Box
|
|
sx={{
|
|
width: 52,
|
|
height: 52,
|
|
borderRadius: 2,
|
|
bgcolor: matchScoreHex(match.matchScore),
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ color: 'white', fontWeight: 700, lineHeight: 1 }}>
|
|
{match.matchScore}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Property + need */}
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25, flexWrap: 'wrap' }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>
|
|
{property?.title ?? match.propertyId}
|
|
</Typography>
|
|
<MatchStatusBadge status={match.status} />
|
|
</Box>
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
|
{[property?.location.city, property?.areaSqm ? `${property.areaSqm} m²` : null].filter(Boolean).join(' · ')}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', gap: 0.75, mt: 0.5, flexWrap: 'wrap' }}>
|
|
{need && (
|
|
<Chip
|
|
label={need.isAnonymous ? 'Anonyme Anfrage' : need.companyName}
|
|
size="small"
|
|
sx={need.isAnonymous
|
|
? { bgcolor: '#f5f3ff', color: '#6d28d9', fontSize: 11, height: 20, fontWeight: 600, border: '1px solid #ddd6fe' }
|
|
: { bgcolor: '#eff6ff', color: '#152642', fontSize: 11, height: 20, fontWeight: 500 }
|
|
}
|
|
/>
|
|
)}
|
|
<Chip
|
|
label={strength.label}
|
|
size="small"
|
|
sx={{ bgcolor: strength.bg, color: strength.color, fontSize: 11, height: 20 }}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Summary excerpt */}
|
|
{summary && (
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{
|
|
maxWidth: 220,
|
|
display: { xs: 'none', lg: 'block' },
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{summary.length > 90 ? summary.slice(0, 90) + '…' : summary}
|
|
</Typography>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<Box sx={{ display: 'flex', gap: 0.75, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
|
{match.status !== 'APPROVED' && (
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
onClick={onApprove}
|
|
sx={{ textTransform: 'none', bgcolor: '#1a7a4a', '&:hover': { bgcolor: '#15643c' } }}
|
|
>
|
|
Genehmigen
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
onClick={onSelect}
|
|
sx={{ textTransform: 'none' }}
|
|
>
|
|
Details
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|