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 {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
ExternalLink,
|
|
||||||
Globe,
|
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -14,7 +12,8 @@ import {
|
|||||||
import type { Match } from '../../domain/match'
|
import type { Match } from '../../domain/match'
|
||||||
import type { FutureSignal } from '../../domain/futureSignal'
|
import type { FutureSignal } from '../../domain/futureSignal'
|
||||||
import { SIGNAL_TYPE_LABELS, SIGNAL_ACTION, SOURCE_META } from './futureAvailabilityConstants'
|
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 {
|
interface Props {
|
||||||
match: Match
|
match: Match
|
||||||
@@ -197,100 +196,13 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
|||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
{/* ── 7. Quellen & Belege ───────────────────────────────────────────── */}
|
{/* ── 7. Quellen & Belege ───────────────────────────────────────────── */}
|
||||||
<Box sx={{ mb: 2.5 }}>
|
<SignalSourcesSection
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1.25 }}>
|
sourceMeta={sourceMeta}
|
||||||
<Globe size={14} color="#0369a1" />
|
credMeta={credMeta}
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>Quellen & Belege</Typography>
|
isVerifiedContract={isVerifiedContract}
|
||||||
</Box>
|
allSourceUrls={allSourceUrls}
|
||||||
|
signal={signal}
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* ── Footer ──────────────────────────────────────────────────────────── */}
|
{/* ── Footer ──────────────────────────────────────────────────────────── */}
|
||||||
<Divider sx={{ mb: 1.5 }} />
|
<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