da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
7.6 KiB
TypeScript
194 lines
7.6 KiB
TypeScript
import { useNavigate } from 'react-router'
|
|
import { Box, Button, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'
|
|
import { useDraggable } from '@dnd-kit/core'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import { ExternalLink, MapPin, MessageSquare, Send, MessagesSquare } from 'lucide-react'
|
|
import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay'
|
|
import { HeatBadge } from '../shared'
|
|
import type { PipelineItem } from '../../domain/pipeline'
|
|
import { STAGES, RESULT_TYPE_META } from './pipelineConstants'
|
|
import { detailPath } from './pipelineUtils'
|
|
import { useInquiryStore } from '../../stores/inquiryStore'
|
|
|
|
// ── 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 openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
|
|
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: item.id })
|
|
const stageConfig = STAGES.find(s => s.key === item.stage) ?? STAGES[0]
|
|
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 ? '#152642' : '#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_META[item.resultType]?.label ?? item.resultType}
|
|
sx={{ bgcolor: RESULT_TYPE_META[item.resultType]?.color ?? '#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: '#152642', '&:hover': { bgcolor: '#eff6ff' } }}>
|
|
<MessageSquare size={13} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{path && (
|
|
<Tooltip title="Match öffnen">
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e) => { e.stopPropagation(); navigate(path) }}
|
|
sx={{ p: 0.25, color: '#94a3b8', '&:hover': { color: '#152642', 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>
|
|
)}
|
|
|
|
{/* Row 6: Anfrage / Konversation */}
|
|
{!isDragOverlay && (
|
|
<Box sx={{ mt: 1, pt: 0.75, borderTop: '1px solid #f1f5f9' }}>
|
|
{item.stage !== 'SAVED' || item.inquiryId ? (
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
startIcon={<MessagesSquare size={11} />}
|
|
fullWidth
|
|
onMouseDown={e => e.stopPropagation()}
|
|
onClick={e => {
|
|
e.stopPropagation()
|
|
navigate(`/demand/anfragen?inquiry=${item.inquiryId}`)
|
|
}}
|
|
sx={{
|
|
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
|
borderColor: '#152642', color: '#152642',
|
|
'&:hover': { bgcolor: '#eff6ff', borderColor: '#152642' },
|
|
}}
|
|
>
|
|
Zur Konversation
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
startIcon={<Send size={11} />}
|
|
fullWidth
|
|
onMouseDown={e => e.stopPropagation()}
|
|
onClick={e => {
|
|
e.stopPropagation()
|
|
openInquiryDialog({
|
|
propertyTitle: item.title,
|
|
location: item.propertyAddress ?? item.location,
|
|
areaLabel: item.areaLabel,
|
|
rentLabel: item.rentLabel,
|
|
matchScore: item.matchScore,
|
|
pipelineItemId: item.id,
|
|
})
|
|
}}
|
|
sx={{
|
|
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
|
bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' },
|
|
}}
|
|
>
|
|
Anfrage senden
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
)
|
|
}
|