Files
property-match/src/components/data-quality/ProvenancePanel.tsx
T
Benjamin Sutter da50f3b5ea feat: premium redesign — DM Serif, refined palette, flat score badges
- 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>
2026-06-09 22:26:30 +02:00

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>
)
}