fix: Compare→Pipeline flow, pipeline card navigation, remove floating AI button

- Remove CompareTray fixed bottom bar; replace with compare count badge on Vergleich nav item
- Pipeline cards redesigned to match search result card visual style (MatchScoreDisplay, type chip, stage chip, MapPin layout)
- CompareColumnHeader: redesigned with proper Details/Merken action buttons, clickable title
- AddToPipelineDialog now mounted on Compare page (was missing — bookmark had no effect)
- Pipeline card ExternalLink icon navigates to property detail page; detailPath prefers stable propertyId over volatile session matchId
- Add matchId field to PipelineItem domain; pipelineStore stores it on save
- All 8 mock pipeline items now have propertyId for reliable cross-session navigation
- Remove GlobalAIAssistantButton floating overlay (was blocking form submissions and clicks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-22 22:11:29 +02:00
parent 6a6ff7f2e2
commit 72e4f08900
7 changed files with 201 additions and 78 deletions
+62 -27
View File
@@ -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 (
<Box sx={{ p: 0.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
{/* Row 1: type chip + remove */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Chip label={meta.label} size="small" sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 10 }} />
<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} />
<Tooltip title="Aus Vergleich entfernen">
<IconButton size="small" onClick={onRemove} sx={{ p: 0.375, color: '#94a3b8', '&:hover': { color: '#c0392b' } }}>
<X size={15} />
</IconButton>
</Box>
</Tooltip>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.25, lineHeight: 1.3 }}>
{/* Row 2: title (clickable) */}
<Typography
variant="subtitle1"
sx={{
fontWeight: 700, lineHeight: 1.3, mb: 0.25,
cursor: 'pointer',
'&:hover': { color: '#1e3a5f' },
}}
onClick={() => navigate(`/demand/results/${item.matchId}`)}
>
{title}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.25 }}>
{subtitle}{district ? `, ${district}` : ''}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 0.5 }}>
{/* Row 3: score */}
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 1 }}>
<Typography variant="h4" sx={{ fontWeight: 800, color: SCORE_COLOR(item.matchScore), lineHeight: 1 }}>
{item.matchScore}
</Typography>
<Typography variant="caption" color="text.secondary">/100</Typography>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 0.5 }}>
{/* Row 4: meta chips */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1.25 }}>
<Tooltip title="Konfidenz">
<Chip
label={`${confidence}% Konfidenz`}
@@ -113,7 +113,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
</Box>
{item.resultType === 'FUTURE_AVAILABILITY' && sig && (
<Box sx={{ mt: 1, p: 0.75, bgcolor: '#faf5ff', borderRadius: 1, border: '1px solid #e9d5ff' }}>
<Box sx={{ mb: 1.25, p: 0.75, bgcolor: '#faf5ff', borderRadius: 1, border: '1px solid #e9d5ff' }}>
<Typography variant="caption" sx={{ color: '#7c3aed', fontWeight: 600, display: 'block' }}>
Probabilistisches Signal
</Typography>
@@ -122,6 +122,41 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
</Typography>
</Box>
)}
{/* Row 5: action buttons */}
<Box sx={{ display: 'flex', gap: 0.75, pt: 1, borderTop: '1px solid #f1f5f9' }}>
<Button
size="small"
variant="outlined"
startIcon={<ExternalLink size={12} />}
onClick={() => navigate(`/demand/results/${item.matchId}`)}
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5 }}
>
Details
</Button>
{isInPipeline ? (
<Button
size="small"
variant="outlined"
startIcon={<BookmarkCheck size={12} />}
disabled
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, color: '#1a7a4a', borderColor: '#86efac' }}
>
In Pipeline
</Button>
) : (
<Button
size="small"
variant="contained"
startIcon={<Kanban size={12} />}
onClick={handleSave}
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
>
Merken
</Button>
)}
</Box>
</Box>
)
}
+27 -15
View File
@@ -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({
>
<Icon size={16} color={isActive ? TEXT_WHITE : TEXT_MUTED} />
{!collapsed && (
<Typography
variant="body2"
sx={{
color: isActive ? TEXT_WHITE : TEXT_MUTED,
fontWeight: isActive ? 500 : 400,
fontSize: '0.8125rem',
whiteSpace: 'nowrap',
}}
>
{item.label}
</Typography>
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
<Typography
variant="body2"
sx={{
color: isActive ? TEXT_WHITE : TEXT_MUTED,
fontWeight: isActive ? 500 : 400,
fontSize: '0.8125rem',
whiteSpace: 'nowrap',
}}
>
{item.label}
</Typography>
{item.path === '/demand/compare' && compareCount > 0 && (
<Box sx={{
minWidth: 18, height: 18, borderRadius: '9px',
bgcolor: '#d97706', color: 'white',
fontSize: '0.65rem', fontWeight: 700,
display: 'flex', alignItems: 'center', justifyContent: 'center',
px: 0.5, flexShrink: 0,
}}>
{compareCount}
</Box>
)}
</Box>
)}
</Box>
)}
@@ -605,9 +619,7 @@ export function AppShell() {
<RightContextPanel />
</Box>
<CompareTray />
<GlobalAIAssistantDrawer />
<GlobalAIAssistantButton />
<ToastProvider />
<WelcomeDialog />
</Box>