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:
@@ -19,4 +19,23 @@ export default defineConfig([
|
|||||||
globals: globals.browser,
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -3,17 +3,10 @@ import { X, AlertTriangle, BookmarkCheck, ExternalLink, Kanban } from 'lucide-re
|
|||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
import { usePipelineItems } from '../../hooks/usePipeline'
|
import { usePipelineItems } from '../../hooks/usePipeline'
|
||||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||||
|
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||||
|
import { matchScoreHex } from '../../lib/utils'
|
||||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
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 {
|
interface Props {
|
||||||
item: UnifiedMatchResult
|
item: UnifiedMatchResult
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
@@ -24,7 +17,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
|||||||
const { data: pipelineItems = [] } = usePipelineItems()
|
const { data: pipelineItems = [] } = usePipelineItems()
|
||||||
const { openSavedDialog } = usePipelineStore()
|
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 prop = item.resultType !== 'FUTURE_AVAILABILITY' ? (item as any).property : null
|
||||||
const sig = item.resultType === 'FUTURE_AVAILABILITY' ? (item as any).signal : 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 */}
|
{/* Row 3: score */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 1 }}>
|
<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}
|
{item.matchScore}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">/100</Typography>
|
<Typography variant="caption" color="text.secondary">/100</Typography>
|
||||||
@@ -152,7 +145,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<Kanban size={12} />}
|
startIcon={<Kanban size={12} />}
|
||||||
onClick={handleSave}
|
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
|
Merken
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
import { AlertOctagon, AlertTriangle, CheckCircle2, Trophy, XCircle, Zap } from 'lucide-react'
|
import { AlertOctagon, AlertTriangle, CheckCircle2, Trophy, XCircle, Zap } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
HARD_CRITERIA,
|
HARD_CRITERIA,
|
||||||
SCORE_COLOR,
|
|
||||||
TYPE_META,
|
|
||||||
RISK_LEVEL_ORDER,
|
RISK_LEVEL_ORDER,
|
||||||
getProp,
|
getProp,
|
||||||
getSig,
|
getSig,
|
||||||
@@ -20,6 +18,8 @@ import {
|
|||||||
scoreBar,
|
scoreBar,
|
||||||
} from './compareUtils'
|
} from './compareUtils'
|
||||||
import { CompareCell, MissingDataCell } from './index'
|
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'
|
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||||
|
|
||||||
// ── Local helper ──────────────────────────────────────────────────────────────
|
// ── Local helper ──────────────────────────────────────────────────────────────
|
||||||
@@ -61,7 +61,7 @@ export function CompareTableBody({
|
|||||||
|
|
||||||
{/* 1. Result Type */}
|
{/* 1. Result Type */}
|
||||||
{row('1. Result-Typ', compareItems.map(item => {
|
{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 }} />
|
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"
|
iconTooltip="Höchster Match Score"
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5 }}>
|
<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}
|
{item.matchScore}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">/100</Typography>
|
<Typography variant="caption" color="text.secondary">/100</Typography>
|
||||||
@@ -172,9 +172,9 @@ export function CompareTableBody({
|
|||||||
const sig = getSig(item)
|
const sig = getSig(item)
|
||||||
if (prop) return <Typography variant="body2">{prop.availabilityDate}</Typography>
|
if (prop) return <Typography variant="body2">{prop.availabilityDate}</Typography>
|
||||||
if (sig) return (
|
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="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
|
{Math.round(sig.probability * 100)}% Wahrscheinlichkeit
|
||||||
</Typography>
|
</Typography>
|
||||||
</CompareCell>
|
</CompareCell>
|
||||||
@@ -305,9 +305,9 @@ export function CompareTableBody({
|
|||||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>Nicht anwendbar</Typography>
|
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>Nicht anwendbar</Typography>
|
||||||
)
|
)
|
||||||
return (
|
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 }}>
|
<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
|
{Math.round(sig.probability * 100)}% Wahrscheinlichkeit
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
|
|||||||
import { Box, LinearProgress, Typography } from '@mui/material'
|
import { Box, LinearProgress, Typography } from '@mui/material'
|
||||||
import type { WeightingKey } from '../../domain/needBuilder'
|
import type { WeightingKey } from '../../domain/needBuilder'
|
||||||
import type { UnifiedMatchResult, VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult'
|
import type { UnifiedMatchResult, VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult'
|
||||||
|
import { dataQualityHex } from '../../lib/utils'
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
|
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) {
|
export function getProp(item: UnifiedMatchResult) {
|
||||||
return item.resultType !== 'FUTURE_AVAILABILITY'
|
return item.resultType !== 'FUTURE_AVAILABILITY'
|
||||||
@@ -20,13 +20,6 @@ export function getSig(item: UnifiedMatchResult) {
|
|||||||
: null
|
: 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 RISK_LEVEL_ORDER: Record<string, number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||||
|
|
||||||
export const CRITERION_ALIASES: Record<WeightingKey, string[]> = {
|
export const CRITERION_ALIASES: Record<WeightingKey, string[]> = {
|
||||||
@@ -78,7 +71,7 @@ export const DATA_SX = {
|
|||||||
// ── Score bar helper ──────────────────────────────────────────────────────────
|
// ── Score bar helper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function scoreBar(value: number, label?: string): ReactNode {
|
export function scoreBar(value: number, label?: string): ReactNode {
|
||||||
const color = value >= 0.8 ? '#1a7a4a' : value >= 0.6 ? '#d97706' : '#c0392b'
|
const color = dataQualityHex(value)
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Box sx={{ width: 70 }}>
|
<Box sx={{ width: 70 }}>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { Box, Chip, Typography } from '@mui/material'
|
import { Box, Chip, Typography } from '@mui/material'
|
||||||
import { Kanban } from 'lucide-react'
|
import { Kanban } from 'lucide-react'
|
||||||
|
import { INQUIRY_STATUS_META, DS_COLORS } from '../../lib/ds'
|
||||||
import type { Inquiry } from '../../domain/inquiry'
|
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 {
|
interface AnfragenInquiryItemProps {
|
||||||
inq: Inquiry
|
inq: Inquiry
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
@@ -17,7 +11,7 @@ interface AnfragenInquiryItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }: 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 displayDate = new Date(inq.updatedAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit' })
|
||||||
const lastMsg = inq.thread[inq.thread.length - 1]
|
const lastMsg = inq.thread[inq.thread.length - 1]
|
||||||
|
|
||||||
@@ -26,21 +20,22 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
|||||||
px: 2, py: 1.5,
|
px: 2, py: 1.5,
|
||||||
borderBottom: '1px solid #f1f5f9',
|
borderBottom: '1px solid #f1f5f9',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
bgcolor: isSelected ? '#eff6ff' : !inq.isRead ? 'rgba(220,38,38,0.03)' : 'transparent',
|
bgcolor: isSelected ? DS_COLORS.futureCard.signal.headerBg : !inq.isRead ? 'rgba(220,38,38,0.03)' : 'transparent',
|
||||||
borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent',
|
borderLeft: isSelected ? '3px solid' : '3px solid transparent',
|
||||||
'&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' },
|
borderLeftColor: isSelected ? 'primary.main' : 'transparent',
|
||||||
|
'&:hover': { bgcolor: isSelected ? DS_COLORS.futureCard.signal.headerBg : '#f8fafc' },
|
||||||
transition: 'background-color 0.1s ease',
|
transition: 'background-color 0.1s ease',
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.375 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.375 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
|
<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>
|
<Typography variant="body2" sx={{ fontWeight: !inq.isRead ? 700 : 500, fontSize: '0.8125rem' }} noWrap>
|
||||||
{inq.tenantName}
|
{inq.tenantName}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
|
||||||
{inq.unreadCount > 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>
|
<Typography sx={{ color: 'white', fontSize: '0.6rem', fontWeight: 700 }}>{inq.unreadCount}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -63,7 +58,7 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
|||||||
)}
|
)}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Chip label={cfg?.label ?? inq.status} size="small"
|
<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 && (
|
{inq.matchScore && (
|
||||||
<Typography variant="caption" sx={{ color: inq.matchScore >= 80 ? '#1a7a4a' : '#d97706', fontWeight: 700, fontSize: '0.7rem' }}>
|
<Typography variant="caption" sx={{ color: inq.matchScore >= 80 ? '#1a7a4a' : '#d97706', fontWeight: 700, fontSize: '0.7rem' }}>
|
||||||
{inq.matchScore}%
|
{inq.matchScore}%
|
||||||
@@ -74,7 +69,12 @@ export function AnfragenInquiryItem({ inq, isSelected, hasPipeline, onSelect }:
|
|||||||
icon={<Kanban size={9} />}
|
icon={<Kanban size={9} />}
|
||||||
label="Pipeline"
|
label="Pipeline"
|
||||||
size="small"
|
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>
|
</Box>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import { Bot, Building2, FileText } from 'lucide-react'
|
import { Bot, Building2, FileText } from 'lucide-react'
|
||||||
|
import { DS_COLORS } from '../../lib/ds'
|
||||||
import type { InquiryMessage } from '../../domain/inquiry'
|
import type { InquiryMessage } from '../../domain/inquiry'
|
||||||
|
|
||||||
export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
|
export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
|
||||||
@@ -22,15 +23,15 @@ export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
|
|||||||
maxWidth: { xs: '88%', md: '72%' },
|
maxWidth: { xs: '88%', md: '72%' },
|
||||||
px: 2, py: 1.25,
|
px: 2, py: 1.25,
|
||||||
borderRadius: isOwnMessage ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
borderRadius: isOwnMessage ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
||||||
bgcolor: isOwnMessage ? '#1e3a5f' : isAI ? '#f5f3ff' : 'white',
|
bgcolor: isOwnMessage ? 'primary.main' : isAI ? DS_COLORS.futureCard.controlled.headerBg : 'white',
|
||||||
border: isOwnMessage ? 'none' : isAI ? '1px solid #ddd6fe' : '1px solid #e2e8f0',
|
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)',
|
boxShadow: isOwnMessage ? '0 2px 6px rgba(30,58,95,0.2)' : '0 1px 2px rgba(0,0,0,0.06)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
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',
|
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 { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||||
import { floorLabel, SOURCE_META, ASSET_TYPE_LABELS, getOpportunityHeadline } from './futureAvailabilityCardHelpers'
|
import { floorLabel, SOURCE_META, ASSET_TYPE_LABELS, getOpportunityHeadline } from './futureAvailabilityCardHelpers'
|
||||||
import { SignalQualityDots } from './SignalQualityDots'
|
import { SignalQualityDots } from './SignalQualityDots'
|
||||||
|
import { DS_COLORS } from '../../lib/ds'
|
||||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||||
|
|
||||||
// ── Future Availability Card ──────────────────────────────────────────────────
|
// ── Future Availability Card ──────────────────────────────────────────────────
|
||||||
@@ -12,13 +13,12 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
|||||||
const isControlled = vm.signalIsControlled ?? false
|
const isControlled = vm.signalIsControlled ?? false
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// PRE-MARKET VERIFIED: soft purple / institutional premium
|
const cardColors = isControlled ? DS_COLORS.futureCard.controlled : DS_COLORS.futureCard.signal
|
||||||
// MARKET SIGNAL: slate blue / analytical
|
const accentColor = cardColors.accent
|
||||||
const accentColor = isControlled ? '#7c3aed' : '#1d4ed8'
|
const headerBg = cardColors.headerBg
|
||||||
const headerBg = isControlled ? '#faf5ff' : '#eff6ff'
|
const borderColor = cardColors.border
|
||||||
const borderColor = isControlled ? '#e9d5ff' : '#bfdbfe'
|
const badgeBg = cardColors.badgeBg
|
||||||
const badgeBg = isControlled ? '#ede9fe' : '#dbeafe'
|
const badgeColor = cardColors.badgeText
|
||||||
const badgeColor = isControlled ? '#5b21b6' : '#1e40af'
|
|
||||||
|
|
||||||
const assetLabel = vm.assetType ? (ASSET_TYPE_LABELS[vm.assetType] ?? vm.assetType) : 'Fläche'
|
const assetLabel = vm.assetType ? (ASSET_TYPE_LABELS[vm.assetType] ?? vm.assetType) : 'Fläche'
|
||||||
const headline = getOpportunityHeadline(vm.signalType, assetLabel)
|
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 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.35 }}>
|
||||||
{(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => (
|
{(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => (
|
||||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.6 }}>
|
<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>
|
<Typography sx={{ fontSize: '0.72rem', color: '#475569', lineHeight: 1.3 }}>{ind}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
@@ -210,7 +210,7 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
|||||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||||
...(a.variant === 'primary' && {
|
...(a.variant === 'primary' && {
|
||||||
bgcolor: accentColor,
|
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}` : ''}`)}
|
onClick={() => navigate(`/demand/property/${vm.propertyId}${vm.unitId ? `?unit=${vm.unitId}` : ''}`)}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
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 →
|
Zur Einheit →
|
||||||
|
|||||||
@@ -7,18 +7,10 @@ import { useSessionStore } from '../../stores/sessionStore'
|
|||||||
import { LocationPreview } from '../shared/LocationPreview'
|
import { LocationPreview } from '../shared/LocationPreview'
|
||||||
import { HeatBadge } from '../shared/HeatBadge'
|
import { HeatBadge } from '../shared/HeatBadge'
|
||||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||||
|
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||||
import { FutureAvailabilityCard } from './FutureAvailabilityCard'
|
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 ────────────────────────────────────────────────────────────
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -163,10 +155,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
disabled={a.disabled}
|
disabled={a.disabled}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||||
...(a.variant === 'primary' && {
|
|
||||||
bgcolor: '#1e3a5f',
|
|
||||||
'&:hover': { bgcolor: '#162d4a' },
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{a.label}
|
{a.label}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
|
import { matchScoreHex } from '../../lib/utils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
score: number
|
score: number
|
||||||
size?: 'sm' | 'md' | 'lg'
|
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' }
|
const FONT_SIZES: Record<string, string> = { sm: '1.25rem', md: '1.75rem', lg: '2.5rem' }
|
||||||
|
|
||||||
export function MatchScoreDisplay({ score, size = 'md' }: Props) {
|
export function MatchScoreDisplay({ score, size = 'md' }: Props) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.25, flexShrink: 0 }}>
|
<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}
|
{score}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">/100</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 { CREDIBILITY_LABELS, HARD_KEYS, factorLabel } from './scoreBreakdownConstants'
|
||||||
import { CriterionRow } from './CriterionRow'
|
import { CriterionRow } from './CriterionRow'
|
||||||
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
|
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) ──────────────
|
// ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ──────────────
|
||||||
|
|
||||||
@@ -38,17 +40,17 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hard criteria */}
|
{/* 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'}
|
{hasAllFactors ? 'Hart-Kriterien' : 'Bewertungsfaktoren'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{hardFactors.map((f, i) => <CriterionRow key={i} factor={f} maxWeight={maxHardWeight} />)}
|
{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 }}>
|
<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%
|
Hart-Kriterien {sb.hardMatchScore}/100 × 60%
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1e3a5f' }}>
|
<Typography variant="caption" sx={{ fontWeight: 800, color: 'primary.main' }}>
|
||||||
{hardContrib} Pkt
|
{hardContrib} Pkt
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -120,7 +122,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
|
|||||||
{/* Total */}
|
{/* Total */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
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,
|
p: 1.5, borderRadius: 1,
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
|
||||||
@@ -161,7 +163,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
|
|||||||
<>
|
<>
|
||||||
{/* Schritt 1 — Kriterien-Match */}
|
{/* Schritt 1 — Kriterien-Match */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<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
|
Schritt 1 — Kriterien-Match
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.25, lineHeight: 1.4 }}>
|
<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 */}
|
{/* Schritt 2 — Verlässlichkeit */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<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
|
Schritt 2 — Verlässlichkeit des Signals
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Box, Tooltip, Typography } from '@mui/material'
|
import { Box, Tooltip, Typography } from '@mui/material'
|
||||||
import { Flame } from 'lucide-react'
|
import { Flame } from 'lucide-react'
|
||||||
import { getHeatLevel, heatTooltip } from '../../lib/propertyHeat'
|
import { getHeatLevel, heatTooltip } from '../../lib/propertyHeat'
|
||||||
|
import { DS_COLORS } from '../../lib/ds'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
propertyId: string | undefined
|
propertyId: string | undefined
|
||||||
@@ -13,27 +14,24 @@ export function HeatBadge({ propertyId, size = 'md' }: Props) {
|
|||||||
|
|
||||||
const isVeryHot = level === 'VERY_HOT'
|
const isVeryHot = level === 'VERY_HOT'
|
||||||
const tooltip = heatTooltip(propertyId)
|
const tooltip = heatTooltip(propertyId)
|
||||||
|
const colors = isVeryHot ? DS_COLORS.heat.VERY_HOT : DS_COLORS.heat.HOT
|
||||||
|
|
||||||
const iconSize = size === 'sm' ? 10 : 12
|
const iconSize = size === 'sm' ? 10 : 12
|
||||||
const fontSize = size === 'sm' ? '0.6rem' : '0.65rem'
|
const fontSize = size === 'sm' ? '0.6rem' : '0.65rem'
|
||||||
const label = isVeryHot ? 'Sehr gefragt' : 'Gefragt'
|
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'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltip} placement="top" arrow>
|
<Tooltip title={tooltip} placement="top" arrow>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'inline-flex', alignItems: 'center', gap: 0.35,
|
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,
|
px: size === 'sm' ? 0.6 : 0.75, py: 0.15,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flame size={iconSize} color={iconColor} fill={isVeryHot ? iconColor : 'none'} />
|
<Flame size={iconSize} color={colors.icon} fill={isVeryHot ? colors.icon : 'none'} />
|
||||||
<Typography sx={{ fontSize, fontWeight: 700, color: textColor, lineHeight: 1.2 }}>
|
<Typography sx={{ fontSize, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -42,8 +42,40 @@ export const DS_COLORS = {
|
|||||||
LOW: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' },
|
LOW: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' },
|
||||||
INCOMPLETE: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' },
|
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
|
} as const
|
||||||
|
|
||||||
|
// ── Inquiry status metadata ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const INQUIRY_STATUS_META: Record<string, { label: string; fg: string; bg: string }> = {
|
||||||
|
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 ────────────────────────────────────────
|
// ── Score → qualitative level helpers ────────────────────────────────────────
|
||||||
|
|
||||||
export function scoreToConfidenceLevel(score: number): ConfidenceLevel {
|
export function scoreToConfidenceLevel(score: number): ConfidenceLevel {
|
||||||
|
|||||||
@@ -12,16 +12,10 @@ import type { InquiryMessage } from '../../domain/inquiry'
|
|||||||
import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection'
|
import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection'
|
||||||
import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble'
|
import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble'
|
||||||
import { AnfragenInquiryItem } from '../../components/demand/AnfragenInquiryItem'
|
import { AnfragenInquiryItem } from '../../components/demand/AnfragenInquiryItem'
|
||||||
|
import { INQUIRY_STATUS_META, DS_COLORS } from '../../lib/ds'
|
||||||
|
|
||||||
// ── Config ────────────────────────────────────────────────────────────────────
|
// ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
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' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILTER_TABS = [
|
const FILTER_TABS = [
|
||||||
{ key: 'all', label: 'Alle' },
|
{ key: 'all', label: 'Alle' },
|
||||||
{ key: 'new', label: 'Neu' },
|
{ key: 'new', label: 'Neu' },
|
||||||
@@ -147,7 +141,7 @@ export default function Anfragen() {
|
|||||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>Anfragen</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>Anfragen</Typography>
|
||||||
{totalUnread > 0 && (
|
{totalUnread > 0 && (
|
||||||
<Chip label={`${totalUnread} neu`} size="small"
|
<Chip label={`${totalUnread} neu`} size="small"
|
||||||
sx={{ bgcolor: '#dc2626', color: 'white', fontWeight: 700, height: 20, fontSize: '0.7rem' }} />
|
sx={{ bgcolor: INQUIRY_STATUS_META.new.fg, color: 'white', fontWeight: 700, height: 20, fontSize: '0.7rem' }} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -161,10 +155,10 @@ export default function Anfragen() {
|
|||||||
<Chip key={tab.key} label={tab.label} size="small" onClick={() => setStatusFilter(tab.key)}
|
<Chip key={tab.key} label={tab.label} size="small" onClick={() => setStatusFilter(tab.key)}
|
||||||
sx={{
|
sx={{
|
||||||
height: 22, fontSize: '0.7rem', cursor: 'pointer',
|
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',
|
color: statusFilter === tab.key ? 'white' : '#475569',
|
||||||
fontWeight: statusFilter === tab.key ? 700 : 400,
|
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() {
|
|||||||
<IconButton size="small" sx={{ display: { md: 'none' }, mr: -0.5 }} onClick={() => setMobileShowChat(false)}>
|
<IconButton size="small" sx={{ display: { md: 'none' }, mr: -0.5 }} onClick={() => setMobileShowChat(false)}>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Avatar sx={{ width: 36, height: 36, bgcolor: '#1e3a5f', fontSize: '0.8rem', flexShrink: 0 }}>
|
<Avatar sx={{ width: 36, height: 36, bgcolor: 'primary.main', fontSize: '0.8rem', flexShrink: 0 }}>
|
||||||
{selected.tenantName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
{selected.tenantName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
@@ -232,9 +226,9 @@ export default function Anfragen() {
|
|||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
<Chip
|
<Chip
|
||||||
label={STATUS_CONFIG[selected.status ?? 'new']?.label ?? selected.status}
|
label={INQUIRY_STATUS_META[selected.status ?? 'new']?.label ?? selected.status}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ bgcolor: STATUS_CONFIG[selected.status ?? 'new']?.bgColor, color: STATUS_CONFIG[selected.status ?? 'new']?.color, fontWeight: 600, height: 22, fontSize: '0.75rem' }}
|
sx={{ bgcolor: INQUIRY_STATUS_META[selected.status ?? 'new']?.bg, color: INQUIRY_STATUS_META[selected.status ?? 'new']?.fg, fontWeight: 600, height: 22, fontSize: '0.75rem' }}
|
||||||
/>
|
/>
|
||||||
{/* Pipeline link */}
|
{/* Pipeline link */}
|
||||||
{linkedPipelineItem && (
|
{linkedPipelineItem && (
|
||||||
@@ -245,10 +239,11 @@ export default function Anfragen() {
|
|||||||
onClick={() => navigate('/demand/pipeline')}
|
onClick={() => navigate('/demand/pipeline')}
|
||||||
sx={{
|
sx={{
|
||||||
height: 22, fontSize: '0.75rem', cursor: 'pointer',
|
height: 22, fontSize: '0.75rem', cursor: 'pointer',
|
||||||
bgcolor: '#eff6ff', color: '#1e3a5f', fontWeight: 600,
|
bgcolor: DS_COLORS.futureCard.signal.headerBg, fontWeight: 600,
|
||||||
border: '1px solid #bfdbfe',
|
color: 'primary.main',
|
||||||
'& .MuiChip-icon': { color: '#1e3a5f' },
|
border: `1px solid ${DS_COLORS.futureCard.signal.border}`,
|
||||||
'&:hover': { bgcolor: '#dbeafe' },
|
'& .MuiChip-icon': { color: 'primary.main' },
|
||||||
|
'&:hover': { bgcolor: DS_COLORS.futureCard.signal.badgeBg },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -273,7 +268,7 @@ export default function Anfragen() {
|
|||||||
severity="info"
|
severity="info"
|
||||||
icon={<Bot size={16} />}
|
icon={<Bot size={16} />}
|
||||||
onClose={() => setKiAlert(null)}
|
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 } }}
|
||||||
>
|
>
|
||||||
<strong>KI erkannt:</strong> „{kiAlert.title}" wurde in der Pipeline auf <strong>{kiAlert.stage}</strong> verschoben.{' '}
|
<strong>KI erkannt:</strong> „{kiAlert.title}" wurde in der Pipeline auf <strong>{kiAlert.stage}</strong> verschoben.{' '}
|
||||||
<Box component="span" sx={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate('/demand/pipeline')}>
|
<Box component="span" sx={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate('/demand/pipeline')}>
|
||||||
@@ -304,7 +299,7 @@ export default function Anfragen() {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||||
<IconButton size="small" sx={{ color: '#94a3b8' }}><Paperclip size={16} /></IconButton>
|
<IconButton size="small" sx={{ color: '#94a3b8' }}><Paperclip size={16} /></IconButton>
|
||||||
<IconButton size="small" onClick={handleSend} disabled={!replyText.trim()}
|
<IconButton size="small" onClick={handleSend} disabled={!replyText.trim()}
|
||||||
sx={{ bgcolor: '#1e3a5f', color: 'white', '&:hover': { bgcolor: '#1a3050' }, '&:disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' } }}>
|
sx={{ bgcolor: 'primary.main', color: 'white', '&:hover': { bgcolor: 'primary.dark' }, '&:disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' } }}>
|
||||||
<Send size={16} />
|
<Send size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user