feat: Pipeline↔Anfragen integration + Compare→Pipeline + KI stage detection
Navigation: Deal Pipeline moved after Vergleich (before Anfragen) Compare → Pipeline: - Bookmark icon per column header; BookmarkCheck when already in pipeline - Passes propertyId, propertyAddress, area/rent labels on save Pipeline cards now unit-level: - propertyAddress shown with MapPin on every card - Chat icon (MessageSquare) on cards with linked inquiry → navigates to /demand/anfragen?inquiry=xxx - Detail panel: Chat chip links to specific inquiry thread, propertyAddress displayed Anfragen → Pipeline KI detection: - Keyword scan on every sent message (besichtigung → VISITED, mietvertrag → NEGOTIATION, unterschrieben → CLOSED_WON) - Only advances stage, never goes back - Purple KI alert banner with direct Pipeline link, auto-dismisses after 6s - Pipeline badge in inquiry list + stage chip in chat header with nav link - URL param ?inquiry=xxx pre-selects inquiry (used from Pipeline chat button) Domain: PipelineItem gains propertyId, unitId, propertyAddress, inquiryId Mock data: pl-001/pl-002/pl-004 linked to inq-001/inq-005/inq-004 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import {
|
||||
Box, Typography, Chip, Paper, Button, IconButton, TextField, Divider,
|
||||
Box, Typography, Chip, Paper, Button, IconButton, TextField, Divider, Tooltip,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
DndContext, DragOverlay, PointerSensor, useSensor, useSensors,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
} from '@dnd-kit/core'
|
||||
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { X, Sparkles, FileText, StickyNote, ChevronRight, TrendingUp, AlertTriangle, CheckCircle, Bookmark } from 'lucide-react'
|
||||
import { X, Sparkles, FileText, StickyNote, ChevronRight, TrendingUp, AlertTriangle, CheckCircle, MessageSquare, MapPin } from 'lucide-react'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import { AddToPipelineDialog } from '../../components/shortlist'
|
||||
import type { PipelineItem, PipelineStage } from '../../domain/pipeline'
|
||||
@@ -107,11 +108,13 @@ function DraggableCard({
|
||||
isSelected,
|
||||
onSelect,
|
||||
isDragOverlay = false,
|
||||
onChatClick,
|
||||
}: {
|
||||
item: PipelineItem
|
||||
isSelected: boolean
|
||||
onSelect: (item: PipelineItem) => void
|
||||
isDragOverlay?: boolean
|
||||
onChatClick?: (e: React.MouseEvent) => void
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: item.id })
|
||||
|
||||
@@ -144,17 +147,29 @@ function DraggableCard({
|
||||
}}
|
||||
{...(isDragOverlay ? {} : { ...attributes, ...listeners })}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 1, mb: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 1, mb: 0.375 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, flex: 1, minWidth: 0, lineHeight: 1.3 }} noWrap>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 900, fontSize: '0.9rem', color: scoreColor(item.matchScore), flexShrink: 0 }}>
|
||||
{item.matchScore}%
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, flexShrink: 0 }}>
|
||||
{item.inquiryId && onChatClick && !isDragOverlay && (
|
||||
<Tooltip title="Chat öffnen">
|
||||
<IconButton size="small" onClick={onChatClick} sx={{ p: 0.25, color: '#1e3a5f', '&:hover': { bgcolor: '#eff6ff' } }}>
|
||||
<MessageSquare size={13} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Typography sx={{ fontWeight: 900, fontSize: '0.9rem', color: scoreColor(item.matchScore) }}>
|
||||
{item.matchScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<MapPin size={10} color="#94a3b8" />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
{item.propertyAddress ?? item.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
||||
{item.location}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
size="small"
|
||||
@@ -180,12 +195,14 @@ function DroppableColumn({
|
||||
items,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onChatClick,
|
||||
isOver,
|
||||
}: {
|
||||
stage: typeof STAGES[number]
|
||||
items: PipelineItem[]
|
||||
selectedId: string | null
|
||||
onSelect: (item: PipelineItem) => void
|
||||
onChatClick: (inquiryId: string) => void
|
||||
isOver: boolean
|
||||
}) {
|
||||
const { setNodeRef } = useDroppable({ id: stage.key })
|
||||
@@ -211,6 +228,7 @@ function DroppableColumn({
|
||||
item={item}
|
||||
isSelected={item.id === selectedId}
|
||||
onSelect={onSelect}
|
||||
onChatClick={item.inquiryId ? (e) => { e.stopPropagation(); onChatClick(item.inquiryId!) } : undefined}
|
||||
/>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
@@ -227,6 +245,7 @@ function DroppableColumn({
|
||||
// ── DetailPanel ───────────────────────────────────────────────────────────────
|
||||
|
||||
function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => void }) {
|
||||
const navigate = useNavigate()
|
||||
const { moveStage, updateNotes, loseItem } = usePipelineStore()
|
||||
const [notes, setNotes] = useState(item.notes ?? '')
|
||||
const stageConfig = STAGES.find(s => s.key === item.stage)!
|
||||
@@ -265,8 +284,33 @@ function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => voi
|
||||
<Box key={s.key} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: idx <= progressIdx ? stageConfig.color : '#e2e8f0' }} />
|
||||
))}
|
||||
</Box>
|
||||
<Chip label={stageConfig.label} size="small" sx={{ bgcolor: stageConfig.bgColor, color: stageConfig.color, fontWeight: 700, height: 22, fontSize: '0.75rem' }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip label={stageConfig.label} size="small" sx={{ bgcolor: stageConfig.bgColor, color: stageConfig.color, fontWeight: 700, height: 22, fontSize: '0.75rem' }} />
|
||||
{item.inquiryId && (
|
||||
<Chip
|
||||
icon={<MessageSquare size={11} />}
|
||||
label="Chat"
|
||||
size="small"
|
||||
onClick={() => navigate(`/demand/anfragen?inquiry=${item.inquiryId}`)}
|
||||
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' },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Property / unit address */}
|
||||
{item.propertyAddress && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 1.25 }}>
|
||||
<MapPin size={12} color="#64748b" />
|
||||
<Typography variant="caption" color="text.secondary">{item.propertyAddress}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
@@ -391,6 +435,7 @@ function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => voi
|
||||
// ── Pipeline page ─────────────────────────────────────────────────────────────
|
||||
|
||||
export default function Pipeline() {
|
||||
const navigate = useNavigate()
|
||||
const { items, moveStage } = usePipelineStore()
|
||||
const [selectedItem, setSelectedItem] = useState<PipelineItem | null>(null)
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
@@ -492,6 +537,7 @@ export default function Pipeline() {
|
||||
items={columnItems}
|
||||
selectedId={syncedSelected?.id ?? null}
|
||||
onSelect={handleSelect}
|
||||
onChatClick={(inquiryId) => navigate(`/demand/anfragen?inquiry=${inquiryId}`)}
|
||||
isOver={isOver}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user