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>
This commit is contained in:
Benjamin Sutter
2026-05-24 01:47:08 +02:00
parent 5d53f35dea
commit 86bdf142b4
16 changed files with 60 additions and 138 deletions
@@ -1,22 +1,17 @@
import { Chip } from '@mui/material' import { Chip } from '@mui/material'
import { confidenceHex } from '../../lib/utils'
interface Props { interface Props {
confidence: number confidence: number
size?: 'small' | 'medium' size?: 'small' | 'medium'
} }
function confidenceColor(c: number): string {
if (c >= 0.8) return '#1a7a4a'
if (c >= 0.6) return '#d97706'
return '#c0392b'
}
export function ConfidenceFieldBadge({ confidence, size = 'small' }: Props) { export function ConfidenceFieldBadge({ confidence, size = 'small' }: Props) {
return ( return (
<Chip <Chip
label={`${Math.round(confidence * 100)}%`} label={`${Math.round(confidence * 100)}%`}
size={size} size={size}
sx={{ bgcolor: confidenceColor(confidence), color: 'white', fontWeight: 700, fontSize: 11 }} sx={{ bgcolor: confidenceHex(confidence), color: 'white', fontWeight: 700, fontSize: 11 }}
/> />
) )
} }
+3 -14
View File
@@ -3,21 +3,10 @@ import { Building2 } from 'lucide-react'
import { MatchScoreDisplay } from './MatchScoreDisplay' import { MatchScoreDisplay } from './MatchScoreDisplay'
import { HeatBadge } from '../shared' import { HeatBadge } from '../shared'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import { RESULT_TYPE_META } from '../../lib/ds'
import { confidenceHex } from '../../lib/utils'
import type { MatchCardViewModel } from './MatchCardViewModel' import type { MatchCardViewModel } from './MatchCardViewModel'
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
VERIFIED_PORTFOLIO: { label: 'Plattform', color: '#1e3a5f' },
EXTERNAL_MARKET: { label: 'Plattform', color: '#1e3a5f' },
MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
}
function confidenceColor(score: number): string {
if (score >= 0.75) return '#1a7a4a'
if (score >= 0.55) return '#d97706'
return '#c0392b'
}
interface Props { interface Props {
vm: MatchCardViewModel vm: MatchCardViewModel
compact?: boolean compact?: boolean
@@ -61,7 +50,7 @@ export function MatchCardHeader({ vm, compact }: Props) {
<Chip <Chip
label={`${confPct}% Konfidenz`} label={`${confPct}% Konfidenz`}
size="small" size="small"
sx={{ bgcolor: confidenceColor(vm.confidenceScore), color: 'white', fontSize: 11 }} sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 11 }}
/> />
{vm.availabilityLabel && ( {vm.availabilityLabel && (
<Chip label={vm.availabilityLabel} size="small" variant="outlined" sx={{ fontSize: 11 }} /> <Chip label={vm.availabilityLabel} size="small" variant="outlined" sx={{ fontSize: 11 }} />
@@ -1,5 +1,6 @@
import { Box, Divider, LinearProgress, Typography } from '@mui/material' import { Box, Divider, LinearProgress, Typography } from '@mui/material'
import type { ScoreFactor } from '../../domain/match' import type { ScoreFactor } from '../../domain/match'
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
interface ScoreBreakdownData { interface ScoreBreakdownData {
hardMatchScore: number hardMatchScore: number
@@ -22,14 +23,6 @@ const CRITERION_LABEL: Record<string, string> = {
const HARD_KEYS = new Set(['area', 'location', 'budget', 'timing']) const HARD_KEYS = new Set(['area', 'location', 'budget', 'timing'])
function scoreColor(v: number): 'success' | 'warning' | 'error' {
return v >= 70 ? 'success' : v >= 50 ? 'warning' : 'error'
}
function scoreTextColor(v: number): string {
return v >= 70 ? '#1a7a4a' : v >= 50 ? '#d97706' : '#c0392b'
}
function importanceLabel(weight: number, maxWeight: number): { label: string; color: string } { function importanceLabel(weight: number, maxWeight: number): { label: string; color: string } {
const ratio = maxWeight > 0 ? weight / maxWeight : 0 const ratio = maxWeight > 0 ? weight / maxWeight : 0
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#1e3a5f' } if (ratio >= 0.85) return { label: 'Entscheidend', color: '#1e3a5f' }
@@ -43,7 +36,7 @@ function FactorRow({ factor, maxWeight }: { factor: ScoreFactor; maxWeight: numb
const label = CRITERION_LABEL[factor.criterion] ?? factor.criterion const label = CRITERION_LABEL[factor.criterion] ?? factor.criterion
const pct = Math.round(factor.weight * 100) const pct = Math.round(factor.weight * 100)
const imp = importanceLabel(factor.weight, maxWeight) const imp = importanceLabel(factor.weight, maxWeight)
const color = scoreColor(factor.score) const color = criterionScoreColor(factor.score)
return ( return (
<Box sx={{ mb: 1.25 }}> <Box sx={{ mb: 1.25 }}>
@@ -54,7 +47,7 @@ function FactorRow({ factor, maxWeight }: { factor: ScoreFactor; maxWeight: numb
<Typography variant="caption" sx={{ color: '#cbd5e1', fontSize: '0.65rem' }}>{pct}%</Typography> <Typography variant="caption" sx={{ color: '#cbd5e1', fontSize: '0.65rem' }}>{pct}%</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.75 }}> <Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: scoreTextColor(factor.score) }}> <Typography variant="caption" sx={{ fontWeight: 700, color: criterionScoreTextColor(factor.score) }}>
{factor.score}/100 {factor.score}/100
</Typography> </Typography>
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', minWidth: 40, textAlign: 'right' }}> <Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', minWidth: 40, textAlign: 'right' }}>
@@ -82,7 +75,7 @@ export function ScoreInlineBreakdown({ scoreBreakdown: sb, allFactors, compact =
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#1e293b', display: 'block', lineHeight: 1.5 }}> <Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#1e293b', display: 'block', lineHeight: 1.5 }}>
Hart {sb.hardMatchScore} × 60% + Soft {sb.softFactorScore} × 40% Hart {sb.hardMatchScore} × 60% + Soft {sb.softFactorScore} × 40%
{' = '}{hardContrib} + {softContrib}{' = '} {' = '}{hardContrib} + {softContrib}{' = '}
<span style={{ fontWeight: 800, color: scoreTextColor(sb.totalScore) }}>{sb.totalScore}</span> <span style={{ fontWeight: 800, color: criterionScoreTextColor(sb.totalScore) }}>{sb.totalScore}</span>
</Typography> </Typography>
</Box> </Box>
) )
@@ -3,6 +3,7 @@ import { MatchStatusBadge } from './MatchStatusBadge'
import type { Match } from '../../domain/match' import type { Match } from '../../domain/match'
import type { Property } from '../../domain/property' import type { Property } from '../../domain/property'
import type { Need } from '../../domain/need' import type { Need } from '../../domain/need'
import { matchScoreHex } from '../../lib/utils'
const STRENGTH_META: Record<string, { label: string; bg: string; color: string }> = { const STRENGTH_META: Record<string, { label: string; bg: string; color: string }> = {
STRONG: { label: 'Stark', bg: '#f0fdf4', color: '#1a7a4a' }, STRONG: { label: 'Stark', bg: '#f0fdf4', color: '#1a7a4a' },
@@ -10,12 +11,6 @@ const STRENGTH_META: Record<string, { label: string; bg: string; color: string }
WEAK: { label: 'Schwach', bg: '#fff1f2', color: '#c0392b' }, WEAK: { label: 'Schwach', bg: '#fff1f2', color: '#c0392b' },
} }
function scoreColor(score: number) {
if (score >= 80) return '#1a7a4a'
if (score >= 60) return '#d97706'
return '#c0392b'
}
interface Props { interface Props {
match: Match match: Match
property: Property | undefined property: Property | undefined
@@ -49,7 +44,7 @@ export function MatchListCard({ match, property, need, onSelect, onApprove }: Pr
width: 52, width: 52,
height: 52, height: 52,
borderRadius: 2, borderRadius: 2,
bgcolor: scoreColor(match.matchScore), bgcolor: matchScoreHex(match.matchScore),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
+3 -10
View File
@@ -1,19 +1,12 @@
import { Box, LinearProgress, Typography } from '@mui/material' import { Box, LinearProgress, Typography } from '@mui/material'
import type { ScoreFactor } from '../../domain/match' import type { ScoreFactor } from '../../domain/match'
import { factorLabel, importanceLabel } from './scoreBreakdownConstants' import { factorLabel, importanceLabel } from './scoreBreakdownConstants'
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
function scoreColor(v: number): 'success' | 'warning' | 'error' {
return v >= 70 ? 'success' : v >= 50 ? 'warning' : 'error'
}
function scoreTextColor(v: number): string {
return v >= 70 ? '#1a7a4a' : v >= 50 ? '#d97706' : '#c0392b'
}
export function CriterionRow({ factor, maxWeight }: { factor: ScoreFactor; maxWeight: number }) { export function CriterionRow({ factor, maxWeight }: { factor: ScoreFactor; maxWeight: number }) {
const label = factorLabel(factor.criterion) const label = factorLabel(factor.criterion)
const pct = Math.round(factor.weight * 100) const pct = Math.round(factor.weight * 100)
const color = scoreColor(factor.score) const color = criterionScoreColor(factor.score)
const imp = importanceLabel(factor.weight, maxWeight) const imp = importanceLabel(factor.weight, maxWeight)
return ( return (
@@ -34,7 +27,7 @@ export function CriterionRow({ factor, maxWeight }: { factor: ScoreFactor; maxWe
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: scoreTextColor(factor.score) }}> <Typography variant="caption" sx={{ fontWeight: 700, color: criterionScoreTextColor(factor.score) }}>
{factor.score}/100 {factor.score}/100
</Typography> </Typography>
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.68rem', minWidth: 44, textAlign: 'right' }}> <Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.68rem', minWidth: 44, textAlign: 'right' }}>
@@ -5,16 +5,7 @@ import type { Match } from '../../domain/match'
import type { FutureSignal } from '../../domain/futureSignal' import type { FutureSignal } from '../../domain/futureSignal'
import { CREDIBILITY_LABELS, HARD_KEYS, factorLabel } from './scoreBreakdownConstants' import { CREDIBILITY_LABELS, HARD_KEYS, factorLabel } from './scoreBreakdownConstants'
import { CriterionRow } from './CriterionRow' import { CriterionRow } from './CriterionRow'
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
// ── Shared helpers ─────────────────────────────────────────────────────────────
function scoreColor(v: number): 'success' | 'warning' | 'error' {
return v >= 70 ? 'success' : v >= 50 ? 'warning' : 'error'
}
function scoreTextColor(v: number): string {
return v >= 70 ? '#1a7a4a' : v >= 50 ? '#d97706' : '#c0392b'
}
// ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ────────────── // ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ──────────────
@@ -135,7 +126,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography> <Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
<Typography <Typography
variant="h4" variant="h4"
sx={{ fontWeight: 800, color: scoreTextColor(Math.round(baseSum)) }} sx={{ fontWeight: 800, color: criterionScoreTextColor(Math.round(baseSum)) }}
> >
{baseSum}/100 {baseSum}/100
</Typography> </Typography>
@@ -188,13 +179,13 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
} }
<Box sx={{ flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}> <Box sx={{ flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e293b' }}>{factorLabel(f.criterion)}</Typography> <Typography variant="caption" sx={{ fontWeight: 600, color: '#1e293b' }}>{factorLabel(f.criterion)}</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: scoreTextColor(f.score), ml: 1 }}>{f.score}/100</Typography> <Typography variant="caption" sx={{ fontWeight: 700, color: criterionScoreTextColor(f.score), ml: 1 }}>{f.score}/100</Typography>
</Box> </Box>
</Box> </Box>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={f.score} value={f.score}
color={scoreColor(f.score)} color={criterionScoreColor(f.score)}
sx={{ height: 5, borderRadius: 3, mb: 0.35, ml: 2.25 }} sx={{ height: 5, borderRadius: 3, mb: 0.35, ml: 2.25 }}
/> />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', ml: 2.25, lineHeight: 1.3 }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', ml: 2.25, lineHeight: 1.3 }}>
@@ -266,7 +257,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography> <Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
<Typography <Typography
variant="h4" variant="h4"
sx={{ fontWeight: 800, color: scoreTextColor(sb.totalScore) }} sx={{ fontWeight: 800, color: criterionScoreTextColor(sb.totalScore) }}
> >
{sb.totalScore}/100 {sb.totalScore}/100
</Typography> </Typography>
+2 -7
View File
@@ -1,10 +1,5 @@
import { Box, LinearProgress, Tooltip, Typography } from '@mui/material' import { Box, LinearProgress, Tooltip, Typography } from '@mui/material'
import { confidenceHex } from '../../lib/utils'
function scoreColor(score: number): string {
if (score >= 0.85) return '#15803d'
if (score >= 0.65) return '#a16207'
return '#dc2626'
}
interface ReliabilityScorePanelProps { interface ReliabilityScorePanelProps {
score: number score: number
@@ -13,7 +8,7 @@ interface ReliabilityScorePanelProps {
export function ReliabilityScorePanel({ score, compact = false }: ReliabilityScorePanelProps) { export function ReliabilityScorePanel({ score, compact = false }: ReliabilityScorePanelProps) {
const pct = Math.round(score * 100) const pct = Math.round(score * 100)
const color = scoreColor(score) const color = confidenceHex(score)
if (compact) { if (compact) {
return ( return (
+3 -3
View File
@@ -6,7 +6,7 @@ import { ExternalLink, MapPin, MessageSquare } from 'lucide-react'
import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay' import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay'
import { HeatBadge } from '../shared' import { HeatBadge } from '../shared'
import type { PipelineItem } from '../../domain/pipeline' import type { PipelineItem } from '../../domain/pipeline'
import { STAGES, RESULT_TYPE_LABEL, RESULT_TYPE_COLOR } from './pipelineConstants' import { STAGES, RESULT_TYPE_META } from './pipelineConstants'
import { detailPath } from './pipelineUtils' import { detailPath } from './pipelineUtils'
// ── DraggableCard ───────────────────────────────────────────────────────────── // ── DraggableCard ─────────────────────────────────────────────────────────────
@@ -68,8 +68,8 @@ export function DraggableCard({
<HeatBadge propertyId={item.propertyId} size="sm" /> <HeatBadge propertyId={item.propertyId} size="sm" />
<Chip <Chip
size="small" size="small"
label={RESULT_TYPE_LABEL[item.resultType] ?? item.resultType} label={RESULT_TYPE_META[item.resultType]?.label ?? item.resultType}
sx={{ bgcolor: RESULT_TYPE_COLOR[item.resultType] ?? '#475569', color: 'white', fontSize: 10, height: 20, fontWeight: 600 }} sx={{ bgcolor: RESULT_TYPE_META[item.resultType]?.color ?? '#475569', color: 'white', fontSize: 10, height: 20, fontWeight: 600 }}
/> />
<Chip <Chip
size="small" size="small"
+1 -14
View File
@@ -1,4 +1,5 @@
import type { PipelineStage } from '../../domain/pipeline' import type { PipelineStage } from '../../domain/pipeline'
export { RESULT_TYPE_META } from '../../lib/ds'
// ── Stage config ────────────────────────────────────────────────────────────── // ── Stage config ──────────────────────────────────────────────────────────────
@@ -20,20 +21,6 @@ export const NEXT_STAGE: Partial<Record<PipelineStage, { key: PipelineStage; lab
NEGOTIATION: { key: 'CLOSED_WON', label: 'Als gewonnen markieren' }, NEGOTIATION: { key: 'CLOSED_WON', label: 'Als gewonnen markieren' },
} }
export const RESULT_TYPE_LABEL: Record<string, string> = {
VERIFIED_PORTFOLIO: 'Plattform',
EXTERNAL_MARKET: 'Plattform',
MAISON_WORK: 'Maison Work',
FUTURE_AVAILABILITY: 'Future',
}
export const RESULT_TYPE_COLOR: Record<string, string> = {
VERIFIED_PORTFOLIO: '#1e3a5f',
EXTERNAL_MARKET: '#1e3a5f',
MAISON_WORK: '#0369a1',
FUTURE_AVAILABILITY: '#7c3aed',
}
export const MOCK_DOCS: Record<string, { name: string; date: string }[]> = { export const MOCK_DOCS: Record<string, { name: string; date: string }[]> = {
'pl-001': [ 'pl-001': [
{ name: 'Expose_Zollstrasse12.pdf', date: '05.05.2026' }, { name: 'Expose_Zollstrasse12.pdf', date: '05.05.2026' },
+1 -4
View File
@@ -1,8 +1,5 @@
import type { PipelineItem } from '../../domain/pipeline' import type { PipelineItem } from '../../domain/pipeline'
export { matchScoreHex as scoreColor } from '../../lib/utils'
export function scoreColor(score: number) {
return score >= 80 ? '#1a7a4a' : score >= 65 ? '#d97706' : '#c0392b'
}
export function detailPath(item: PipelineItem): string | null { export function detailPath(item: PipelineItem): string | null {
// propertyId is always stable across sessions — prefer it // propertyId is always stable across sessions — prefer it
+2 -8
View File
@@ -1,15 +1,9 @@
import type { ResultType } from '../../domain/enums' import type { ResultType } from '../../domain/enums'
import { Chip } from '@mui/material' import { Chip } from '@mui/material'
import { RESULT_TYPE_META } from '../../lib/ds'
const TYPE_META: Record<string, { label: string; color: string }> = {
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
EXTERNAL_MARKET: { label: 'Direktinserat', color: '#d97706' },
MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
}
export function ResultTypeBadge({ resultType }: { resultType: ResultType }) { export function ResultTypeBadge({ resultType }: { resultType: ResultType }) {
const meta = TYPE_META[resultType] ?? { label: resultType, color: '#64748b' } const meta = RESULT_TYPE_META[resultType] ?? { label: resultType, color: '#64748b' }
return ( return (
<Chip <Chip
label={meta.label} label={meta.label}
+2 -7
View File
@@ -1,19 +1,14 @@
import { Box, Button, Card, CardContent, Chip, LinearProgress, Typography } from '@mui/material' import { Box, Button, Card, CardContent, Chip, LinearProgress, Typography } from '@mui/material'
import type { DataQualitySummary } from '../../domain/dashboard' import type { DataQualitySummary } from '../../domain/dashboard'
import { dataQualityColor } from '../../lib/utils'
interface DataQualityWidgetProps { interface DataQualityWidgetProps {
summary: DataQualitySummary summary: DataQualitySummary
onNavigate: () => void onNavigate: () => void
} }
function qualityColor(score: number): 'success' | 'warning' | 'error' {
if (score >= 80) return 'success'
if (score >= 60) return 'warning'
return 'error'
}
export function DataQualityWidget({ summary, onNavigate }: DataQualityWidgetProps) { export function DataQualityWidget({ summary, onNavigate }: DataQualityWidgetProps) {
const color = qualityColor(summary.avgScore) const color = dataQualityColor(summary.avgScore / 100)
return ( return (
<Card> <Card>
@@ -1,17 +1,12 @@
import { Box, Button, Card, CardContent, Chip, Typography } from '@mui/material' import { Box, Button, Card, CardContent, Chip, Typography } from '@mui/material'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import type { StrongMatchItem } from '../../domain/dashboard' import type { StrongMatchItem } from '../../domain/dashboard'
import { matchScoreHex } from '../../lib/utils'
interface StrongMatchMiniCardProps { interface StrongMatchMiniCardProps {
match: StrongMatchItem match: StrongMatchItem
} }
function scoreColor(score: number): string {
if (score >= 80) return '#1a7a4a'
if (score >= 60) return '#d97706'
return '#c0392b'
}
export function StrongMatchMiniCard({ match }: StrongMatchMiniCardProps) { export function StrongMatchMiniCard({ match }: StrongMatchMiniCardProps) {
const navigate = useNavigate() const navigate = useNavigate()
@@ -28,7 +23,7 @@ export function StrongMatchMiniCard({ match }: StrongMatchMiniCardProps) {
sx={{ sx={{
ml: 1, ml: 1,
fontWeight: 700, fontWeight: 700,
bgcolor: scoreColor(match.matchScore), bgcolor: matchScoreHex(match.matchScore),
color: 'white', color: 'white',
flexShrink: 0, flexShrink: 0,
}} }}
+5 -25
View File
@@ -1,4 +1,6 @@
import type { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums' import type { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums'
import { RESULT_TYPE_META } from '../../lib/ds'
import { dataQualityColor } from '../../lib/utils'
export function getAssetTypeLabel(type: AssetType): string { export function getAssetTypeLabel(type: AssetType): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
@@ -51,33 +53,11 @@ export function getAvailabilityChipColor(status: AvailabilityStatus): 'success'
} }
export function getResultTypeLabel(type: ResultType): string { export function getResultTypeLabel(type: ResultType): string {
const labels: Record<string, string> = { return RESULT_TYPE_META[type]?.label ?? type
VERIFIED_PORTFOLIO: 'Portfolio',
EXTERNAL_MARKET: 'Direktinserat',
MAISON_WORK: 'Maison Work',
FUTURE_AVAILABILITY: 'Zukunft',
}
return labels[type] ?? type
} }
export function getResultTypeColor(type: ResultType): string { export function getResultTypeColor(type: ResultType): string {
const colors: Record<string, string> = { return RESULT_TYPE_META[type]?.color ?? '#6b7280'
VERIFIED_PORTFOLIO: '#1e3a5f',
EXTERNAL_MARKET: '#1a7a4a',
MAISON_WORK: '#0369a1',
FUTURE_AVAILABILITY: '#7c3aed',
}
return colors[type] ?? '#6b7280'
} }
export function qualityColor(score: number): 'success' | 'warning' | 'error' { export const qualityColor = dataQualityColor
if (score >= 0.8) return 'success'
if (score >= 0.6) return 'warning'
return 'error'
}
export function confidenceColor(score: number): string {
if (score >= 0.85) return '#1a7a4a'
if (score >= 0.65) return '#1e3a5f'
return '#d97706'
}
+9
View File
@@ -60,3 +60,12 @@ export function scoreToDataQualityLevel(score: number): DataQualityLevel {
if (score > 0) return 'LOW' if (score > 0) return 'LOW'
return 'INCOMPLETE' return 'INCOMPLETE'
} }
// ── Result type meta — single source for labels + colors ─────────────────────
export const RESULT_TYPE_META: Record<string, { label: string; color: string; bg: string }> = {
VERIFIED_PORTFOLIO: { label: 'Plattform', color: '#1e3a5f', bg: 'rgba(30,58,95,0.10)' },
EXTERNAL_MARKET: { label: 'Plattform', color: '#1e3a5f', bg: 'rgba(30,58,95,0.10)' },
MAISON_WORK: { label: 'Maison Work', color: '#0369a1', bg: 'rgba(3,105,161,0.10)' },
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed', bg: 'rgba(109,40,217,0.10)' },
}
+14
View File
@@ -83,6 +83,20 @@ export function probabilityHex(prob: number): string {
return '#c0392b' return '#c0392b'
} }
export function matchScoreHex(score: number): string {
if (score >= SCORE_STRONG) return '#1a7a4a'
if (score >= SCORE_MODERATE) return '#d97706'
return '#c0392b'
}
export function criterionScoreColor(score: number): 'success' | 'warning' | 'error' {
return score >= 70 ? 'success' : score >= 50 ? 'warning' : 'error'
}
export function criterionScoreTextColor(score: number): string {
return score >= 70 ? '#1a7a4a' : score >= 50 ? '#d97706' : '#c0392b'
}
// ── Misc ────────────────────────────────────────────────────────────────────── // ── Misc ──────────────────────────────────────────────────────────────────────
export function clamp(value: number, min: number, max: number): number { export function clamp(value: number, min: number, max: number): number {