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,5 +1,6 @@
|
||||
import { Box, Chip, IconButton, Tooltip, Typography } from '@mui/material'
|
||||
import { X, AlertTriangle } from 'lucide-react'
|
||||
import { X, AlertTriangle, Bookmark, BookmarkCheck } from 'lucide-react'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||
|
||||
const TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
@@ -17,30 +18,70 @@ interface Props {
|
||||
}
|
||||
|
||||
export function CompareColumnHeader({ item, onRemove }: Props) {
|
||||
const { items: pipelineItems, openSavedDialog } = usePipelineStore()
|
||||
|
||||
const meta = 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
|
||||
|
||||
const title = prop?.title ?? sig?.companyName ?? sig?.locationHint ?? '–'
|
||||
const subtitle = prop?.location?.city ?? sig?.locationHint ?? '–'
|
||||
const title = prop?.title ?? sig?.companyName ?? sig?.locationHint ?? '–'
|
||||
const subtitle = prop?.location?.city ?? sig?.locationHint ?? '–'
|
||||
const district = prop?.location?.district
|
||||
const availability = prop?.availabilityDate ?? (sig ? `~${sig.timeHorizonMonths} Monate` : null)
|
||||
const confidence = Math.round(item.match.confidenceLevel * 100)
|
||||
const source = prop?.sourceLabel ?? sig?.source?.type ?? '–'
|
||||
const confidence = Math.round(item.match.confidenceLevel * 100)
|
||||
const source = prop?.sourceLabel ?? sig?.source?.type ?? '–'
|
||||
|
||||
const isInPipeline = pipelineItems.some(
|
||||
pi => pi.id === item.matchId || (prop?.id && pi.propertyId === prop.id)
|
||||
)
|
||||
|
||||
function handleSave() {
|
||||
if (isInPipeline) return
|
||||
openSavedDialog({
|
||||
resultId: item.matchId,
|
||||
resultType: item.resultType,
|
||||
title,
|
||||
location: prop?.location?.city ?? sig?.locationHint,
|
||||
matchScore: item.matchScore,
|
||||
propertyId: prop?.id,
|
||||
propertyAddress: prop ? `${title}, ${subtitle}${district ? `, ${district}` : ''}` : undefined,
|
||||
areaLabel: prop?.areaSqm ? `${prop.areaSqm.toLocaleString('de-CH')} m²` : undefined,
|
||||
rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²` : undefined,
|
||||
availabilityLabel: prop?.availabilityDate ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||
<Chip label={meta.label} size="small" sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 10 }} />
|
||||
<IconButton size="small" onClick={onRemove} sx={{ p: 0.25, ml: 1 }}>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<Tooltip title={isInPipeline ? 'Bereits in Pipeline' : 'In Pipeline merken'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
p: 0.25,
|
||||
color: isInPipeline ? '#1a7a4a' : '#94a3b8',
|
||||
'&:hover': { color: isInPipeline ? '#1a7a4a' : '#1e3a5f' },
|
||||
}}
|
||||
>
|
||||
{isInPipeline
|
||||
? <BookmarkCheck size={14} />
|
||||
: <Bookmark size={14} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton size="small" onClick={onRemove} sx={{ p: 0.25, ml: 0.25 }}>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.25, lineHeight: 1.3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
|
||||
{subtitle}
|
||||
{subtitle}{district ? `, ${district}` : ''}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 0.5 }}>
|
||||
|
||||
Reference in New Issue
Block a user