Files
property-match/src/components/match-card/ScoreInlineBreakdown.tsx
T
Benjamin Sutter 86bdf142b4 refactor: consolidate duplicated UI score/color/badge logic into lib/
- Add matchScoreHex(), criterionScoreColor(), criterionScoreTextColor() to lib/utils.ts
- Add RESULT_TYPE_META (labels + colors) to lib/ds.ts as single source of truth
- Remove 5 local scoreColor() functions: StrongMatchMiniCard, MatchListCard,
  pipelineUtils (re-exported as matchScoreHex), CriterionRow, ScoreBreakdownPanel,
  ScoreInlineBreakdown
- Remove local RESULT_TYPE_META/TYPE_META from MatchCardHeader, ResultTypeBadge;
  remove RESULT_TYPE_LABEL/COLOR from pipelineConstants — all now use lib/ds.ts
- Replace local confidenceColor() in MatchCardHeader, ConfidenceFieldBadge,
  ReliabilityScorePanel with confidenceHex() from lib/utils.ts
- Replace local qualityColor() in propertyHelpers, DataQualityWidget with
  dataQualityColor() from lib/utils.ts
- Fix FUTURE_AVAILABILITY label inconsistency ('Future'/'Zukunft' → 'Zukunftssignal')
- Fix EXTERNAL_MARKET label inconsistency ('Direktinserat' → 'Plattform')
- tsc --noEmit passes with zero errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 01:47:08 +02:00

128 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Box, Divider, LinearProgress, Typography } from '@mui/material'
import type { ScoreFactor } from '../../domain/match'
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
interface ScoreBreakdownData {
hardMatchScore: number
softFactorScore: number
totalScore: number
}
interface Props {
scoreBreakdown: ScoreBreakdownData
allFactors?: ScoreFactor[]
compact?: boolean
}
const CRITERION_LABEL: Record<string, string> = {
area: 'Fläche', location: 'Standort', budget: 'Budget', timing: 'Verfügbarkeit',
prestige: 'Prestige', accessibility: 'Erreichbarkeit', expansionPotential: 'Expansion',
flexibility: 'Flexibilität', visibility: 'Sichtbarkeit', footfall: 'Passantenfrequenz',
talentAccess: 'Talent-Zugang', esg: 'ESG', taxEnvironment: 'Steuerumfeld',
}
const HARD_KEYS = new Set(['area', 'location', 'budget', 'timing'])
function importanceLabel(weight: number, maxWeight: number): { label: string; color: string } {
const ratio = maxWeight > 0 ? weight / maxWeight : 0
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#1e3a5f' }
if (ratio >= 0.65) return { label: 'Sehr wichtig', color: '#1d4ed8' }
if (ratio >= 0.40) return { label: 'Wichtig', color: '#475569' }
if (ratio >= 0.20) return { label: 'Wenig wichtig',color: '#94a3b8' }
return { label: 'Unwichtig', color: '#cbd5e1' }
}
function FactorRow({ factor, maxWeight }: { factor: ScoreFactor; maxWeight: number }) {
const label = CRITERION_LABEL[factor.criterion] ?? factor.criterion
const pct = Math.round(factor.weight * 100)
const imp = importanceLabel(factor.weight, maxWeight)
const color = criterionScoreColor(factor.score)
return (
<Box sx={{ mb: 1.25 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mb: 0.35 }}>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e293b', minWidth: 85 }}>{label}</Typography>
<Typography variant="caption" sx={{ color: imp.color, fontSize: '0.68rem', fontWeight: 500 }}>{imp.label}</Typography>
<Typography variant="caption" sx={{ color: '#cbd5e1', fontSize: '0.65rem' }}>{pct}%</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: criterionScoreTextColor(factor.score) }}>
{factor.score}/100
</Typography>
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', minWidth: 40, textAlign: 'right' }}>
{factor.contribution.toFixed(1)} Pkt
</Typography>
</Box>
</Box>
<LinearProgress variant="determinate" value={factor.score} color={color} sx={{ height: 4, borderRadius: 3, mb: 0.3 }} />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.3, fontSize: '0.7rem' }}>
{factor.explanation}
</Typography>
</Box>
)
}
export function ScoreInlineBreakdown({ scoreBreakdown: sb, allFactors, compact = false }: Props) {
const hardContrib = Math.round(sb.hardMatchScore * 0.60 * 10) / 10
const softContrib = Math.round(sb.softFactorScore * 0.40 * 10) / 10
const formulaBox = (
<Box sx={{ bgcolor: '#f8fafc', px: 1.25, py: 0.6, borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: '#64748b', display: 'block', mb: 0.25, fontWeight: 500, fontSize: '0.68rem' }}>
Berechnung
</Typography>
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#1e293b', display: 'block', lineHeight: 1.5 }}>
Hart {sb.hardMatchScore} × 60% + Soft {sb.softFactorScore} × 40%
{' = '}{hardContrib} + {softContrib}{' = '}
<span style={{ fontWeight: 800, color: criterionScoreTextColor(sb.totalScore) }}>{sb.totalScore}</span>
</Typography>
</Box>
)
// Compact: only the formula line
if (compact || !allFactors || allFactors.length === 0) {
return formulaBox
}
// Full: all criteria + formula
const hardFactors = allFactors.filter(f => HARD_KEYS.has(f.criterion))
const softFactors = allFactors.filter(f => !HARD_KEYS.has(f.criterion))
const maxHardWeight = Math.max(...hardFactors.map(f => f.weight), 0.001)
const maxSoftWeight = Math.max(...softFactors.map(f => f.weight), 0.001)
return (
<Box>
<Typography variant="caption" sx={{ fontWeight: 700, color: '#1e3a5f', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 1 }}>
Hart-Kriterien
</Typography>
{hardFactors.map((f, i) => <FactorRow key={i} factor={f} maxWeight={maxHardWeight} />)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#eef2ff', px: 1.25, py: 0.6, borderRadius: 1, mb: softFactors.length > 0 ? 2 : 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e3a5f' }}>
{sb.hardMatchScore}/100 × 60%
</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1e3a5f' }}>{hardContrib} Pkt</Typography>
</Box>
{softFactors.length > 0 && (
<>
<Divider sx={{ mb: 1.5 }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 1 }}>
Soft-Faktoren
</Typography>
{softFactors.map((f, i) => <FactorRow key={i} factor={f} maxWeight={maxSoftWeight} />)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#f8fafc', px: 1.25, py: 0.6, borderRadius: 1, mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569' }}>
{sb.softFactorScore}/100 × 40%
</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#475569' }}>{softContrib} Pkt</Typography>
</Box>
</>
)}
<Divider sx={{ mb: 1 }} />
{formulaBox}
</Box>
)
}