refactor: split large page components + taxonomy/HeatBadge/FutureAvailability improvements
- Taxonomy: merge VERIFIED_PORTFOLIO + EXTERNAL_MARKET display → 'Plattform' (dark blue) across all surfaces - HeatBadge: new flame indicator for hot properties (grid, list, pipeline views) - FutureAvailabilityContextPanel: richer detail page with AI summary, strategic assessment, sources - Refactor Pipeline.tsx (630→152 lines) → pipeline/PipelineCard, PipelineColumn, PipelineDetailPanel, pipelineConstants, pipelineUtils - Refactor IntelligenceMatchCard.tsx (483→179 lines) → FutureAvailabilityCard extracted - Refactor MatchDetail.tsx (559→464 lines) → useMatchDetailData hook, MatchDetailPropertyDetails - Refactor Compare.tsx (638→485 lines) → compareUtils, CompareCriteriaCard extracted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Box, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { ExternalLink, MapPin, MessageSquare } from 'lucide-react'
|
||||
import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay'
|
||||
import { HeatBadge } from '../shared'
|
||||
import type { PipelineItem } from '../../domain/pipeline'
|
||||
import { STAGES, RESULT_TYPE_LABEL, RESULT_TYPE_COLOR } from './pipelineConstants'
|
||||
import { detailPath } from './pipelineUtils'
|
||||
|
||||
// ── DraggableCard ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function DraggableCard({
|
||||
item,
|
||||
isSelected,
|
||||
onSelect,
|
||||
isDragOverlay = false,
|
||||
onChatClick,
|
||||
}: {
|
||||
item: PipelineItem
|
||||
isSelected: boolean
|
||||
onSelect: (item: PipelineItem) => void
|
||||
isDragOverlay?: boolean
|
||||
onChatClick?: (e: React.MouseEvent) => void
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: item.id })
|
||||
const stageConfig = STAGES.find(s => s.key === item.stage)!
|
||||
const path = detailPath(item)
|
||||
|
||||
const style = !isDragOverlay ? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
opacity: isDragging ? 0.35 : 1,
|
||||
transition: isDragging ? undefined : 'opacity 0.15s ease',
|
||||
} : undefined
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={!isDragOverlay ? setNodeRef : undefined}
|
||||
style={style}
|
||||
elevation={isDragOverlay ? 6 : 0}
|
||||
onClick={() => !isDragging && onSelect(item)}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
border: isSelected && !isDragOverlay
|
||||
? '2px solid #1e3a5f'
|
||||
: isDragOverlay
|
||||
? '2px solid transparent'
|
||||
: '2px solid transparent',
|
||||
cursor: isDragOverlay ? 'grabbing' : 'grab',
|
||||
bgcolor: isDragOverlay ? 'white' : isSelected ? '#eff6ff' : 'white',
|
||||
boxShadow: isDragOverlay ? '0 8px 24px rgba(0,0,0,0.18)' : '0 1px 3px rgba(0,0,0,0.08)',
|
||||
'&:hover': isDragOverlay ? {} : {
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
borderColor: isSelected ? '#1e3a5f' : '#bfdbfe',
|
||||
},
|
||||
transition: isDragOverlay ? undefined : 'box-shadow 0.15s, border-color 0.15s',
|
||||
userSelect: 'none',
|
||||
rotate: isDragOverlay ? '2deg' : undefined,
|
||||
}}
|
||||
{...(isDragOverlay ? {} : { ...attributes, ...listeners })}
|
||||
>
|
||||
{/* Row 1: Score + type chip + chat icon */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 0.5, mb: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flex: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<MatchScoreDisplay score={item.matchScore} size="sm" />
|
||||
<HeatBadge propertyId={item.propertyId} size="sm" />
|
||||
<Chip
|
||||
size="small"
|
||||
label={RESULT_TYPE_LABEL[item.resultType] ?? item.resultType}
|
||||
sx={{ bgcolor: RESULT_TYPE_COLOR[item.resultType] ?? '#475569', color: 'white', fontSize: 10, height: 20, fontWeight: 600 }}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={stageConfig.label}
|
||||
sx={{ bgcolor: stageConfig.bgColor, color: stageConfig.color, fontSize: 10, height: 20, fontWeight: 600 }}
|
||||
/>
|
||||
</Box>
|
||||
{!isDragOverlay && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.125, flexShrink: 0 }}>
|
||||
{item.inquiryId && onChatClick && (
|
||||
<Tooltip title="Chat öffnen">
|
||||
<IconButton size="small" onClick={onChatClick} sx={{ p: 0.25, color: '#1e3a5f', '&:hover': { bgcolor: '#eff6ff' } }}>
|
||||
<MessageSquare size={13} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{path && (
|
||||
<Tooltip title="Objekt öffnen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); navigate(path) }}
|
||||
sx={{ p: 0.25, color: '#94a3b8', '&:hover': { color: '#1e3a5f', bgcolor: '#eff6ff' } }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Row 2: Title */}
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, lineHeight: 1.3, mb: 0.25 }} noWrap>
|
||||
{item.title}
|
||||
</Typography>
|
||||
|
||||
{/* Row 3: Location */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<MapPin size={11} color="#64748b" />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }} noWrap>
|
||||
{item.propertyAddress ?? item.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Row 4: Area + rent */}
|
||||
{(item.areaLabel || item.rentLabel) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
{item.areaLabel && (
|
||||
<Typography variant="caption" sx={{ color: '#475569', fontSize: '0.72rem' }}>{item.areaLabel}</Typography>
|
||||
)}
|
||||
{item.areaLabel && item.rentLabel && (
|
||||
<Box sx={{ width: 3, height: 3, borderRadius: '50%', bgcolor: '#cbd5e1' }} />
|
||||
)}
|
||||
{item.rentLabel && (
|
||||
<Typography variant="caption" sx={{ color: '#475569', fontSize: '0.72rem' }}>{item.rentLabel}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Row 5: Notes preview */}
|
||||
{item.notes && (
|
||||
<Typography variant="caption" sx={{ fontStyle: 'italic', color: '#94a3b8', mt: 0.5, display: 'block', fontSize: '0.7rem' }} noWrap>
|
||||
{item.notes}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user