refactor: split large page components + taxonomy/HeatBadge/FutureAvailability improvements

- Taxonomy: merge VERIFIED_PORTFOLIO + EXTERNAL_MARKET display → 'Plattform' (dark blue) across all surfaces
- HeatBadge: new flame indicator for hot properties (grid, list, pipeline views)
- FutureAvailabilityContextPanel: richer detail page with AI summary, strategic assessment, sources
- Refactor Pipeline.tsx (630→152 lines) → pipeline/PipelineCard, PipelineColumn, PipelineDetailPanel, pipelineConstants, pipelineUtils
- Refactor IntelligenceMatchCard.tsx (483→179 lines) → FutureAvailabilityCard extracted
- Refactor MatchDetail.tsx (559→464 lines) → useMatchDetailData hook, MatchDetailPropertyDetails
- Refactor Compare.tsx (638→485 lines) → compareUtils, CompareCriteriaCard extracted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-23 23:36:03 +02:00
parent 72e4f08900
commit 22c195b4a5
28 changed files with 1615 additions and 1282 deletions
@@ -1,14 +1,19 @@
import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
import { Box, Button, Chip, Divider, LinearProgress, Paper, Typography } from '@mui/material'
import {
AlertTriangle,
BarChart2,
Briefcase,
Calendar,
CheckCircle2,
Clock,
ExternalLink,
FileCheck,
FileText,
Globe,
Newspaper,
ShieldCheck,
Sparkles,
TrendingUp,
User,
Zap,
} from 'lucide-react'
@@ -25,14 +30,14 @@ const SIGNAL_TYPE_LABELS: Record<string, string> = {
LEASE_EXPIRY: 'Vertragsende (Pre-Market)',
}
const SIGNAL_RELEVANCE_BRIDGE: Record<string, string> = {
EXPANSION: 'Ein Unternehmen sucht neue Flächen — Ihr Angebot könnte gefragt sein.',
POSSIBLE_MOVE_OUT: 'Dieses Unternehmen könnte seinen Standort aufgeben — die Fläche wird für Sie verfügbar.',
CONSTRUCTION_PROJECT:'Ein Neubau entsteht — neue Flächen könnten zur Vermietung angeboten werden.',
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 diese Fläche für kontrolliertes Pre-Market Matching freigegeben — Vertragsende aus internem ERP bestätigt.',
const SIGNAL_ACTION: Record<string, { label: string; urgency: 'high' | 'medium' | 'low' }> = {
EXPANSION: { label: 'Unternehmen proaktiv kontaktieren — aktive Flächensuche wahrscheinlich', urgency: 'high' },
POSSIBLE_MOVE_OUT: { label: 'Mieter ansprechen und Verlängerungsgespräch initiieren', urgency: 'high' },
CONSTRUCTION_PROJECT: { label: 'Frühzeitiges Interesse beim Bauherrn anmelden, bevor Vermietungsmandat vergeben', urgency: 'medium' },
RESTRUCTURING: { label: 'Situation beobachten, bei Bestätigung sofort handeln', urgency: 'medium' },
PROJECT_DEVELOPMENT: { label: 'Entwicklungsfortschritt monitoren und Kontakt zum Projektentwickler suchen', urgency: 'medium' },
SPACE_CONSOLIDATION: { label: 'Teilflächen-Anforderungen klären, Gespräch mit Verwaltung suchen', urgency: 'medium' },
LEASE_EXPIRY: { label: 'Anfrage direkt über die Verwaltung stellen — Fläche ist für Matching freigegeben', urgency: 'high' },
}
const SOURCE_META: Record<string, { label: string; icon: React.ReactNode }> = {
@@ -57,6 +62,32 @@ const SENSITIVITY_META: Record<string, { label: string; color: 'error' | 'warnin
PUBLIC: { label: 'Öffentlich', color: 'default' },
}
const RISK_META: Record<string, { label: string; color: string }> = {
LOW: { label: 'Niedrig', color: '#1a7a4a' },
MEDIUM: { label: 'Mittel', color: '#d97706' },
HIGH: { label: 'Hoch', color: '#c0392b' },
CRITICAL: { label: 'Kritisch', color: '#7f1d1d' },
}
function ageLabel(isoDate: string): string {
const diffMs = Date.now() - new Date(isoDate).getTime()
const days = Math.floor(diffMs / 86400000)
if (days < 1) return 'Heute erkannt'
if (days < 7) return `Vor ${days} Tag${days === 1 ? '' : 'en'} erkannt`
if (days < 30) return `Vor ${Math.floor(days / 7)} Woche${Math.floor(days / 7) === 1 ? '' : 'n'} erkannt`
if (days < 365) return `Vor ${Math.floor(days / 30)} Monat${Math.floor(days / 30) === 1 ? '' : 'en'} erkannt`
return `Vor ${Math.floor(days / 365)} Jahr${Math.floor(days / 365) === 1 ? '' : 'en'} erkannt`
}
function domainLabel(url: string): string {
try {
const u = new URL(url)
return u.hostname.replace(/^www\./, '')
} catch {
return url.length > 60 ? url.slice(0, 57) + '…' : url
}
}
interface Props {
match: Match
signal: FutureSignal | null
@@ -66,15 +97,24 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
if (!signal) return null
const sourceMeta = SOURCE_META[signal.source.type] ?? null
const credMeta = CREDIBILITY_META[signal.source.credibility] ?? null
const credMeta = CREDIBILITY_META[signal.source.credibility] ?? null
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 riskMeta = signal.riskLevel ? (RISK_META[signal.riskLevel] ?? null) : null
const probPct = Math.round(signal.probability * 100)
const isVerifiedContract = signal.isVerified && signal.source.type === 'LEASE_CONTRACT'
const action = signal.signalType ? (SIGNAL_ACTION[signal.signalType] ?? null) : null
const allSourceUrls = [
...(signal.source.url ? [signal.source.url] : []),
...(signal.evidence?.sourceUrls ?? []),
]
const probBarColor = probPct >= 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b'
return (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
{/* ── Header ──────────────────────────────────────────────────────────── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.75, flexWrap: 'wrap' }}>
<Zap size={16} color="#7c3aed" />
<Typography variant="h6" sx={{ fontWeight: 700 }}>Future Availability Signal</Typography>
{signal.isVerified && (
@@ -83,34 +123,185 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
<Typography sx={{ fontSize: '0.68rem', color: '#1a7a4a', fontWeight: 700 }}>Verifiziert</Typography>
</Box>
)}
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 0.5, color: '#94a3b8' }}>
<Clock size={12} />
<Typography variant="caption" color="text.secondary">{ageLabel(signal.createdAt)}</Typography>
</Box>
</Box>
{/* 1. Quelle & Verlässlichkeit — FIRST */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>Quelle &amp; Verlässlichkeit</Typography>
{signal.title && (
<Typography sx={{ fontSize: '0.95rem', fontWeight: 600, color: '#1e293b', mb: 2, lineHeight: 1.4 }}>
{signal.title}
</Typography>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap', mb: isVerifiedContract ? 1 : 0 }}>
{/* ── 1. KI-Zusammenfassung ─────────────────────────────────────────── */}
{signal.aiSummary && (
<Box sx={{ mb: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.875 }}>
<Sparkles size={14} color="#7c3aed" />
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: '#4c1d95', fontSize: '0.8125rem' }}>KI-Zusammenfassung</Typography>
</Box>
<Box sx={{ bgcolor: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 1.5, p: 1.75 }}>
<Typography variant="body2" sx={{ color: '#1e1b4b', lineHeight: 1.7 }}>
{signal.aiSummary}
</Typography>
</Box>
</Box>
)}
{/* ── 2. Strategische Einschätzung ─────────────────────────────────── */}
{signal.strategicInterpretation && (
<Box sx={{ mb: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.875 }}>
<TrendingUp size={14} color="#0369a1" />
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>Strategische Einschätzung</Typography>
</Box>
<Box sx={{ bgcolor: '#f0f9ff', border: '1px solid #bae6fd', borderRadius: 1.5, p: 1.75 }}>
<Typography variant="body2" sx={{ color: '#0c4a6e', lineHeight: 1.7 }}>
{signal.strategicInterpretation}
</Typography>
</Box>
</Box>
)}
{/* ── 3. Handlungsempfehlung ───────────────────────────────────────── */}
{action && (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, bgcolor: action.urgency === 'high' ? '#fff7ed' : '#f8fafc', border: `1px solid ${action.urgency === 'high' ? '#fed7aa' : '#e2e8f0'}`, borderRadius: 1.5, px: 1.5, py: 1.25, mb: 2.5 }}>
<Zap size={14} color={action.urgency === 'high' ? '#c2410c' : '#64748b'} style={{ flexShrink: 0, marginTop: 2 }} />
<Box>
<Typography sx={{ fontSize: '0.72rem', fontWeight: 700, color: action.urgency === 'high' ? '#9a3412' : '#475569', mb: 0.25 }}>Empfohlene Aktion</Typography>
<Typography sx={{ fontSize: '0.82rem', color: action.urgency === 'high' ? '#7c2d12' : '#334155', lineHeight: 1.5 }}>{action.label}</Typography>
</Box>
</Box>
)}
<Divider sx={{ mb: 2 }} />
{/* ── 4. Signal-Kenndaten ──────────────────────────────────────────── */}
<Box sx={{ mb: 2.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1.25, fontSize: '0.8125rem' }}>Signal-Kenndaten</Typography>
{/* Probability bar */}
<Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.4 }}>
<Typography variant="caption" color="text.secondary">Eintretenswahrscheinlichkeit</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: probBarColor }}>{probPct}%</Typography>
</Box>
<LinearProgress
variant="determinate"
value={probPct}
sx={{
height: 6, borderRadius: 3, bgcolor: '#f1f5f9',
'& .MuiLinearProgress-bar': { bgcolor: probBarColor, borderRadius: 3 },
}}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5 }}>
{signal.signalType && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Signaltyp</Typography>
<Chip label={SIGNAL_TYPE_LABELS[signal.signalType] ?? signal.signalType} size="small" sx={{ bgcolor: '#ede9fe', color: '#6d28d9', fontWeight: 600 }} />
</Box>
)}
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Zeithorizont</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Calendar size={13} color="#64748b" />
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.timeHorizonMonths} Monate</Typography>
</Box>
</Box>
{signal.areaSqmEstimate && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Flächenschätzung</Typography>
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²</Typography>
</Box>
)}
{riskMeta && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Risikoniveau</Typography>
<Typography variant="body2" sx={{ fontWeight: 700, color: riskMeta.color }}>{riskMeta.label}</Typography>
</Box>
)}
</Box>
</Box>
<Divider sx={{ mb: 2 }} />
{/* ── 5. Erkannte Marktindikatoren ──────────────────────────────────── */}
{signal.marketIndicators && signal.marketIndicators.length > 0 && (
<Box sx={{ mb: 2.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>Erkannte Marktindikatoren</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{signal.marketIndicators.map((indicator, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.875 }}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '6px', flexShrink: 0 }} />
<Typography variant="body2" sx={{ color: '#334155', lineHeight: 1.55 }}>{indicator}</Typography>
</Box>
))}
</Box>
</Box>
)}
{/* ── 6. Transparenz (Bestätigt / Nicht bestätigt) ─────────────────── */}
{((signal.confirmedFacts && signal.confirmedFacts.length > 0) ||
(signal.unconfirmedFacts && signal.unconfirmedFacts.length > 0)) && (
<Box sx={{ mb: 2.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>Transparenz Was ist gesichert?</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
{(signal.confirmedFacts ?? []).map((fact, i) => (
<Box key={`c-${i}`} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
<CheckCircle2 size={14} color="#1a7a4a" style={{ flexShrink: 0, marginTop: 1 }} />
<Typography variant="body2" sx={{ color: '#1a7a4a', fontWeight: 500, lineHeight: 1.45 }}>{fact}</Typography>
</Box>
))}
{(signal.unconfirmedFacts ?? []).map((fact, i) => (
<Box key={`u-${i}`} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
<AlertTriangle size={14} color="#d97706" style={{ flexShrink: 0, marginTop: 1 }} />
<Typography variant="body2" sx={{ color: '#92400e', lineHeight: 1.45 }}>{fact}</Typography>
</Box>
))}
</Box>
</Box>
)}
<Divider sx={{ mb: 2 }} />
{/* ── 7. Quellen & Belege ───────────────────────────────────────────── */}
<Box sx={{ mb: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1.25 }}>
<Globe size={14} color="#0369a1" />
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>Quellen &amp; Belege</Typography>
</Box>
{/* Primary source metadata — always shown */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1.25 }}>
{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>
<Typography sx={{ fontSize: '0.78rem', 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 sx={{ display: 'flex', alignItems: 'center', gap: 0.5, bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, px: 0.875, py: 0.4 }}>
<Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: credMeta.color }} />
<Typography sx={{ fontSize: '0.75rem', 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 sx={{ display: 'flex', alignItems: 'center', gap: 0.5, bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, px: 0.875, py: 0.4 }}>
<Calendar size={11} color="#64748b" />
<Typography variant="caption" color="text.secondary">
{new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })}
</Typography>
</Box>
)}
</Box>
{/* Verified contract special box */}
{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', flexDirection: 'column', gap: 0.5, bgcolor: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 1, px: 1.25, py: 1, mb: 1.25 }}>
<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>
@@ -122,176 +313,72 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
</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>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Signaltyp</Typography>
<Chip label={SIGNAL_TYPE_LABELS[signal.signalType] ?? signal.signalType} size="small" sx={{ bgcolor: '#ede9fe', color: '#6d28d9', fontWeight: 600 }} />
</Box>
)}
<Box>
<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 }}>Zeithorizont</Typography>
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.timeHorizonMonths} Monate</Typography>
</Box>
{signal.areaSqmEstimate && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>Flächenschätzung</Typography>
<Typography variant="body2" sx={{ fontWeight: 700 }}>~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²</Typography>
</Box>
)}
</Box>
<Divider sx={{ mb: 2 }} />
{/* 3. KI-Analyse */}
{signal.aiSummary && (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<Zap size={14} color="#7c3aed" />
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: '#4c1d95' }}>KI-Analyse</Typography>
</Box>
<Box sx={{ bgcolor: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 1, p: 1.5 }}>
<Typography variant="body2" sx={{ color: '#1e1b4b', lineHeight: 1.65 }}>
{signal.aiSummary}
</Typography>
</Box>
</Box>
)}
<Divider sx={{ mb: 2 }} />
{/* 4. Erkannte Marktindikatoren */}
{signal.marketIndicators && signal.marketIndicators.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>
Erkannte Marktindikatoren
</Typography>
{/* All source URLs */}
{allSourceUrls.length > 0 ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
{signal.marketIndicators.map((indicator, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '5px', flexShrink: 0 }} />
<Typography variant="body2" sx={{ color: '#374151', lineHeight: 1.45 }}>{indicator}</Typography>
</Box>
{allSourceUrls.map((url, i) => (
<Button
key={i}
component="a"
href={url}
target="_blank"
rel="noopener noreferrer"
size="small"
variant="outlined"
startIcon={<ExternalLink size={12} />}
sx={{
textTransform: 'none', justifyContent: 'flex-start',
color: '#0369a1', borderColor: '#bae6fd',
'&:hover': { borderColor: '#0369a1', bgcolor: '#f0f9ff' },
fontSize: '0.78rem', fontWeight: 500,
}}
>
{domainLabel(url)}
</Button>
))}
</Box>
</Box>
)}
{/* 5. Strategische Interpretation */}
{signal.strategicInterpretation && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.75, fontSize: '0.8125rem' }}>
Strategische Interpretation
</Typography>
<Box sx={{ bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, p: 1.5 }}>
<Typography variant="body2" sx={{ color: '#374151', lineHeight: 1.65, fontStyle: 'italic' }}>
{signal.strategicInterpretation}
) : (
<Box sx={{ bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, px: 1.25, py: 0.875 }}>
<Typography variant="caption" color="text.secondary">
Kein direkter Quellenlink verfügbar Signal basiert auf aggregierten {sourceMeta?.label ?? 'Marktdaten'}.
</Typography>
</Box>
</Box>
)}
)}
{/* 6. Transparenz (Bestätigt / Nicht bestätigt) */}
{((signal.confirmedFacts && signal.confirmedFacts.length > 0) ||
(signal.unconfirmedFacts && signal.unconfirmedFacts.length > 0)) && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>
Transparenz
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{(signal.confirmedFacts ?? []).map((fact, i) => (
<Box key={`c-${i}`} sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<CheckCircle2 size={13} color="#1a7a4a" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#1a7a4a', fontWeight: 500 }}>{fact}</Typography>
</Box>
))}
{(signal.unconfirmedFacts ?? []).map((fact, i) => (
<Box key={`u-${i}`} sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<AlertTriangle size={13} color="#d97706" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#d97706', fontWeight: 500 }}>{fact}</Typography>
</Box>
))}
{/* Evidence extraction date */}
{signal.evidence?.extractedAt && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.875 }}>
<Clock size={11} color="#94a3b8" />
<Typography variant="caption" color="text.secondary">
Daten extrahiert: {new Date(signal.evidence.extractedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'short', year: 'numeric' })}
</Typography>
</Box>
</Box>
)}
)}
{/* 7. 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>
{/* Evidence summary */}
{signal.evidence?.summary && (
<Box sx={{ bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, p: 1.25, mt: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', display: 'block', mb: 0.3 }}>Evidenz-Zusammenfassung</Typography>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.55 }}>{signal.evidence.summary}</Typography>
</Box>
</Box>
)}
)}
</Box>
{/* 5. Evidence */}
{signal.evidence?.summary && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.75 }}>Evidenz</Typography>
<Box sx={{ bgcolor: '#f8fafc', borderRadius: 1, p: 1.5 }}>
<Typography variant="body2" color="text.secondary">{signal.evidence.summary}</Typography>
</Box>
</Box>
)}
{/* ── Footer ──────────────────────────────────────────────────────────── */}
<Divider sx={{ mb: 1.5 }} />
{/* 6. Vertraulichkeit */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.25, flexWrap: 'wrap' }}>
<Typography variant="caption" color="text.secondary">Vertraulichkeit:</Typography>
<Chip label={sensitiveMeta.label} size="small" color={sensitiveMeta.color} variant="outlined" />
{signal.verifiedBy && (
<>
<Typography variant="caption" color="text.secondary">Verifiziert von:</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1a7a4a' }}>{signal.verifiedBy}</Typography>
</>
)}
</Box>
{/* 7. Disclaimer — grey footnote */}
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.5, borderTop: '1px solid #f1f5f9', pt: 1.5 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.55, bgcolor: '#f8fafc', borderRadius: 1, px: 1.25, py: 1, fontStyle: 'italic' }}>
{signal.disclaimer}
</Typography>
</Paper>