refactor: extract SignalSourcesSection from FutureAvailabilityContextPanel (314 → 227 lines)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 01:18:49 +02:00
parent 01c0f8b06f
commit eedee83b49
2 changed files with 127 additions and 98 deletions
@@ -1,11 +1,9 @@
import { Box, Button, Chip, Divider, LinearProgress, Paper, Typography } from '@mui/material'
import { Box, Chip, Divider, LinearProgress, Paper, Typography } from '@mui/material'
import {
AlertTriangle,
Calendar,
CheckCircle2,
Clock,
ExternalLink,
Globe,
ShieldCheck,
Sparkles,
TrendingUp,
@@ -14,7 +12,8 @@ import {
import type { Match } from '../../domain/match'
import type { FutureSignal } from '../../domain/futureSignal'
import { SIGNAL_TYPE_LABELS, SIGNAL_ACTION, SOURCE_META } from './futureAvailabilityConstants'
import { CREDIBILITY_META, SENSITIVITY_META, RISK_META, ageLabel, domainLabel } from './futureAvailabilityContextUtils'
import { CREDIBILITY_META, SENSITIVITY_META, RISK_META, ageLabel } from './futureAvailabilityContextUtils'
import { SignalSourcesSection } from './SignalSourcesSection'
interface Props {
match: Match
@@ -197,100 +196,13 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
<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.78rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
</Box>
)}
{credMeta && (
<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 && (
<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, 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>
</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>
)}
{/* All source URLs */}
{allSourceUrls.length > 0 ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
{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 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>
)}
{/* 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>
)}
{/* 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>
<SignalSourcesSection
sourceMeta={sourceMeta}
credMeta={credMeta}
isVerifiedContract={isVerifiedContract}
allSourceUrls={allSourceUrls}
signal={signal}
/>
{/* ── Footer ──────────────────────────────────────────────────────────── */}
<Divider sx={{ mb: 1.5 }} />
@@ -0,0 +1,117 @@
import { Box, Button, Typography } from '@mui/material'
import { Calendar, Clock, ExternalLink, Globe, ShieldCheck } from 'lucide-react'
import type { FutureSignal } from '../../domain/futureSignal'
import { domainLabel } from './futureAvailabilityContextUtils'
interface SignalSourcesSectionProps {
sourceMeta: { label: string; icon: React.ReactNode; badge?: string; description?: string } | null
credMeta: { label: string; color: string } | null
isVerifiedContract: boolean
allSourceUrls: string[]
signal: FutureSignal
}
export function SignalSourcesSection({
sourceMeta,
credMeta,
isVerifiedContract,
allSourceUrls,
signal,
}: SignalSourcesSectionProps) {
return (
<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.78rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
</Box>
)}
{credMeta && (
<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 && (
<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, 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>
</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>
)}
{/* All source URLs */}
{allSourceUrls.length > 0 ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
{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 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>
)}
{/* 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>
)}
{/* 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>
)
}