9e7a09f8a2
- 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>
159 lines
5.0 KiB
TypeScript
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>
|
|
)
|
|
}
|