refactor: extract SignalSourcesSection from FutureAvailabilityContextPanel (314 → 227 lines)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 & 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 & 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user