From 7f090a48ff1dc30bdca37aa880212652a6649b99 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 24 May 2026 13:02:01 +0200 Subject: [PATCH] refactor: replace hardcoded hex colors with design tokens across priority components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- eslint.config.js | 19 +++++++++++ .../compare/CompareColumnHeader.tsx | 17 +++------- src/components/compare/CompareTableBody.tsx | 16 ++++----- src/components/compare/compareUtils.tsx | 11 ++----- src/components/demand/AnfragenInquiryItem.tsx | 30 ++++++++--------- .../demand/AnfragenMessageBubble.tsx | 7 ++-- .../match-card/FutureAvailabilityCard.tsx | 22 +++++++------ .../match-card/IntelligenceMatchCard.tsx | 14 +------- .../match-card/MatchScoreDisplay.tsx | 9 ++--- .../match-detail/ScoreBreakdownPanel.tsx | 14 ++++---- src/components/shared/HeatBadge.tsx | 18 +++++----- src/lib/ds.ts | 32 ++++++++++++++++++ src/pages/demand/Anfragen.tsx | 33 ++++++++----------- 13 files changed, 130 insertions(+), 112 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index ef614d2..d9ae4b4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,4 +19,23 @@ export default defineConfig([ globals: globals.browser, }, }, + // Discourage new hardcoded hex colors in components and pages. + // Use DS_COLORS, matchScoreHex(), RESULT_TYPE_META, or MUI theme tokens ("primary.main") instead. + // See src/lib/ds.ts and src/lib/utils.ts for available tokens. + { + files: ['src/components/**/*.{ts,tsx}', 'src/pages/**/*.{ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'warn', + { + selector: [ + 'Property[key.name=/^(bgcolor|color|borderColor|background|fill)$/]', + ' > Literal[value=/^#[0-9A-Fa-f]{3,8}$/]', + ].join(''), + message: + 'Avoid hardcoded hex colors in sx props. Use DS_COLORS, matchScoreHex(), RESULT_TYPE_META, or MUI theme tokens ("primary.main") from src/lib/ds.ts and src/lib/utils.ts.', + }, + ], + }, + }, ]) diff --git a/src/components/compare/CompareColumnHeader.tsx b/src/components/compare/CompareColumnHeader.tsx index a2b0427..e94a82d 100644 --- a/src/components/compare/CompareColumnHeader.tsx +++ b/src/components/compare/CompareColumnHeader.tsx @@ -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 = { - 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 */} - + {item.matchScore} /100 @@ -152,7 +145,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) { variant="contained" startIcon={} 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 diff --git a/src/components/compare/CompareTableBody.tsx b/src/components/compare/CompareTableBody.tsx index 7eee20d..fa83728 100644 --- a/src/components/compare/CompareTableBody.tsx +++ b/src/components/compare/CompareTableBody.tsx @@ -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 }))} @@ -83,7 +83,7 @@ export function CompareTableBody({ iconTooltip="Höchster Match Score" > - + {item.matchScore} /100 @@ -172,9 +172,9 @@ export function CompareTableBody({ const sig = getSig(item) if (prop) return {prop.availabilityDate} if (sig) return ( - } iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit"> + } iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit"> ~{sig.timeHorizonMonths} Monate - + {Math.round(sig.probability * 100)}% Wahrscheinlichkeit @@ -305,9 +305,9 @@ export function CompareTableBody({ Nicht anwendbar ) return ( - } iconTooltip="Probabilistisches Zukunftssignal"> + } iconTooltip="Probabilistisches Zukunftssignal"> - + {Math.round(sig.probability * 100)}% Wahrscheinlichkeit diff --git a/src/components/compare/compareUtils.tsx b/src/components/compare/compareUtils.tsx index 0582c37..c1abb8e 100644 --- a/src/components/compare/compareUtils.tsx +++ b/src/components/compare/compareUtils.tsx @@ -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 = { - 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 = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } export const CRITERION_ALIASES: Record = { @@ -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 ( diff --git a/src/components/demand/AnfragenInquiryItem.tsx b/src/components/demand/AnfragenInquiryItem.tsx index 2a4bdd7..cf26218 100644 --- a/src/components/demand/AnfragenInquiryItem.tsx +++ b/src/components/demand/AnfragenInquiryItem.tsx @@ -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 = { - 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', }}> - {!inq.isRead && } + {!inq.isRead && } {inq.tenantName} {inq.unreadCount > 0 && ( - + {inq.unreadCount} )} @@ -63,7 +58,7 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }: )} + sx={{ height: 16, fontSize: '0.6rem', bgcolor: cfg?.bg, color: cfg?.fg, fontWeight: 600 }} /> {inq.matchScore && ( = 80 ? '#1a7a4a' : '#d97706', fontWeight: 700, fontSize: '0.7rem' }}> {inq.matchScore}% @@ -74,7 +69,12 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }: icon={} 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' }, + }} /> )} diff --git a/src/components/demand/AnfragenMessageBubble.tsx b/src/components/demand/AnfragenMessageBubble.tsx index a70d84a..8a4cd92 100644 --- a/src/components/demand/AnfragenMessageBubble.tsx +++ b/src/components/demand/AnfragenMessageBubble.tsx @@ -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)', }} > diff --git a/src/components/match-card/FutureAvailabilityCard.tsx b/src/components/match-card/FutureAvailabilityCard.tsx index a9c4e39..e4b4735 100644 --- a/src/components/match-card/FutureAvailabilityCard.tsx +++ b/src/components/match-card/FutureAvailabilityCard.tsx @@ -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 }) { {(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => ( - + {ind} ))} @@ -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 → diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx index 832e41c..f415162 100644 --- a/src/components/match-card/IntelligenceMatchCard.tsx +++ b/src/components/match-card/IntelligenceMatchCard.tsx @@ -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 = { - 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} diff --git a/src/components/match-card/MatchScoreDisplay.tsx b/src/components/match-card/MatchScoreDisplay.tsx index a94acf9..67ef63f 100644 --- a/src/components/match-card/MatchScoreDisplay.tsx +++ b/src/components/match-card/MatchScoreDisplay.tsx @@ -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 = { sm: '1.25rem', md: '1.75rem', lg: '2.5rem' } export function MatchScoreDisplay({ score, size = 'md' }: Props) { return ( - + {score} /100 diff --git a/src/components/match-detail/ScoreBreakdownPanel.tsx b/src/components/match-detail/ScoreBreakdownPanel.tsx index 7625e10..0cb27d5 100644 --- a/src/components/match-detail/ScoreBreakdownPanel.tsx +++ b/src/components/match-detail/ScoreBreakdownPanel.tsx @@ -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 */} - + {hasAllFactors ? 'Hart-Kriterien' : 'Bewertungsfaktoren'} {hardFactors.map((f, i) => )} 0 ? 2 : 1.5 }}> - + Hart-Kriterien {sb.hardMatchScore}/100 × 60% - + {hardContrib} Pkt @@ -120,7 +122,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps) {/* Total */} = 78 ? '#f0fdf4' : baseSum >= 52 ? '#fffbeb' : '#fef2f2', + bgcolor: baseSum >= SCORE_STRONG ? '#f0fdf4' : baseSum >= SCORE_MODERATE ? '#fffbeb' : '#fef2f2', p: 1.5, borderRadius: 1, }}> Gesamt-Score @@ -161,7 +163,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) { <> {/* Schritt 1 — Kriterien-Match */} - + Schritt 1 — Kriterien-Match @@ -205,7 +207,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) { {/* Schritt 2 — Verlässlichkeit */} - + Schritt 2 — Verlässlichkeit des Signals diff --git a/src/components/shared/HeatBadge.tsx b/src/components/shared/HeatBadge.tsx index 963a9cb..9d6a57e 100644 --- a/src/components/shared/HeatBadge.tsx +++ b/src/components/shared/HeatBadge.tsx @@ -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 ( - - + + {label} diff --git a/src/lib/ds.ts b/src/lib/ds.ts index 6bc9ace..0f23906 100644 --- a/src/lib/ds.ts +++ b/src/lib/ds.ts @@ -42,8 +42,40 @@ export const DS_COLORS = { LOW: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' }, INCOMPLETE: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' }, }, + heat: { + VERY_HOT: { bg: '#fef3c7', border: '#fcd34d', icon: '#d97706', text: '#92400e' }, + HOT: { bg: '#fff7ed', border: '#fed7aa', icon: '#ea580c', text: '#c2410c' }, + }, + futureCard: { + controlled: { + accent: '#7c3aed', + accentHover: '#6d28d9', + headerBg: '#faf5ff', + border: '#e9d5ff', + badgeBg: '#ede9fe', + badgeText: '#5b21b6', + }, + signal: { + accent: '#1d4ed8', + accentHover: '#1e40af', + headerBg: '#eff6ff', + border: '#bfdbfe', + badgeBg: '#dbeafe', + badgeText: '#1e40af', + indicator: '#1d4ed8', + }, + }, } as const +// ── Inquiry status metadata ─────────────────────────────────────────────────── + +export const INQUIRY_STATUS_META: Record = { + new: { label: 'Neu', fg: '#dc2626', bg: '#fef2f2' }, + in_progress: { label: 'Aktiv', fg: '#d97706', bg: '#fffbeb' }, + answered: { label: 'Beantwortet', fg: '#1a7a4a', bg: '#f0fdf4' }, + archived: { label: 'Archiviert', fg: '#64748b', bg: '#f8fafc' }, +} + // ── Score → qualitative level helpers ──────────────────────────────────────── export function scoreToConfidenceLevel(score: number): ConfidenceLevel { diff --git a/src/pages/demand/Anfragen.tsx b/src/pages/demand/Anfragen.tsx index 03f4d90..05fdab2 100644 --- a/src/pages/demand/Anfragen.tsx +++ b/src/pages/demand/Anfragen.tsx @@ -12,16 +12,10 @@ import type { InquiryMessage } from '../../domain/inquiry' import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection' import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble' import { AnfragenInquiryItem } from '../../components/demand/AnfragenInquiryItem' +import { INQUIRY_STATUS_META, DS_COLORS } from '../../lib/ds' // ── Config ──────────────────────────────────────────────────────────────────── -const STATUS_CONFIG: Record = { - 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' }, -} - const FILTER_TABS = [ { key: 'all', label: 'Alle' }, { key: 'new', label: 'Neu' }, @@ -147,7 +141,7 @@ export default function Anfragen() { Anfragen {totalUnread > 0 && ( + sx={{ bgcolor: INQUIRY_STATUS_META.new.fg, color: 'white', fontWeight: 700, height: 20, fontSize: '0.7rem' }} /> )} setStatusFilter(tab.key)} sx={{ height: 22, fontSize: '0.7rem', cursor: 'pointer', - bgcolor: statusFilter === tab.key ? '#1e3a5f' : '#f1f5f9', + bgcolor: statusFilter === tab.key ? 'primary.main' : '#f1f5f9', color: statusFilter === tab.key ? 'white' : '#475569', fontWeight: statusFilter === tab.key ? 700 : 400, - '&:hover': { bgcolor: statusFilter === tab.key ? '#1a3050' : '#e2e8f0' }, + '&:hover': { bgcolor: statusFilter === tab.key ? 'primary.dark' : '#e2e8f0' }, }} /> ))} @@ -210,7 +204,7 @@ export default function Anfragen() { setMobileShowChat(false)}> - + {selected.tenantName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)} @@ -232,9 +226,9 @@ export default function Anfragen() { }} /> )} {/* Pipeline link */} {linkedPipelineItem && ( @@ -245,10 +239,11 @@ export default function Anfragen() { onClick={() => navigate('/demand/pipeline')} sx={{ height: 22, fontSize: '0.75rem', cursor: 'pointer', - bgcolor: '#eff6ff', color: '#1e3a5f', fontWeight: 600, - border: '1px solid #bfdbfe', - '& .MuiChip-icon': { color: '#1e3a5f' }, - '&:hover': { bgcolor: '#dbeafe' }, + bgcolor: DS_COLORS.futureCard.signal.headerBg, fontWeight: 600, + color: 'primary.main', + border: `1px solid ${DS_COLORS.futureCard.signal.border}`, + '& .MuiChip-icon': { color: 'primary.main' }, + '&:hover': { bgcolor: DS_COLORS.futureCard.signal.badgeBg }, }} /> )} @@ -273,7 +268,7 @@ export default function Anfragen() { severity="info" icon={} onClose={() => setKiAlert(null)} - sx={{ py: 0.5, bgcolor: '#faf5ff', color: '#4c1d95', border: '1px solid #ddd6fe', '& .MuiAlert-icon': { color: '#7c3aed' } }} + sx={{ py: 0.5, bgcolor: DS_COLORS.futureCard.controlled.headerBg, color: DS_COLORS.futureCard.controlled.badgeText, border: `1px solid ${DS_COLORS.futureCard.controlled.border}`, '& .MuiAlert-icon': { color: DS_COLORS.futureCard.controlled.accent } }} > KI erkannt: „{kiAlert.title}" wurde in der Pipeline auf {kiAlert.stage} verschoben.{' '} navigate('/demand/pipeline')}> @@ -304,7 +299,7 @@ export default function Anfragen() { + sx={{ bgcolor: 'primary.main', color: 'white', '&:hover': { bgcolor: 'primary.dark' }, '&:disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' } }}>