feat: in-memory inquiry flow — send from card, pipeline, and detail

New inquiryStore (Zustand) holds sent inquiries for the session.
InquiryQuickDialog reads from the store (no props), renders wherever needed.
Sent inquiries appear immediately at the top of the Anfragen page.

Entry points:
- Match cards: "Anfrage" button on every card in the results feed
- MatchDetail: primary action in NextActionsPanel
- Pipeline: chat icon on every DraggableCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 18:00:46 +02:00
parent 9df5d285f5
commit 128d28af8d
9 changed files with 252 additions and 58 deletions
+7 -3
View File
@@ -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<string | null>(
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<HTMLDivElement>(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
+13 -4
View File
@@ -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 (
<Box sx={{ bgcolor: DS_BG.page, minHeight: '100vh' }}>
<AddToPipelineDialog />
<InquiryQuickDialog />
{/* Sticky back nav */}
<Box sx={{
@@ -164,7 +168,7 @@ export default function MatchDetail() {
/>
{/* ── Main content ── */}
<Box sx={{ maxWidth: 780, mx: 'auto', px: { xs: 1.5, sm: 3 }, py: { xs: 2, sm: 3 }, display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ maxWidth: { sm: 780, md: 960, lg: 1100 }, mx: 'auto', px: { xs: 1.5, sm: 3 }, py: { xs: 2, sm: 3 }, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* ── Section A: Warum dieser Match? ── */}
<Paper sx={{ p: 2.5 }}>
@@ -196,9 +200,14 @@ export default function MatchDetail() {
<NextActionsPanel
match={match}
onCompare={handleCompare}
onShortlist={handleShortlist}
onReview={() => {}}
onReject={() => {}}
onPipeline={handleShortlist}
onInquire={() => openInquiryDialog({
propertyTitle: title,
location,
areaLabel: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')}` : undefined,
rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined,
matchScore: match.matchScore,
})}
/>
{/* ── Full analysis toggle ── */}
+10
View File
@@ -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<PipelineItem | null>(null)
const [activeId, setActiveId] = useState<string | null>(null)
const [overId, setOverId] = useState<string | null>(null)
@@ -66,6 +69,7 @@ export default function Pipeline() {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<AddToPipelineDialog />
<InquiryQuickDialog />
{/* Header */}
<Box sx={{
@@ -123,6 +127,12 @@ export default function Pipeline() {
selectedId={syncedSelected?.id ?? null}
onSelect={handleSelect}
onChatClick={(inquiryId) => navigate(`/demand/anfragen?inquiry=${inquiryId}`)}
onCardChatClick={(item) => openInquiryDialog({
propertyTitle: item.title,
location: item.location ?? '',
matchScore: item.matchScore ?? 0,
propertyId: item.propertyId,
})}
isOver={isOver}
/>
</Box>
+2
View File
@@ -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 (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<AddToPipelineDialog />
<InquiryQuickDialog />
<ResultFeedHeader
total={sorted.length}
platformCount={platformCount}