refactor: replace hardcoded hex colors with design tokens across priority components
- Add DS_COLORS.heat, DS_COLORS.futureCard, INQUIRY_STATUS_META tokens to ds.ts - Eliminate 4 duplicate RESULT_TYPE_META / TYPE_META constants (now all use ds.ts) - Eliminate 3 duplicate scoreColor / SCORE_COLOR functions (now all use matchScoreHex) - HeatBadge: use DS_COLORS.heat.VERY_HOT / HOT tokens - FutureAvailabilityCard: use DS_COLORS.futureCard.controlled / signal tokens - IntelligenceMatchCard: import RESULT_TYPE_META from ds.ts, remove primary override - MatchScoreDisplay: replace local scoreColor() with matchScoreHex() - CompareColumnHeader: remove local TYPE_META + SCORE_COLOR, use ds.ts + utils.ts - CompareTableBody: import RESULT_TYPE_META + matchScoreHex, fix #7c3aed → token - compareUtils: remove TYPE_META + SCORE_COLOR, use dataQualityHex in scoreBar - Anfragen + AnfragenInquiryItem: deduplicate STATUS_CONFIG → INQUIRY_STATUS_META - Anfragen: #1e3a5f → 'primary.main', KI alert → futureCard.controlled tokens - AnfragenMessageBubble: own-message bubble → 'primary.main', AI bubble → tokens - ScoreBreakdownPanel: #1e3a5f → 'primary.main', #6d28d9 → futureCard.controlled - ESLint: add warn rule against new hex colors in component sx props Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,17 +3,10 @@ import { X, AlertTriangle, BookmarkCheck, ExternalLink, Kanban } from 'lucide-re
|
||||
import { useNavigate } from 'react-router'
|
||||
import { usePipelineItems } from '../../hooks/usePipeline'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||
import { matchScoreHex } from '../../lib/utils'
|
||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||
|
||||
const 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' },
|
||||
}
|
||||
|
||||
const SCORE_COLOR = (s: number) => s >= 78 ? '#1a7a4a' : s >= 52 ? '#d97706' : '#c0392b'
|
||||
|
||||
interface Props {
|
||||
item: UnifiedMatchResult
|
||||
onRemove: () => void
|
||||
@@ -24,7 +17,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
||||
const { data: pipelineItems = [] } = usePipelineItems()
|
||||
const { openSavedDialog } = usePipelineStore()
|
||||
|
||||
const meta = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
|
||||
const meta = RESULT_TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
|
||||
const prop = item.resultType !== 'FUTURE_AVAILABILITY' ? (item as any).property : null
|
||||
const sig = item.resultType === 'FUTURE_AVAILABILITY' ? (item as any).signal : null
|
||||
|
||||
@@ -86,7 +79,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
||||
|
||||
{/* Row 3: score */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 1 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: SCORE_COLOR(item.matchScore), lineHeight: 1 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: matchScoreHex(item.matchScore), lineHeight: 1 }}>
|
||||
{item.matchScore}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">/100</Typography>
|
||||
@@ -152,7 +145,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
||||
variant="contained"
|
||||
startIcon={<Kanban size={12} />}
|
||||
onClick={handleSave}
|
||||
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5 }}
|
||||
>
|
||||
Merken
|
||||
</Button>
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
import { AlertOctagon, AlertTriangle, CheckCircle2, Trophy, XCircle, Zap } from 'lucide-react'
|
||||
import {
|
||||
HARD_CRITERIA,
|
||||
SCORE_COLOR,
|
||||
TYPE_META,
|
||||
RISK_LEVEL_ORDER,
|
||||
getProp,
|
||||
getSig,
|
||||
@@ -20,6 +18,8 @@ import {
|
||||
scoreBar,
|
||||
} from './compareUtils'
|
||||
import { CompareCell, MissingDataCell } from './index'
|
||||
import { RESULT_TYPE_META, DS_COLORS } from '../../lib/ds'
|
||||
import { matchScoreHex } from '../../lib/utils'
|
||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||
|
||||
// ── Local helper ──────────────────────────────────────────────────────────────
|
||||
@@ -61,7 +61,7 @@ export function CompareTableBody({
|
||||
|
||||
{/* 1. Result Type */}
|
||||
{row('1. Result-Typ', compareItems.map(item => {
|
||||
const m = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
|
||||
const m = RESULT_TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
|
||||
return <Chip label={m.label} size="small" sx={{ bgcolor: m.color, color: 'white', fontWeight: 600, fontSize: 11 }} />
|
||||
}))}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function CompareTableBody({
|
||||
iconTooltip="Höchster Match Score"
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, color: SCORE_COLOR(item.matchScore), lineHeight: 1 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, color: matchScoreHex(item.matchScore), lineHeight: 1 }}>
|
||||
{item.matchScore}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">/100</Typography>
|
||||
@@ -172,9 +172,9 @@ export function CompareTableBody({
|
||||
const sig = getSig(item)
|
||||
if (prop) return <Typography variant="body2">{prop.availabilityDate}</Typography>
|
||||
if (sig) return (
|
||||
<CompareCell highlight="future" icon={<Zap size={14} color="#7c3aed" />} iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit">
|
||||
<CompareCell highlight="future" icon={<Zap size={14} color={DS_COLORS.futureCard.controlled.accent} />} iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit">
|
||||
<Typography variant="body2">~{sig.timeHorizonMonths} Monate</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#7c3aed' }}>
|
||||
<Typography variant="caption" sx={{ color: DS_COLORS.futureCard.controlled.accent }}>
|
||||
{Math.round(sig.probability * 100)}% Wahrscheinlichkeit
|
||||
</Typography>
|
||||
</CompareCell>
|
||||
@@ -305,9 +305,9 @@ export function CompareTableBody({
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>Nicht anwendbar</Typography>
|
||||
)
|
||||
return (
|
||||
<CompareCell highlight="future" icon={<Zap size={14} color="#7c3aed" />} iconTooltip="Probabilistisches Zukunftssignal">
|
||||
<CompareCell highlight="future" icon={<Zap size={14} color={DS_COLORS.futureCard.controlled.accent} />} iconTooltip="Probabilistisches Zukunftssignal">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#7c3aed' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_COLORS.futureCard.controlled.accent }}>
|
||||
{Math.round(sig.probability * 100)}% Wahrscheinlichkeit
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
|
||||
import { Box, LinearProgress, Typography } from '@mui/material'
|
||||
import type { WeightingKey } from '../../domain/needBuilder'
|
||||
import type { UnifiedMatchResult, VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult'
|
||||
import { dataQualityHex } from '../../lib/utils'
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
|
||||
export const SCORE_COLOR = (s: number) => s >= 78 ? '#1a7a4a' : s >= 52 ? '#d97706' : '#c0392b'
|
||||
|
||||
export function getProp(item: UnifiedMatchResult) {
|
||||
return item.resultType !== 'FUTURE_AVAILABILITY'
|
||||
@@ -20,13 +20,6 @@ export function getSig(item: UnifiedMatchResult) {
|
||||
: null
|
||||
}
|
||||
|
||||
export const 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' },
|
||||
}
|
||||
|
||||
export const RISK_LEVEL_ORDER: Record<string, number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
|
||||
export const CRITERION_ALIASES: Record<WeightingKey, string[]> = {
|
||||
@@ -78,7 +71,7 @@ export const DATA_SX = {
|
||||
// ── Score bar helper ──────────────────────────────────────────────────────────
|
||||
|
||||
export function scoreBar(value: number, label?: string): ReactNode {
|
||||
const color = value >= 0.8 ? '#1a7a4a' : value >= 0.6 ? '#d97706' : '#c0392b'
|
||||
const color = dataQualityHex(value)
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{ width: 70 }}>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { Box, Chip, Typography } from '@mui/material'
|
||||
import { Kanban } from 'lucide-react'
|
||||
import { INQUIRY_STATUS_META, DS_COLORS } from '../../lib/ds'
|
||||
import type { Inquiry } from '../../domain/inquiry'
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string; bgColor: string }> = {
|
||||
new: { label: 'Neu', color: '#dc2626', bgColor: '#fef2f2' },
|
||||
in_progress: { label: 'Aktiv', color: '#d97706', bgColor: '#fffbeb' },
|
||||
answered: { label: 'Beantwortet', color: '#1a7a4a', bgColor: '#f0fdf4' },
|
||||
archived: { label: 'Archiviert', color: '#64748b', bgColor: '#f8fafc' },
|
||||
}
|
||||
|
||||
interface AnfragenInquiryItemProps {
|
||||
inq: Inquiry
|
||||
isSelected: boolean
|
||||
@@ -17,7 +11,7 @@ interface AnfragenInquiryItemProps {
|
||||
}
|
||||
|
||||
export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }: AnfragenInquiryItemProps) {
|
||||
const cfg = STATUS_CONFIG[inq.status ?? 'new']
|
||||
const cfg = INQUIRY_STATUS_META[inq.status ?? 'new']
|
||||
const displayDate = new Date(inq.updatedAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit' })
|
||||
const lastMsg = inq.thread[inq.thread.length - 1]
|
||||
|
||||
@@ -26,21 +20,22 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
||||
px: 2, py: 1.5,
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
cursor: 'pointer',
|
||||
bgcolor: isSelected ? '#eff6ff' : !inq.isRead ? 'rgba(220,38,38,0.03)' : 'transparent',
|
||||
borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent',
|
||||
'&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' },
|
||||
bgcolor: isSelected ? DS_COLORS.futureCard.signal.headerBg : !inq.isRead ? 'rgba(220,38,38,0.03)' : 'transparent',
|
||||
borderLeft: isSelected ? '3px solid' : '3px solid transparent',
|
||||
borderLeftColor: isSelected ? 'primary.main' : 'transparent',
|
||||
'&:hover': { bgcolor: isSelected ? DS_COLORS.futureCard.signal.headerBg : '#f8fafc' },
|
||||
transition: 'background-color 0.1s ease',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.375 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
|
||||
{!inq.isRead && <Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: '#dc2626', flexShrink: 0 }} />}
|
||||
{!inq.isRead && <Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: cfg?.fg ?? '#dc2626', flexShrink: 0 }} />}
|
||||
<Typography variant="body2" sx={{ fontWeight: !inq.isRead ? 700 : 500, fontSize: '0.8125rem' }} noWrap>
|
||||
{inq.tenantName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
|
||||
{inq.unreadCount > 0 && (
|
||||
<Box sx={{ width: 18, height: 18, borderRadius: '50%', bgcolor: '#dc2626', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Box sx={{ width: 18, height: 18, borderRadius: '50%', bgcolor: cfg?.fg ?? '#dc2626', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography sx={{ color: 'white', fontSize: '0.6rem', fontWeight: 700 }}>{inq.unreadCount}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -63,7 +58,7 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip label={cfg?.label ?? inq.status} size="small"
|
||||
sx={{ height: 16, fontSize: '0.6rem', bgcolor: cfg?.bgColor, color: cfg?.color, fontWeight: 600 }} />
|
||||
sx={{ height: 16, fontSize: '0.6rem', bgcolor: cfg?.bg, color: cfg?.fg, fontWeight: 600 }} />
|
||||
{inq.matchScore && (
|
||||
<Typography variant="caption" sx={{ color: inq.matchScore >= 80 ? '#1a7a4a' : '#d97706', fontWeight: 700, fontSize: '0.7rem' }}>
|
||||
{inq.matchScore}%
|
||||
@@ -74,7 +69,12 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
||||
icon={<Kanban size={9} />}
|
||||
label="Pipeline"
|
||||
size="small"
|
||||
sx={{ height: 16, fontSize: '0.6rem', bgcolor: '#eff6ff', color: '#1e3a5f', fontWeight: 600, '& .MuiChip-icon': { color: '#1e3a5f' } }}
|
||||
sx={{
|
||||
height: 16, fontSize: '0.6rem', fontWeight: 600,
|
||||
bgcolor: DS_COLORS.futureCard.signal.headerBg,
|
||||
color: 'primary.main',
|
||||
'& .MuiChip-icon': { color: 'primary.main' },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { Bot, Building2, FileText } from 'lucide-react'
|
||||
import { DS_COLORS } from '../../lib/ds'
|
||||
import type { InquiryMessage } from '../../domain/inquiry'
|
||||
|
||||
export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
|
||||
@@ -22,15 +23,15 @@ export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
|
||||
maxWidth: { xs: '88%', md: '72%' },
|
||||
px: 2, py: 1.25,
|
||||
borderRadius: isOwnMessage ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
||||
bgcolor: isOwnMessage ? '#1e3a5f' : isAI ? '#f5f3ff' : 'white',
|
||||
border: isOwnMessage ? 'none' : isAI ? '1px solid #ddd6fe' : '1px solid #e2e8f0',
|
||||
bgcolor: isOwnMessage ? 'primary.main' : isAI ? DS_COLORS.futureCard.controlled.headerBg : 'white',
|
||||
border: isOwnMessage ? 'none' : isAI ? `1px solid ${DS_COLORS.futureCard.controlled.border}` : '1px solid #e2e8f0',
|
||||
boxShadow: isOwnMessage ? '0 2px 6px rgba(30,58,95,0.2)' : '0 1px 2px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: isOwnMessage ? 'white' : isAI ? '#5b21b6' : '#1e293b',
|
||||
color: isOwnMessage ? 'white' : isAI ? DS_COLORS.futureCard.controlled.badgeText : '#1e293b',
|
||||
whiteSpace: 'pre-wrap', lineHeight: 1.65, fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router'
|
||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||
import { floorLabel, SOURCE_META, ASSET_TYPE_LABELS, getOpportunityHeadline } from './futureAvailabilityCardHelpers'
|
||||
import { SignalQualityDots } from './SignalQualityDots'
|
||||
import { DS_COLORS } from '../../lib/ds'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
|
||||
// ── Future Availability Card ──────────────────────────────────────────────────
|
||||
@@ -12,13 +13,12 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
const isControlled = vm.signalIsControlled ?? false
|
||||
const navigate = useNavigate()
|
||||
|
||||
// PRE-MARKET VERIFIED: soft purple / institutional premium
|
||||
// MARKET SIGNAL: slate blue / analytical
|
||||
const accentColor = isControlled ? '#7c3aed' : '#1d4ed8'
|
||||
const headerBg = isControlled ? '#faf5ff' : '#eff6ff'
|
||||
const borderColor = isControlled ? '#e9d5ff' : '#bfdbfe'
|
||||
const badgeBg = isControlled ? '#ede9fe' : '#dbeafe'
|
||||
const badgeColor = isControlled ? '#5b21b6' : '#1e40af'
|
||||
const cardColors = isControlled ? DS_COLORS.futureCard.controlled : DS_COLORS.futureCard.signal
|
||||
const accentColor = cardColors.accent
|
||||
const headerBg = cardColors.headerBg
|
||||
const borderColor = cardColors.border
|
||||
const badgeBg = cardColors.badgeBg
|
||||
const badgeColor = cardColors.badgeText
|
||||
|
||||
const assetLabel = vm.assetType ? (ASSET_TYPE_LABELS[vm.assetType] ?? vm.assetType) : 'Fläche'
|
||||
const headline = getOpportunityHeadline(vm.signalType, assetLabel)
|
||||
@@ -168,7 +168,7 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.35 }}>
|
||||
{(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.6 }}>
|
||||
<Box sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '5px', flexShrink: 0 }} />
|
||||
<Box sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: DS_COLORS.futureCard.signal.indicator, mt: '5px', flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.72rem', color: '#475569', lineHeight: 1.3 }}>{ind}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
@@ -210,7 +210,7 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||
...(a.variant === 'primary' && {
|
||||
bgcolor: accentColor,
|
||||
'&:hover': { bgcolor: isControlled ? '#6d28d9' : '#1e40af' },
|
||||
'&:hover': { bgcolor: cardColors.accentHover },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
@@ -224,7 +224,9 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
onClick={() => navigate(`/demand/property/${vm.propertyId}${vm.unitId ? `?unit=${vm.unitId}` : ''}`)}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||
bgcolor: '#7c3aed', '&:hover': { bgcolor: '#6d28d9' }, ml: 'auto',
|
||||
bgcolor: DS_COLORS.futureCard.controlled.accent,
|
||||
'&:hover': { bgcolor: DS_COLORS.futureCard.controlled.accentHover },
|
||||
ml: 'auto',
|
||||
}}
|
||||
>
|
||||
Zur Einheit →
|
||||
|
||||
@@ -7,18 +7,10 @@ import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { LocationPreview } from '../shared/LocationPreview'
|
||||
import { HeatBadge } from '../shared/HeatBadge'
|
||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
import { FutureAvailabilityCard } from './FutureAvailabilityCard'
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
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: 'Future Availability', color: '#7c3aed' },
|
||||
}
|
||||
|
||||
// ── Main component ────────────────────────────────────────────────────────────
|
||||
|
||||
interface Props {
|
||||
@@ -163,10 +155,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
||||
disabled={a.disabled}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||
...(a.variant === 'primary' && {
|
||||
bgcolor: '#1e3a5f',
|
||||
'&:hover': { bgcolor: '#162d4a' },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{a.label}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { matchScoreHex } from '../../lib/utils'
|
||||
|
||||
interface Props {
|
||||
score: number
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
function scoreColor(score: number): string {
|
||||
if (score >= 78) return '#1a7a4a'
|
||||
if (score >= 52) return '#d97706'
|
||||
return '#c0392b'
|
||||
}
|
||||
|
||||
const FONT_SIZES: Record<string, string> = { sm: '1.25rem', md: '1.75rem', lg: '2.5rem' }
|
||||
|
||||
export function MatchScoreDisplay({ score, size = 'md' }: Props) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.25, flexShrink: 0 }}>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: FONT_SIZES[size], color: scoreColor(score), lineHeight: 1 }}>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: FONT_SIZES[size], color: matchScoreHex(score), lineHeight: 1 }}>
|
||||
{score}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">/100</Typography>
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { FutureSignal } from '../../domain/futureSignal'
|
||||
import { CREDIBILITY_LABELS, HARD_KEYS, factorLabel } from './scoreBreakdownConstants'
|
||||
import { CriterionRow } from './CriterionRow'
|
||||
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
|
||||
import { DS_COLORS } from '../../lib/ds'
|
||||
import { SCORE_STRONG, SCORE_MODERATE } from '../../lib/constants'
|
||||
|
||||
// ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ──────────────
|
||||
|
||||
@@ -38,17 +40,17 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
|
||||
return (
|
||||
<>
|
||||
{/* Hard criteria */}
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#1e3a5f', textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 1.25 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 1.25 }}>
|
||||
{hasAllFactors ? 'Hart-Kriterien' : 'Bewertungsfaktoren'}
|
||||
</Typography>
|
||||
|
||||
{hardFactors.map((f, i) => <CriterionRow key={i} factor={f} maxWeight={maxHardWeight} />)}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#eef2ff', px: 1.25, py: 0.75, borderRadius: 1, mt: 0.25, mb: hasAllFactors && softFactors.length > 0 ? 2 : 1.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e3a5f' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'primary.main' }}>
|
||||
Hart-Kriterien {sb.hardMatchScore}/100 × 60%
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1e3a5f' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: 'primary.main' }}>
|
||||
{hardContrib} Pkt
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -120,7 +122,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
|
||||
{/* Total */}
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
||||
bgcolor: baseSum >= 78 ? '#f0fdf4' : baseSum >= 52 ? '#fffbeb' : '#fef2f2',
|
||||
bgcolor: baseSum >= SCORE_STRONG ? '#f0fdf4' : baseSum >= SCORE_MODERATE ? '#fffbeb' : '#fef2f2',
|
||||
p: 1.5, borderRadius: 1,
|
||||
}}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
|
||||
@@ -161,7 +163,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
|
||||
<>
|
||||
{/* Schritt 1 — Kriterien-Match */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#6d28d9', textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_COLORS.futureCard.controlled.accent, textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 0.75 }}>
|
||||
Schritt 1 — Kriterien-Match
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.25, lineHeight: 1.4 }}>
|
||||
@@ -205,7 +207,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
|
||||
|
||||
{/* Schritt 2 — Verlässlichkeit */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#6d28d9', textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_COLORS.futureCard.controlled.accent, textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 0.75 }}>
|
||||
Schritt 2 — Verlässlichkeit des Signals
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Tooltip, Typography } from '@mui/material'
|
||||
import { Flame } from 'lucide-react'
|
||||
import { getHeatLevel, heatTooltip } from '../../lib/propertyHeat'
|
||||
import { DS_COLORS } from '../../lib/ds'
|
||||
|
||||
interface Props {
|
||||
propertyId: string | undefined
|
||||
@@ -13,27 +14,24 @@ export function HeatBadge({ propertyId, size = 'md' }: Props) {
|
||||
|
||||
const isVeryHot = level === 'VERY_HOT'
|
||||
const tooltip = heatTooltip(propertyId)
|
||||
const colors = isVeryHot ? DS_COLORS.heat.VERY_HOT : DS_COLORS.heat.HOT
|
||||
|
||||
const iconSize = size === 'sm' ? 10 : 12
|
||||
const fontSize = size === 'sm' ? '0.6rem' : '0.65rem'
|
||||
const label = isVeryHot ? 'Sehr gefragt' : 'Gefragt'
|
||||
const bgColor = isVeryHot ? '#fef3c7' : '#fff7ed'
|
||||
const border = isVeryHot ? '1px solid #fcd34d' : '1px solid #fed7aa'
|
||||
const iconColor = isVeryHot ? '#d97706' : '#ea580c'
|
||||
const textColor = isVeryHot ? '#92400e' : '#c2410c'
|
||||
const iconSize = size === 'sm' ? 10 : 12
|
||||
const fontSize = size === 'sm' ? '0.6rem' : '0.65rem'
|
||||
const label = isVeryHot ? 'Sehr gefragt' : 'Gefragt'
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip} placement="top" arrow>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.35,
|
||||
bgcolor: bgColor, border, borderRadius: 1,
|
||||
bgcolor: colors.bg, border: `1px solid ${colors.border}`, borderRadius: 1,
|
||||
px: size === 'sm' ? 0.6 : 0.75, py: 0.15,
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<Flame size={iconSize} color={iconColor} fill={isVeryHot ? iconColor : 'none'} />
|
||||
<Typography sx={{ fontSize, fontWeight: 700, color: textColor, lineHeight: 1.2 }}>
|
||||
<Flame size={iconSize} color={colors.icon} fill={isVeryHot ? colors.icon : 'none'} />
|
||||
<Typography sx={{ fontSize, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user