feat: redesign Schattenmarkt card & detail view for explainability
- Remove abstract Konfidenz/DQ badges from match detail header - Replace ScoreBreakdownPanel with 3-step Bewertungsherleitung for FUTURE_AVAILABILITY: Kriterien-Match per factor with bars, Signal- Abschlag (verified ERP = kein Abschlag / probabilistic = -N Punkte), Gesamt-Score - Restructure FutureAvailabilityContextPanel: source section first, verified ERP box for LEASE_CONTRACT, remove Konfidenz row, disclaimer as grey footnote instead of yellow alert - Add LEASE_EXPIRY to signal type/relevance bridge labels - Show up to 3 match reasons with explanation on cards (was 1) - matchCardAdapter: return top-3 positive factors as reasons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Alert, Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||
import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||
import {
|
||||
BarChart2,
|
||||
Briefcase,
|
||||
@@ -20,6 +20,7 @@ const SIGNAL_TYPE_LABELS: Record<string, string> = {
|
||||
RESTRUCTURING: 'Restrukturierung',
|
||||
PROJECT_DEVELOPMENT: 'Projektentwicklung',
|
||||
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
|
||||
LEASE_EXPIRY: 'Vertragsende (Schattenmarkt)',
|
||||
}
|
||||
|
||||
const SIGNAL_RELEVANCE_BRIDGE: Record<string, string> = {
|
||||
@@ -29,6 +30,7 @@ const SIGNAL_RELEVANCE_BRIDGE: Record<string, string> = {
|
||||
RESTRUCTURING: 'Restrukturierung deutet auf Flächenänderungen hin.',
|
||||
PROJECT_DEVELOPMENT: 'Projektentwicklung könnte neue Gewerbeflächen schaffen.',
|
||||
SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten frei werden.',
|
||||
LEASE_EXPIRY: 'Die Verwaltung hat dieses Objekt für den Markt freigegeben — Vertragsende bekannt und bestätigt.',
|
||||
}
|
||||
|
||||
const SOURCE_META: Record<string, { label: string; icon: React.ReactNode }> = {
|
||||
@@ -38,6 +40,7 @@ const SOURCE_META: Record<string, { label: string; icon: React.ReactNode }> = {
|
||||
COMPANY_REPORT: { label: 'Geschäftsbericht', icon: <FileText size={13} /> },
|
||||
MARKET_DATA: { label: 'Marktdaten', icon: <BarChart2 size={13} /> },
|
||||
MANUAL: { label: 'Analyst', icon: <User size={13} /> },
|
||||
LEASE_CONTRACT: { label: 'Vertrag verifiziert (ERP)', icon: <ShieldCheck size={13} /> },
|
||||
}
|
||||
|
||||
const CREDIBILITY_META: Record<string, { label: string; color: string }> = {
|
||||
@@ -65,11 +68,11 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
const sensitiveMeta = SENSITIVITY_META[signal.sensitivityLevel] ?? SENSITIVITY_META.PUBLIC
|
||||
const relevanceBridge = signal.signalType ? (SIGNAL_RELEVANCE_BRIDGE[signal.signalType] ?? null) : null
|
||||
const probPct = Math.round(signal.probability * 100)
|
||||
const confPct = Math.round(signal.confidenceScore * 100)
|
||||
const isVerifiedContract = signal.isVerified && signal.source.type === 'LEASE_CONTRACT'
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.5, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Zap size={16} color="#7c3aed" />
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Schattenmarkt-Signal</Typography>
|
||||
{signal.isVerified && (
|
||||
@@ -80,23 +83,87 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Mandatory disclaimer */}
|
||||
<Alert severity="warning" sx={{ mb: 2, fontSize: '0.8rem' }}>
|
||||
{signal.disclaimer}
|
||||
</Alert>
|
||||
{/* 1. Quelle & Verlässlichkeit — FIRST */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>Quelle & Verlässlichkeit</Typography>
|
||||
|
||||
{/* Relevance bridge */}
|
||||
{relevanceBridge && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, bgcolor: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 1, px: 1.5, py: 1.25, mb: 2 }}>
|
||||
<Zap size={14} color="#7c3aed" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 700, color: '#4c1d95', mb: 0.25 }}>Warum erscheint dieses Signal in Ihrer Suche?</Typography>
|
||||
<Typography sx={{ fontSize: '0.8rem', color: '#4c1d95', lineHeight: 1.5 }}>{relevanceBridge}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap', mb: isVerifiedContract ? 1 : 0 }}>
|
||||
{sourceMeta && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, bgcolor: isVerifiedContract ? '#f0fdf4' : '#ede9fe', color: isVerifiedContract ? '#1a7a4a' : '#6d28d9', borderRadius: 1, px: 1, py: 0.5 }}>
|
||||
{sourceMeta.icon}
|
||||
<Typography sx={{ fontSize: '0.8rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{credMeta && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: credMeta.color }} />
|
||||
<Typography variant="body2" sx={{ color: credMeta.color, fontWeight: 600 }}>{credMeta.label}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{signal.source.publishedAt && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Publiziert: {new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Key metrics row */}
|
||||
{isVerifiedContract && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, bgcolor: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 1, px: 1.25, py: 1, mt: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<ShieldCheck size={13} color="#1a7a4a" />
|
||||
<Typography variant="caption" sx={{ color: '#1a7a4a', fontWeight: 600 }}>Vertragsende aus internem ERP bestätigt</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<ShieldCheck size={13} color="#1a7a4a" />
|
||||
<Typography variant="caption" sx={{ color: '#1a7a4a', fontWeight: 600 }}>Verwaltung hat Marktfreigabe erteilt</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{signal.source.url && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Button
|
||||
component="a"
|
||||
href={signal.source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<ExternalLink size={13} />}
|
||||
sx={{ textTransform: 'none', color: '#7c3aed', borderColor: '#c4b5fd', '&:hover': { borderColor: '#7c3aed', bgcolor: '#faf5ff' } }}
|
||||
>
|
||||
Originalquelle öffnen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{signal.evidence?.sourceUrls && signal.evidence.sourceUrls.length > 0 && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
Weitere Belege ({signal.evidence.sourceUrls.length})
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{signal.evidence.sourceUrls.map((url, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
component="a"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#7c3aed', fontSize: '0.75rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
|
||||
>
|
||||
<ExternalLink size={11} />
|
||||
<Typography variant="caption" sx={{ color: 'inherit' }}>{url}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{/* 2. Signal-Kenndaten */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5, mb: 2 }}>
|
||||
{signal.signalType && (
|
||||
<Box>
|
||||
@@ -108,10 +175,6 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Wahrscheinlichkeit</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: probPct >= 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b' }}>{probPct}%</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Konfidenz</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>{confPct}%</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Zeithorizont</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.timeHorizonMonths} Monate</Typography>
|
||||
@@ -122,15 +185,11 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Vertraulichkeit</Typography>
|
||||
<Chip label={sensitiveMeta.label} size="small" color={sensitiveMeta.color} variant="outlined" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{/* AI Summary */}
|
||||
{/* 3. KI-Analyse */}
|
||||
{signal.aiSummary && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
|
||||
@@ -145,7 +204,18 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Evidence */}
|
||||
{/* 4. Relevance bridge */}
|
||||
{relevanceBridge && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, bgcolor: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 1, px: 1.5, py: 1.25, mb: 2 }}>
|
||||
<Zap size={14} color="#7c3aed" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 700, color: '#4c1d95', mb: 0.25 }}>Warum erscheint dieses Signal in Ihrer Suche?</Typography>
|
||||
<Typography sx={{ fontSize: '0.8rem', color: '#4c1d95', lineHeight: 1.5 }}>{relevanceBridge}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 5. Evidence */}
|
||||
{signal.evidence?.summary && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.75 }}>Evidenz</Typography>
|
||||
@@ -155,66 +225,16 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{/* Source section */}
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Quelle</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
|
||||
{sourceMeta && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 1, py: 0.5 }}>
|
||||
{sourceMeta.icon}
|
||||
<Typography sx={{ fontSize: '0.8rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{credMeta && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: credMeta.color }} />
|
||||
<Typography variant="body2" sx={{ color: credMeta.color, fontWeight: 600 }}>{credMeta.label}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{signal.source.publishedAt && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Publiziert: {new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Typography>
|
||||
)}
|
||||
{signal.source.url && (
|
||||
<Button
|
||||
component="a"
|
||||
href={signal.source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<ExternalLink size={13} />}
|
||||
sx={{ textTransform: 'none', color: '#7c3aed', borderColor: '#c4b5fd', '&:hover': { borderColor: '#7c3aed', bgcolor: '#faf5ff' } }}
|
||||
>
|
||||
Originalquelle öffnen
|
||||
</Button>
|
||||
)}
|
||||
{/* 6. Vertraulichkeit */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">Vertraulichkeit:</Typography>
|
||||
<Chip label={sensitiveMeta.label} size="small" color={sensitiveMeta.color} variant="outlined" />
|
||||
</Box>
|
||||
|
||||
{signal.evidence?.sourceUrls && signal.evidence.sourceUrls.length > 0 && (
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
Weitere Belege ({signal.evidence.sourceUrls.length})
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{signal.evidence.sourceUrls.map((url, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
component="a"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#7c3aed', fontSize: '0.75rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
|
||||
>
|
||||
<ExternalLink size={11} />
|
||||
<Typography variant="caption" sx={{ color: 'inherit' }}>{url}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{/* 7. Disclaimer — grey footnote */}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.5, borderTop: '1px solid #f1f5f9', pt: 1.5 }}>
|
||||
{signal.disclaimer}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user