diff --git a/src/components/compare/CompareColumnHeader.tsx b/src/components/compare/CompareColumnHeader.tsx
index d055827..9870707 100644
--- a/src/components/compare/CompareColumnHeader.tsx
+++ b/src/components/compare/CompareColumnHeader.tsx
@@ -1,5 +1,6 @@
-import { Box, Chip, IconButton, Tooltip, Typography } from '@mui/material'
-import { X, AlertTriangle, Bookmark, BookmarkCheck } from 'lucide-react'
+import { Box, Button, Chip, IconButton, Tooltip, Typography } from '@mui/material'
+import { X, AlertTriangle, BookmarkCheck, ExternalLink, Kanban } from 'lucide-react'
+import { useNavigate } from 'react-router'
import { usePipelineStore } from '../../stores/pipelineStore'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
@@ -18,6 +19,7 @@ interface Props {
}
export function CompareColumnHeader({ item, onRemove }: Props) {
+ const navigate = useNavigate()
const { items: pipelineItems, openSavedDialog } = usePipelineStore()
const meta = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
@@ -53,45 +55,43 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
return (
-
+
+ {/* Row 1: type chip + remove */}
+
-
-
-
- {isInPipeline
- ?
- : }
-
-
-
-
+
+
+
-
+
-
+ {/* Row 2: title (clickable) */}
+ navigate(`/demand/results/${item.matchId}`)}
+ >
{title}
-
+
{subtitle}{district ? `, ${district}` : ''}
-
+ {/* Row 3: score */}
+
{item.matchScore}
/100
-
+ {/* Row 4: meta chips */}
+
{item.resultType === 'FUTURE_AVAILABILITY' && sig && (
-
+
Probabilistisches Signal
@@ -122,6 +122,41 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
)}
+
+ {/* Row 5: action buttons */}
+
+ }
+ onClick={() => navigate(`/demand/results/${item.matchId}`)}
+ sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5 }}
+ >
+ Details
+
+ {isInPipeline ? (
+ }
+ disabled
+ sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, color: '#1a7a4a', borderColor: '#86efac' }}
+ >
+ In Pipeline
+
+ ) : (
+ }
+ onClick={handleSave}
+ sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
+ >
+ Merken
+
+ )}
+
+
)
}
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx
index 6b842fb..18c8cca 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -46,10 +46,10 @@ import { OrganizationContextBadge } from './OrganizationContextBadge'
import { UserMenu } from './UserMenu'
import { NotificationButton } from './NotificationButton'
import { RightContextPanel } from './RightContextPanel'
-import { CompareTray } from './CompareTray'
-import { GlobalAIAssistantDrawer, GlobalAIAssistantButton } from '../assistant'
+import { GlobalAIAssistantDrawer } from '../assistant'
import { useAssistantStore } from '../../stores/assistantStore'
import { ToastProvider } from '../ui'
+import { useCompareStore } from '../../stores/compareStore'
// ---------------------------------------------------------------------------
// Types
@@ -181,6 +181,7 @@ function Sidebar({
orgName,
}: SidebarProps) {
const config = WORKSPACE_CONFIG[activeWorkspace]
+ const compareCount = useCompareStore(s => s.compareItems.length)
const width = collapsed ? 60 : 264
const visibleWorkspaces = WORKSPACE_ORDER.filter((ws) => allowedWorkspaces.includes(ws))
@@ -337,17 +338,30 @@ function Sidebar({
>
{!collapsed && (
-
- {item.label}
-
+
+
+ {item.label}
+
+ {item.path === '/demand/compare' && compareCount > 0 && (
+
+ {compareCount}
+
+ )}
+
)}
)}
@@ -605,9 +619,7 @@ export function AppShell() {
-
-
diff --git a/src/domain/pipeline.ts b/src/domain/pipeline.ts
index aed508f..97bf10d 100644
--- a/src/domain/pipeline.ts
+++ b/src/domain/pipeline.ts
@@ -7,6 +7,8 @@ export interface PipelineItem {
matchScore: number
resultType: 'VERIFIED_PORTFOLIO' | 'EXTERNAL_MARKET' | 'MAISON_WORK' | 'FUTURE_AVAILABILITY'
stage: PipelineStage
+ // Source match reference — used for navigating back to the detail page
+ matchId?: string
// Property / unit reference — always set for unit-level tracking
propertyId?: string
unitId?: string
diff --git a/src/mock-data/pipelineItems.ts b/src/mock-data/pipelineItems.ts
index c7f8e00..5110c4b 100644
--- a/src/mock-data/pipelineItems.ts
+++ b/src/mock-data/pipelineItems.ts
@@ -36,6 +36,8 @@ export const mockPipelineItems: PipelineItem[] = [
},
{
id: 'pl-003',
+ matchId: 'match-005',
+ propertyId: 'prop-003',
propertyAddress: 'Binzstrasse 23, Zürich-Binz',
title: 'Büro Binzstrasse 23',
location: 'Zürich, Binz',
@@ -65,6 +67,8 @@ export const mockPipelineItems: PipelineItem[] = [
},
{
id: 'pl-005',
+ matchId: 'match-060',
+ propertyId: 'prop-042',
propertyAddress: 'Technopark, Zürich-West',
title: 'DataCloud Systems AG',
location: 'Zürich-West / Technopark',
@@ -78,6 +82,8 @@ export const mockPipelineItems: PipelineItem[] = [
},
{
id: 'pl-006',
+ matchId: 'match-061',
+ propertyId: 'prop-013',
propertyAddress: 'Wankdorf Business Park, Bern',
title: 'Neubau Wankdorf Business',
location: 'Bern, Wankdorf',
@@ -91,6 +97,8 @@ export const mockPipelineItems: PipelineItem[] = [
},
{
id: 'pl-007',
+ matchId: 'match-015',
+ propertyId: 'prop-015',
propertyAddress: 'Stadthaus-Gasse 1, Bern Innenstadt',
title: 'Bürofläche Stadthaus Bern',
location: 'Bern Innenstadt',
@@ -107,6 +115,8 @@ export const mockPipelineItems: PipelineItem[] = [
},
{
id: 'pl-008',
+ matchId: 'match-002',
+ propertyId: 'prop-016',
propertyAddress: 'Güterstrasse 22, Basel',
title: 'Gewerbe Güterstrasse Basel',
location: 'Basel',
diff --git a/src/pages/demand/Compare.tsx b/src/pages/demand/Compare.tsx
index 72e5cb0..62db947 100644
--- a/src/pages/demand/Compare.tsx
+++ b/src/pages/demand/Compare.tsx
@@ -29,6 +29,7 @@ import {
MissingDataCell,
AICompareSummary,
} from '../../components/compare'
+import { AddToPipelineDialog } from '../../components/shortlist'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
import type { VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult'
@@ -204,6 +205,8 @@ export default function Compare() {
return (
+
+
{/* Page header */}
diff --git a/src/pages/demand/Pipeline.tsx b/src/pages/demand/Pipeline.tsx
index ececc34..8aa3580 100644
--- a/src/pages/demand/Pipeline.tsx
+++ b/src/pages/demand/Pipeline.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useNavigate } from 'react-router'
import {
- Box, Typography, Chip, Paper, Button, IconButton, TextField, Divider, Tooltip,
+ Box, Typography, Chip, Paper, Button, IconButton, TextField, Divider, Tooltip, Card,
} from '@mui/material'
import {
DndContext, DragOverlay, PointerSensor, useSensor, useSensors,
@@ -9,9 +9,10 @@ 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, MessageSquare, MapPin } from 'lucide-react'
+import { X, Sparkles, FileText, StickyNote, ChevronRight, TrendingUp, AlertTriangle, CheckCircle, MessageSquare, MapPin, ExternalLink } from 'lucide-react'
import { usePipelineStore } from '../../stores/pipelineStore'
import { AddToPipelineDialog } from '../../components/shortlist'
+import { MatchScoreDisplay } from '../../components/match-card/MatchScoreDisplay'
import type { PipelineItem, PipelineStage } from '../../domain/pipeline'
// ── Stage config ──────────────────────────────────────────────────────────────
@@ -64,6 +65,15 @@ function scoreColor(score: number) {
return score >= 80 ? '#1a7a4a' : score >= 65 ? '#d97706' : '#c0392b'
}
+function detailPath(item: PipelineItem): string | null {
+ // propertyId is always stable across sessions — prefer it
+ if (item.propertyId) return `/demand/property/${item.propertyId}`
+ // matchId / UUID only works in the same session (matchStore is ephemeral)
+ if (item.matchId) return `/demand/results/${item.matchId}`
+ if (item.id.startsWith('match-')) return `/demand/results/${item.id}`
+ return null
+}
+
function getKiInsight(item: PipelineItem): { summary: string; positives: string[]; risks: string[] } {
if (item.stage === 'SAVED') return {
summary: `Merkliste-Eintrag mit ${item.matchScore}% Match. Prüfen Sie, ob dieses Objekt qualifiziert werden soll.`,
@@ -116,7 +126,10 @@ function DraggableCard({
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),
@@ -125,20 +138,23 @@ function DraggableCard({
} : undefined
return (
- !isDragging && onSelect(item)}
sx={{
p: 1.5,
- borderRadius: 1.5,
- border: isSelected && !isDragOverlay ? '2px solid #1e3a5f' : '1px solid #e2e8f0',
+ 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)' : undefined,
+ 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.1)',
+ 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',
@@ -147,44 +163,80 @@ function DraggableCard({
}}
{...(isDragOverlay ? {} : { ...attributes, ...listeners })}
>
-
-
- {item.title}
-
-
- {item.inquiryId && onChatClick && !isDragOverlay && (
-
-
-
-
-
- )}
-
- {item.matchScore}%
-
+ {/* Row 1: Score + type chip + chat icon */}
+
+
+
+
+
+ {!isDragOverlay && (
+
+ {item.inquiryId && onChatClick && (
+
+
+
+
+
+ )}
+ {path && (
+
+ { e.stopPropagation(); navigate(path) }}
+ sx={{ p: 0.25, color: '#94a3b8', '&:hover': { color: '#1e3a5f', bgcolor: '#eff6ff' } }}
+ >
+
+
+
+ )}
+
+ )}
+
+ {/* Row 2: Title */}
+
+ {item.title}
+
+
+ {/* Row 3: Location */}
-
-
+
+
{item.propertyAddress ?? item.location}
-
-
- {item.areaLabel && {item.areaLabel}}
- {item.rentLabel && {item.rentLabel}}
-
+
+ {/* Row 4: Area + rent */}
+ {(item.areaLabel || item.rentLabel) && (
+
+ {item.areaLabel && (
+ {item.areaLabel}
+ )}
+ {item.areaLabel && item.rentLabel && (
+
+ )}
+ {item.rentLabel && (
+ {item.rentLabel}
+ )}
+
+ )}
+
+ {/* Row 5: Notes preview */}
{item.notes && (
-
+
{item.notes}
)}
-
+
)
}
@@ -247,6 +299,7 @@ function DroppableColumn({
function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => void }) {
const navigate = useNavigate()
const { moveStage, updateNotes, loseItem } = usePipelineStore()
+ const path = detailPath(item)
const [notes, setNotes] = useState(item.notes ?? '')
const stageConfig = STAGES.find(s => s.key === item.stage)!
const stageIndex = STAGES.findIndex(s => s.key === item.stage)
@@ -269,10 +322,17 @@ function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => voi
{item.title}
{item.location}
-
+
{item.matchScore}%
+ {path && (
+
+ navigate(path)} sx={{ color: '#64748b', '&:hover': { color: '#1e3a5f' } }}>
+
+
+
+ )}
diff --git a/src/stores/pipelineStore.ts b/src/stores/pipelineStore.ts
index d81de9c..a8d52fd 100644
--- a/src/stores/pipelineStore.ts
+++ b/src/stores/pipelineStore.ts
@@ -49,6 +49,7 @@ export const usePipelineStore = create((set, get) => ({
if (!alreadyExists) {
const newItem: PipelineItem = {
id: pendingItem.resultId,
+ matchId: pendingItem.resultId,
title: pendingItem.title,
location: pendingItem.location ?? '–',
matchScore: pendingItem.matchScore,