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>
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import { Box, LinearProgress, Typography } from '@mui/material'
|
|
import { dataQualityHex } from '../../lib/utils'
|
|
import { FreshnessStatus } from '../../domain/enums'
|
|
import type { Property } from '../../domain/property'
|
|
|
|
interface Dimension {
|
|
label: string
|
|
score: number
|
|
color: string
|
|
}
|
|
|
|
function freshnessScore(f: string): number {
|
|
if (f === FreshnessStatus.FRESH) return 100
|
|
if (f === FreshnessStatus.STALE) return 50
|
|
return 15
|
|
}
|
|
|
|
function completenessScore(missingCritical: number, missingOptional: number): number {
|
|
const critPenalty = missingCritical * 15
|
|
const optPenalty = missingOptional * 5
|
|
return Math.max(0, 100 - critPenalty - optPenalty)
|
|
}
|
|
|
|
const SOURCE_PROVENANCE: Record<string, number> = {
|
|
ERP_IMPORT: 95, MANUAL_ENTRY: 90, PARTNER_FEED: 80,
|
|
IMMOSCOUT_SCRAPE: 65, HOMEGATE_SCRAPE: 65, NEWHOME_SCRAPE: 60,
|
|
MATCHOFFICE_SCRAPE: 60, MAISON_WORK_SCRAPE: 60, AI_SIGNAL: 40, UNKNOWN: 30,
|
|
}
|
|
|
|
function dimColor(score: number): string {
|
|
if (score >= 80) return '#1a7a4a'
|
|
if (score >= 55) return '#d97706'
|
|
return '#c0392b'
|
|
}
|
|
|
|
function buildDimensions(p: Property): Dimension[] {
|
|
const compScore = completenessScore(
|
|
p.dataQuality.missingCriticalFields.length,
|
|
p.dataQuality.missingOptionalFields.length,
|
|
)
|
|
const freshScore = freshnessScore(p.dataQuality.freshness)
|
|
const confScore = Math.round(p.confidenceScore * 100)
|
|
const provScore = SOURCE_PROVENANCE[p.sourceType] ?? 50
|
|
const lastVerified = p.dataQuality.lastVerifiedAt
|
|
const verScore = lastVerified
|
|
? Math.max(10, 100 - Math.floor((Date.now() - new Date(lastVerified).getTime()) / (1000 * 60 * 60 * 24)) * 2)
|
|
: 10
|
|
|
|
return [
|
|
{ label: 'Vollständigkeit', score: compScore, color: dimColor(compScore) },
|
|
{ label: 'Aktualität', score: freshScore, color: dimColor(freshScore) },
|
|
{ label: 'Vertrauensscore', score: confScore, color: dimColor(confScore) },
|
|
{ label: 'Herkunft', score: Math.min(100, provScore), color: dimColor(provScore) },
|
|
{ label: 'Verifikation', score: Math.min(100, verScore), color: dimColor(verScore) },
|
|
]
|
|
}
|
|
|
|
interface DataQualityProgressProps {
|
|
property: Property
|
|
compact?: boolean
|
|
}
|
|
|
|
export function DataQualityProgress({ property, compact = false }: DataQualityProgressProps) {
|
|
const dims = buildDimensions(property)
|
|
const overallPct = Math.round(property.dataQuality.score * 100)
|
|
const hex = dataQualityHex(property.dataQuality.score)
|
|
|
|
if (compact) {
|
|
return (
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|
{dims.map(d => (
|
|
<Box key={d.label} sx={{ flex: 1 }}>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={d.score}
|
|
sx={{
|
|
height: 4,
|
|
borderRadius: 2,
|
|
bgcolor: '#e8e7e4',
|
|
'& .MuiLinearProgress-bar': { bgcolor: d.color },
|
|
}}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
{/* Overall score header */}
|
|
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1, mb: 1.5 }}>
|
|
<Typography variant="h4" sx={{ fontWeight: 800, color: hex, lineHeight: 1 }}>
|
|
{overallPct}%
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">Gesamtqualität</Typography>
|
|
</Box>
|
|
|
|
{/* Dimension bars */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
|
{dims.map(d => (
|
|
<Box key={d.label}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>{d.label}</Typography>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color: d.color, fontSize: '0.7rem' }}>{d.score}%</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={d.score}
|
|
sx={{
|
|
height: 5,
|
|
borderRadius: 3,
|
|
bgcolor: '#e8e7e4',
|
|
'& .MuiLinearProgress-bar': { bgcolor: d.color },
|
|
}}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|