Files
property-match/src/components/ui/DecisionContextPanel.tsx
T
Benjamin Sutter 9e7a09f8a2 feat: F027 screen-level decision design — DecisionContextPanel + Properties + Results
- Add DecisionContextPanel (src/components/ui): compact decision-framing
  strip with question, metric chips, expandable risks/missing data, actions.
  Answers: Welche Entscheidung? Welche Daten helfen/fehlen? Welche Risiken?
  Welche nächste Aktion?

- Properties.tsx: replace generic listing header with decision frame:
  "Welche Objekte sind matchbereit und wo blockieren Datenlücken Matches?"
  Surfaces: matchbereit count, critical gaps, low-confidence count, stale
  data, missing field names, risk statements. Primary CTA → Datenpflege.

- Results.tsx: add decision frame above result feed:
  "Welche Treffer lohnen sich für die Shortlist — welche tragen Risiken?"
  Surfaces: strong-match count, result-type breakdown, data-gap count,
  future-signal probabilistic risk warning. CTAs → Compare, refine search.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:56:12 +02:00

159 lines
5.0 KiB
TypeScript

import { useState } from 'react'
import { Alert, Box, Button, Chip, Collapse, IconButton, Typography } from '@mui/material'
import { AlertTriangle, ChevronDown, ChevronUp, Target } from 'lucide-react'
import type { ReactNode } from 'react'
export interface DecisionMetric {
label: string
value: string | number
severity?: 'neutral' | 'positive' | 'warning' | 'critical'
}
export interface DecisionAction {
label: string
primary?: boolean
onClick: () => void
}
interface Props {
/** The core question this screen answers */
decision: string
/** One-line explanation of why this decision matters */
context?: string
/** Key data points relevant to the decision */
metrics?: DecisionMetric[]
/** Missing data that could affect the decision */
missing?: string[]
/** Active risks the user should be aware of */
risks?: string[]
/** Available actions — first primary action is highlighted */
actions?: DecisionAction[]
/** Custom content after the standard rows */
children?: ReactNode
}
const SEVERITY_COLOR: Record<NonNullable<DecisionMetric['severity']>, string> = {
neutral: '#f1f5f9',
positive: '#f0fdf4',
warning: '#fef9c3',
critical: '#fef2f2',
}
const SEVERITY_TEXT: Record<NonNullable<DecisionMetric['severity']>, string> = {
neutral: '#475569',
positive: '#1a7a4a',
warning: '#92400e',
critical: '#991b1b',
}
export function DecisionContextPanel({
decision,
context,
metrics = [],
missing = [],
risks = [],
actions = [],
children,
}: Props) {
const [expanded, setExpanded] = useState(false)
const hasDetails = missing.length > 0 || risks.length > 0 || !!children
return (
<Box
sx={{
bgcolor: '#f8fafc',
borderBottom: '1px solid #e2e8f0',
borderLeft: '3px solid #1e3a5f',
px: 2.5,
py: 1.25,
flexShrink: 0,
}}
>
{/* Main row */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, flexWrap: 'wrap' }}>
{/* Decision question */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flex: 1, minWidth: 200 }}>
<Target size={15} color="#1e3a5f" style={{ marginTop: 2, flexShrink: 0 }} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 700, fontSize: '0.8125rem', color: '#1e293b', lineHeight: 1.3 }}>
{decision}
</Typography>
{context && (
<Typography variant="caption" sx={{ color: '#64748b', display: 'block', mt: 0.25 }}>
{context}
</Typography>
)}
</Box>
</Box>
{/* Metric chips */}
{metrics.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
{metrics.map((m, i) => {
const sev = m.severity ?? 'neutral'
return (
<Chip
key={i}
label={`${m.value} ${m.label}`}
size="small"
sx={{
bgcolor: SEVERITY_COLOR[sev],
color: SEVERITY_TEXT[sev],
fontWeight: sev !== 'neutral' ? 700 : 400,
fontSize: '0.7rem',
height: 22,
}}
/>
)
})}
</Box>
)}
{/* Actions + expand toggle */}
<Box sx={{ display: 'flex', gap: 0.75, alignItems: 'center', flexShrink: 0 }}>
{actions.map((a, i) => (
<Button
key={i}
size="small"
variant={a.primary ? 'contained' : 'outlined'}
onClick={a.onClick}
sx={a.primary
? { bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none', fontSize: '0.75rem', py: 0.375, px: 1.25 }
: { textTransform: 'none', fontSize: '0.75rem', py: 0.375, px: 1.25 }
}
>
{a.label}
</Button>
))}
{hasDetails && (
<IconButton
size="small"
onClick={() => setExpanded(v => !v)}
sx={{ color: '#64748b', width: 24, height: 24 }}
>
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</IconButton>
)}
</Box>
</Box>
{/* Expandable details */}
<Collapse in={expanded}>
<Box sx={{ mt: 1.25, display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{risks.length > 0 && (
<Alert severity="warning" icon={<AlertTriangle size={14} />} sx={{ py: 0.25, '& .MuiAlert-message': { fontSize: '0.75rem' } }}>
<strong>Risiken:</strong>{' '}{risks.join(' · ')}
</Alert>
)}
{missing.length > 0 && (
<Alert severity="info" sx={{ py: 0.25, '& .MuiAlert-message': { fontSize: '0.75rem' } }}>
<strong>Fehlende Daten:</strong>{' '}{missing.join(' · ')}
</Alert>
)}
{children}
</Box>
</Collapse>
</Box>
)
}