da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import { Box, Button, Chip, LinearProgress, Typography } from '@mui/material'
|
|
import { ExternalLink, Shield, ShieldAlert } from 'lucide-react'
|
|
import { FreshnessIndicator } from './FreshnessIndicator'
|
|
import type { Property } from '../../domain/property'
|
|
|
|
interface ProvenancePanelProps {
|
|
property: Property
|
|
}
|
|
|
|
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
|
ERP_IMPORT: 'ERP-Import',
|
|
MANUAL_ENTRY: 'Manuelle Eingabe',
|
|
IMMOSCOUT_SCRAPE: 'ImmoScout24',
|
|
HOMEGATE_SCRAPE: 'Homegate',
|
|
NEWHOME_SCRAPE: 'Newhome',
|
|
MATCHOFFICE_SCRAPE: 'MatchOffice',
|
|
MAISON_WORK_SCRAPE: 'Maison & Work',
|
|
AI_SIGNAL: 'KI-Signal',
|
|
PARTNER_FEED: 'Partner-Feed',
|
|
UNKNOWN: 'Unbekannt',
|
|
}
|
|
|
|
const SOURCE_CONFIDENCE: Record<string, number> = {
|
|
ERP_IMPORT: 0.95,
|
|
MANUAL_ENTRY: 0.90,
|
|
PARTNER_FEED: 0.80,
|
|
IMMOSCOUT_SCRAPE: 0.65,
|
|
HOMEGATE_SCRAPE: 0.65,
|
|
NEWHOME_SCRAPE: 0.60,
|
|
MATCHOFFICE_SCRAPE: 0.60,
|
|
MAISON_WORK_SCRAPE: 0.60,
|
|
AI_SIGNAL: 0.40,
|
|
UNKNOWN: 0.30,
|
|
}
|
|
|
|
function getSourceConfidence(sourceType: string): number {
|
|
return SOURCE_CONFIDENCE[sourceType] ?? 0.50
|
|
}
|
|
|
|
function provenanceColor(conf: number): string {
|
|
if (conf >= 0.8) return '#1a7a4a'
|
|
if (conf >= 0.6) return '#d97706'
|
|
return '#c0392b'
|
|
}
|
|
|
|
export function ProvenancePanel({ property: p }: ProvenancePanelProps) {
|
|
const sourceLabel = SOURCE_TYPE_LABELS[p.sourceType] ?? p.sourceType
|
|
const sourceConf = getSourceConfidence(p.sourceType)
|
|
const confPct = Math.round(sourceConf * 100)
|
|
const color = provenanceColor(sourceConf)
|
|
const isVerified = sourceConf >= 0.85
|
|
const VerifyIcon = isVerified ? Shield : ShieldAlert
|
|
|
|
return (
|
|
<Box>
|
|
{/* Source header */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
|
<VerifyIcon size={16} color={color} />
|
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>{sourceLabel}</Typography>
|
|
{p.sourceLabel && p.sourceLabel !== sourceLabel && (
|
|
<Typography variant="caption" color="text.secondary">· {p.sourceLabel}</Typography>
|
|
)}
|
|
<Chip
|
|
size="small"
|
|
label={isVerified ? 'Verifiziert' : 'Ungeprüft'}
|
|
sx={{ bgcolor: `${color}18`, color, fontSize: '0.65rem', height: 18, ml: 'auto' }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Source confidence bar */}
|
|
<Box sx={{ mb: 1.5 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
<Typography variant="caption" color="text.secondary">Quell-Vertrauen</Typography>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color }}>{confPct}%</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={confPct}
|
|
sx={{
|
|
height: 5,
|
|
borderRadius: 3,
|
|
bgcolor: '#e8e7e4',
|
|
'& .MuiLinearProgress-bar': { bgcolor: color },
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Dates */}
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5, mb: 1.5 }}>
|
|
<Box>
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Quellaktualisierung</Typography>
|
|
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '0.8125rem' }}>
|
|
{p.sourceUpdatedAt ? new Date(p.sourceUpdatedAt).toLocaleDateString('de-CH') : '—'}
|
|
</Typography>
|
|
</Box>
|
|
<Box>
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Letzte Verifikation</Typography>
|
|
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '0.8125rem' }}>
|
|
{p.dataQuality.lastVerifiedAt ? new Date(p.dataQuality.lastVerifiedAt).toLocaleDateString('de-CH') : '—'}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Freshness */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
|
<Typography variant="caption" color="text.secondary">Aktualität:</Typography>
|
|
<FreshnessIndicator freshness={p.dataQuality.freshness} lastUpdated={p.sourceUpdatedAt} />
|
|
</Box>
|
|
|
|
{/* External URL */}
|
|
{p.sourceUrl && (
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
endIcon={<ExternalLink size={12} />}
|
|
href={p.sourceUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
|
>
|
|
Originalquelle öffnen
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|