diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx index 50ada5f..dbec8a2 100644 --- a/src/components/match-card/IntelligenceMatchCard.tsx +++ b/src/components/match-card/IntelligenceMatchCard.tsx @@ -4,6 +4,7 @@ import { CheckCircle2, } from 'lucide-react' import { useSessionStore } from '../../stores/sessionStore' +import { useInquiryStore } from '../../stores/inquiryStore' import { LocationPreview } from '../shared/LocationPreview' import { HeatBadge } from '../shared/HeatBadge' import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme' @@ -25,6 +26,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro const tier = getScoreTier(vm.matchScore) const theme = SCORE_THEME[tier] const { currentUser } = useSessionStore() + const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog) const isPortfolioOwner = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN' const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' } const isFuture = vm.resultType === 'FUTURE_AVAILABILITY' @@ -141,6 +143,23 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro {/* Actions */} + {vm.actions.map(a => ( + + + + ) +} diff --git a/src/components/match-detail/NextActionsPanel.tsx b/src/components/match-detail/NextActionsPanel.tsx index ecdf6d5..fb4befe 100644 --- a/src/components/match-detail/NextActionsPanel.tsx +++ b/src/components/match-detail/NextActionsPanel.tsx @@ -1,23 +1,17 @@ import { Box, Button, Paper, Typography } from '@mui/material' +import { MessageSquare } from 'lucide-react' import type { Match, NextBestAction } from '../../domain/match' const PRIORITY_ORDER: Record = { HIGH: 0, MEDIUM: 1, LOW: 2 } -const PRIORITY_COLOR: Record = { - HIGH: 'contained', - MEDIUM: 'outlined', - LOW: 'outlined', -} - interface Props { match: Match onCompare?: () => void - onShortlist?: () => void - onReject?: () => void - onReview?: () => void + onPipeline?: () => void + onInquire?: () => void } -export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onReview }: Props) { +export function NextActionsPanel({ match, onCompare, onPipeline, onInquire }: Props) { const engineActions: NextBestAction[] = [...(match.nextBestActions ?? [])].sort( (a, b) => (PRIORITY_ORDER[a.priority] ?? 2) - (PRIORITY_ORDER[b.priority] ?? 2) ) @@ -26,37 +20,41 @@ export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onRe Empfohlene Aktionen - {engineActions.length > 0 && ( - - {engineActions.map((action, i) => ( - - - {action.description && ( - - {action.description} - - )} - - ))} - - )} - - {/* Standard actions */} - {onShortlist && ( - + )} + + {engineActions.length > 0 && engineActions.map((action, i) => ( + + + {action.description && ( + + {action.description} + + )} + + ))} + + {onPipeline && ( + )} {onCompare && ( @@ -64,16 +62,6 @@ export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onRe Zum Vergleich hinzufügen )} - {onReview && ( - - )} - {onReject && ( - - )} ) diff --git a/src/components/pipeline/PipelineColumn.tsx b/src/components/pipeline/PipelineColumn.tsx index b7387b4..1fe9ce0 100644 --- a/src/components/pipeline/PipelineColumn.tsx +++ b/src/components/pipeline/PipelineColumn.tsx @@ -11,6 +11,7 @@ export function DroppableColumn({ selectedId, onSelect, onChatClick, + onCardChatClick, isOver, }: { stage: { key: PipelineStage; label: string; color: string; bgColor: string } @@ -18,6 +19,7 @@ export function DroppableColumn({ selectedId: string | null onSelect: (item: PipelineItem) => void onChatClick: (inquiryId: string) => void + onCardChatClick?: (item: PipelineItem) => void isOver: boolean }) { const { setNodeRef } = useDroppable({ id: stage.key }) @@ -43,7 +45,13 @@ export function DroppableColumn({ item={item} isSelected={item.id === selectedId} onSelect={onSelect} - onChatClick={item.inquiryId ? (e) => { e.stopPropagation(); onChatClick(item.inquiryId!) } : undefined} + onChatClick={ + item.inquiryId + ? (e) => { e.stopPropagation(); onChatClick(item.inquiryId!) } + : onCardChatClick + ? (e) => { e.stopPropagation(); onCardChatClick(item) } + : undefined + } /> ))} {items.length === 0 && ( diff --git a/src/pages/demand/Anfragen.tsx b/src/pages/demand/Anfragen.tsx index 54bd81e..485476a 100644 --- a/src/pages/demand/Anfragen.tsx +++ b/src/pages/demand/Anfragen.tsx @@ -6,6 +6,7 @@ import { } from '@mui/material' import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'lucide-react' import { mockInquiries } from '../../mock-data/inquiries' +import { useInquiryStore } from '../../stores/inquiryStore' import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' import { useToastStore } from '../../stores/toastStore' import type { InquiryMessage } from '../../domain/inquiry' @@ -34,7 +35,10 @@ export default function Anfragen() { const preselectedId = searchParams.get('inquiry') + const storeInquiries = useInquiryStore(s => s.sentInquiries) const [inquiries, setInquiries] = useState(mockInquiries) + const allInquiries = [...storeInquiries, ...inquiries] + const [selectedId, setSelectedId] = useState( preselectedId ?? mockInquiries[0]?.id ?? null ) @@ -45,7 +49,7 @@ export default function Anfragen() { const [kiAlert, setKiAlert] = useState<{ title: string; stage: string } | null>(null) const threadRef = useRef(null) - const filtered = inquiries.filter(inq => { + const filtered = allInquiries.filter(inq => { const q = search.toLowerCase() const matchesSearch = !q || inq.tenantName.toLowerCase().includes(q) || @@ -55,8 +59,8 @@ export default function Anfragen() { return matchesSearch && matchesStatus }) - const selected = inquiries.find(i => i.id === selectedId) ?? null - const totalUnread = inquiries.reduce((sum, i) => sum + i.unreadCount, 0) + const selected = allInquiries.find(i => i.id === selectedId) ?? null + const totalUnread = allInquiries.reduce((sum, i) => sum + i.unreadCount, 0) // Pipeline link for currently selected inquiry const linkedPipelineItem = selected?.propertyId diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx index 1507de5..662638a 100644 --- a/src/pages/demand/MatchDetail.tsx +++ b/src/pages/demand/MatchDetail.tsx @@ -1,9 +1,11 @@ import { useState } from 'react' +import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material' import { AlertTriangle, ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react' import { useNavigate, useParams } from 'react-router' import { useCompareStore } from '../../stores/compareStore' import { usePipelineStore } from '../../stores/pipelineStore' +import { useInquiryStore } from '../../stores/inquiryStore' import { AddToPipelineDialog } from '../../components/shortlist' import { MatchReasonList } from '../../components/match-card/MatchReasonList' import { PropertyMap } from '../../components/shared' @@ -46,6 +48,7 @@ export default function MatchDetail() { const navigate = useNavigate() const { addToCompare } = useCompareStore() const { openSavedDialog } = usePipelineStore() + const { openInquiryDialog } = useInquiryStore() const [showFullAnalysis, setShowFullAnalysis] = useState(false) const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '') @@ -126,6 +129,7 @@ export default function MatchDetail() { return ( + {/* Sticky back nav */} {/* ── Main content ── */} - + {/* ── Section A: Warum dieser Match? ── */} @@ -196,9 +200,14 @@ export default function MatchDetail() { {}} - onReject={() => {}} + onPipeline={handleShortlist} + onInquire={() => openInquiryDialog({ + propertyTitle: title, + location, + areaLabel: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : undefined, + rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined, + matchScore: match.matchScore, + })} /> {/* ── Full analysis toggle ── */} diff --git a/src/pages/demand/Pipeline.tsx b/src/pages/demand/Pipeline.tsx index 4922db5..bf10684 100644 --- a/src/pages/demand/Pipeline.tsx +++ b/src/pages/demand/Pipeline.tsx @@ -9,7 +9,9 @@ import { import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core' import { TrendingUp } from 'lucide-react' import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' +import { useInquiryStore } from '../../stores/inquiryStore' import { AddToPipelineDialog } from '../../components/shortlist' +import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' import type { PipelineItem, PipelineStage } from '../../domain/pipeline' import { STAGES } from '../../components/pipeline/pipelineConstants' import { DraggableCard } from '../../components/pipeline/PipelineCard' @@ -22,6 +24,7 @@ export default function Pipeline() { const navigate = useNavigate() const { data: items = [] } = usePipelineItems() const { mutate: moveStage } = useMoveStage() + const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog) const [selectedItem, setSelectedItem] = useState(null) const [activeId, setActiveId] = useState(null) const [overId, setOverId] = useState(null) @@ -66,6 +69,7 @@ export default function Pipeline() { return ( + {/* Header */} navigate(`/demand/anfragen?inquiry=${inquiryId}`)} + onCardChatClick={(item) => openInquiryDialog({ + propertyTitle: item.title, + location: item.location ?? '', + matchScore: item.matchScore ?? 0, + propertyId: item.propertyId, + })} isOver={isOver} /> diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index acf9983..cd3d1f3 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -14,6 +14,7 @@ import { UnifiedResultFeed, } from '../../components/results' import { AddToPipelineDialog } from '../../components/shortlist' +import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' import { ErrorState } from '../../components/ui' import { useSessionStore } from '../../stores/sessionStore' import type { UnifiedMatchResult } from '../../domain/unifiedResult' @@ -101,6 +102,7 @@ export default function Results() { return ( + void + closeInquiryDialog: () => void + + // Sent inquiries (in-memory, persists for the session) + sentInquiries: Inquiry[] + addInquiry: (item: PendingInquiry, message: string) => void +} + +export const useInquiryStore = create((set) => ({ + dialogOpen: false, + pendingInquiry: null, + openInquiryDialog: (item) => set({ dialogOpen: true, pendingInquiry: item }), + closeInquiryDialog: () => set({ dialogOpen: false, pendingInquiry: null }), + + sentInquiries: [], + addInquiry: (item, message) => { + const now = new Date().toISOString() + const msgId = `msg-sent-${Date.now()}` + const id = `sent-${Date.now()}` + + const thread: InquiryMessage = { + id: msgId, + inquiryId: id, + senderType: 'tenant', + senderName: 'Admin User', + body: message, + attachments: [], + createdAt: now, + } + + const inquiry: Inquiry = { + id, + organizationId: 'org-wincasa', + propertyId: item.propertyId ?? 'unknown', + tenantName: 'Admin User', + tenantCompany: 'Wincasa AG', + subject: 'Anfrage: ' + item.propertyTitle, + message, + status: 'new', + unreadCount: 0, + isRead: true, + matchScore: item.matchScore, + createdAt: now, + updatedAt: now, + thread: [thread], + } + + set(state => ({ sentInquiries: [inquiry, ...state.sentInquiries] })) + }, +}))