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
@@ -4,6 +4,7 @@ import {
CheckCircle2, CheckCircle2,
} from 'lucide-react' } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { LocationPreview } from '../shared/LocationPreview' import { LocationPreview } from '../shared/LocationPreview'
import { HeatBadge } from '../shared/HeatBadge' import { HeatBadge } from '../shared/HeatBadge'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme' 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 tier = getScoreTier(vm.matchScore)
const theme = SCORE_THEME[tier] const theme = SCORE_THEME[tier]
const { currentUser } = useSessionStore() const { currentUser } = useSessionStore()
const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
const isPortfolioOwner = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN' const isPortfolioOwner = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN'
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' } const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
const isFuture = vm.resultType === 'FUTURE_AVAILABILITY' const isFuture = vm.resultType === 'FUTURE_AVAILABILITY'
@@ -141,6 +143,23 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
{/* Actions */} {/* Actions */}
<Box sx={{ display: 'flex', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}> <Box sx={{ display: 'flex', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
<Button
size="small"
variant="contained"
onClick={e => {
e.stopPropagation()
openInquiryDialog({
propertyTitle: vm.title,
location: vm.locationLabel ?? '',
matchScore: vm.matchScore,
matchId: vm.id,
propertyId: vm.propertyId,
})
}}
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
>
Anfrage
</Button>
{vm.actions.map(a => ( {vm.actions.map(a => (
<Button <Button
key={a.id} key={a.id}
@@ -0,0 +1,87 @@
import { useState, useEffect } from 'react'
import {
Box, Button, Dialog, DialogActions, DialogContent, DialogTitle,
TextField, Typography,
} from '@mui/material'
import { Send } from 'lucide-react'
import { useToastStore } from '../../stores/toastStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { DS_BG, DS_BORDER, DS_TEXT } from '../../lib/ds'
function buildTemplate(propertyTitle: string, location: string, areaLabel?: string): string {
const areaLine = areaLabel ? `\nDie Fläche von ${areaLabel} entspricht unserem Bedarf.` : ''
return `Guten Tag\n\nWir interessieren uns für Ihre Fläche «${propertyTitle}» in ${location}.${areaLine} Gerne würden wir einen Besichtigungstermin vereinbaren und offene Fragen klären.\n\nFür Rückfragen stehen wir jederzeit zur Verfügung.\n\nFreundliche Grüsse`
}
export function InquiryQuickDialog() {
const showToast = useToastStore(s => s.showToast)
const dialogOpen = useInquiryStore(s => s.dialogOpen)
const pendingInquiry = useInquiryStore(s => s.pendingInquiry)
const closeInquiryDialog = useInquiryStore(s => s.closeInquiryDialog)
const addInquiry = useInquiryStore(s => s.addInquiry)
const [message, setMessage] = useState('')
useEffect(() => {
if (dialogOpen && pendingInquiry) {
setMessage(buildTemplate(pendingInquiry.propertyTitle, pendingInquiry.location, pendingInquiry.areaLabel))
}
}, [dialogOpen, pendingInquiry])
function handleSend() {
if (!pendingInquiry) return
addInquiry(pendingInquiry, message)
showToast('Anfrage gesendet — Sie erhalten eine Antwort per E-Mail.', 'success')
closeInquiryDialog()
}
if (!pendingInquiry) return null
return (
<Dialog open={dialogOpen} onClose={closeInquiryDialog} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 700, pb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<Send size={16} />
Anfrage senden
</DialogTitle>
<DialogContent sx={{ pt: 0 }}>
<Box sx={{ bgcolor: DS_BG.subtle, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1.5, p: 1.5, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{pendingInquiry.propertyTitle}</Typography>
<Typography variant="caption" sx={{ color: DS_TEXT.secondary }}>
{pendingInquiry.location}
{pendingInquiry.areaLabel && ` · ${pendingInquiry.areaLabel}`}
{pendingInquiry.rentLabel && ` · ${pendingInquiry.rentLabel}`}
{' · '}Match {pendingInquiry.matchScore}%
</Typography>
</Box>
<TextField
fullWidth
multiline
rows={8}
value={message}
onChange={e => setMessage(e.target.value)}
sx={{ '& .MuiInputBase-root': { fontSize: '0.875rem' } }}
/>
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block', mt: 1 }}>
Der Text kann vor dem Absenden angepasst werden.
</Typography>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
<Button onClick={closeInquiryDialog} size="small" sx={{ color: DS_TEXT.muted }}>Abbrechen</Button>
<Button
onClick={handleSend}
variant="contained"
size="small"
disabled={!message.trim()}
startIcon={<Send size={14} />}
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
>
Anfrage senden
</Button>
</DialogActions>
</Dialog>
)
}
@@ -1,23 +1,17 @@
import { Box, Button, Paper, Typography } from '@mui/material' import { Box, Button, Paper, Typography } from '@mui/material'
import { MessageSquare } from 'lucide-react'
import type { Match, NextBestAction } from '../../domain/match' import type { Match, NextBestAction } from '../../domain/match'
const PRIORITY_ORDER: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 } const PRIORITY_ORDER: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
const PRIORITY_COLOR: Record<string, 'contained' | 'outlined'> = {
HIGH: 'contained',
MEDIUM: 'outlined',
LOW: 'outlined',
}
interface Props { interface Props {
match: Match match: Match
onCompare?: () => void onCompare?: () => void
onShortlist?: () => void onPipeline?: () => void
onReject?: () => void onInquire?: () => void
onReview?: () => void
} }
export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onReview }: Props) { export function NextActionsPanel({ match, onCompare, onPipeline, onInquire }: Props) {
const engineActions: NextBestAction[] = [...(match.nextBestActions ?? [])].sort( const engineActions: NextBestAction[] = [...(match.nextBestActions ?? [])].sort(
(a, b) => (PRIORITY_ORDER[a.priority] ?? 2) - (PRIORITY_ORDER[b.priority] ?? 2) (a, b) => (PRIORITY_ORDER[a.priority] ?? 2) - (PRIORITY_ORDER[b.priority] ?? 2)
) )
@@ -26,37 +20,41 @@ export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onRe
<Paper sx={{ p: 2.5, mb: 2 }}> <Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Empfohlene Aktionen</Typography> <Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Empfohlene Aktionen</Typography>
{engineActions.length > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, mb: 1.5 }}>
{engineActions.map((action, i) => (
<Box key={i}>
<Button
fullWidth
size="small"
variant={PRIORITY_COLOR[action.priority]}
sx={
action.priority === 'HIGH'
? { bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' }, justifyContent: 'flex-start' }
: { justifyContent: 'flex-start' }
}
>
{action.label}
</Button>
{action.description && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25, px: 1 }}>
{action.description}
</Typography>
)}
</Box>
))}
</Box>
)}
{/* Standard actions */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{onShortlist && ( {onInquire && (
<Button fullWidth size="small" variant="outlined" onClick={onShortlist} sx={{ justifyContent: 'flex-start' }}> <Button
Zur Shortlist hinzufügen fullWidth
size="small"
variant="contained"
startIcon={<MessageSquare size={14} />}
onClick={onInquire}
sx={{ justifyContent: 'flex-start', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
>
Anfrage senden
</Button>
)}
{engineActions.length > 0 && engineActions.map((action, i) => (
<Box key={i}>
<Button
fullWidth
size="small"
variant="outlined"
sx={{ justifyContent: 'flex-start' }}
>
{action.label}
</Button>
{action.description && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25, px: 1 }}>
{action.description}
</Typography>
)}
</Box>
))}
{onPipeline && (
<Button fullWidth size="small" variant="outlined" onClick={onPipeline} sx={{ justifyContent: 'flex-start' }}>
Zur Pipeline hinzufügen
</Button> </Button>
)} )}
{onCompare && ( {onCompare && (
@@ -64,16 +62,6 @@ export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onRe
Zum Vergleich hinzufügen Zum Vergleich hinzufügen
</Button> </Button>
)} )}
{onReview && (
<Button fullWidth size="small" variant="outlined" onClick={onReview} sx={{ justifyContent: 'flex-start', color: '#d97706', borderColor: '#d97706' }}>
Zur Überprüfung senden
</Button>
)}
{onReject && (
<Button fullWidth size="small" variant="outlined" onClick={onReject} sx={{ justifyContent: 'flex-start', color: '#c0392b', borderColor: '#c0392b' }}>
Match ablehnen
</Button>
)}
</Box> </Box>
</Paper> </Paper>
) )
+9 -1
View File
@@ -11,6 +11,7 @@ export function DroppableColumn({
selectedId, selectedId,
onSelect, onSelect,
onChatClick, onChatClick,
onCardChatClick,
isOver, isOver,
}: { }: {
stage: { key: PipelineStage; label: string; color: string; bgColor: string } stage: { key: PipelineStage; label: string; color: string; bgColor: string }
@@ -18,6 +19,7 @@ export function DroppableColumn({
selectedId: string | null selectedId: string | null
onSelect: (item: PipelineItem) => void onSelect: (item: PipelineItem) => void
onChatClick: (inquiryId: string) => void onChatClick: (inquiryId: string) => void
onCardChatClick?: (item: PipelineItem) => void
isOver: boolean isOver: boolean
}) { }) {
const { setNodeRef } = useDroppable({ id: stage.key }) const { setNodeRef } = useDroppable({ id: stage.key })
@@ -43,7 +45,13 @@ export function DroppableColumn({
item={item} item={item}
isSelected={item.id === selectedId} isSelected={item.id === selectedId}
onSelect={onSelect} 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 && ( {items.length === 0 && (
+7 -3
View File
@@ -6,6 +6,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'lucide-react' import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'lucide-react'
import { mockInquiries } from '../../mock-data/inquiries' import { mockInquiries } from '../../mock-data/inquiries'
import { useInquiryStore } from '../../stores/inquiryStore'
import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline'
import { useToastStore } from '../../stores/toastStore' import { useToastStore } from '../../stores/toastStore'
import type { InquiryMessage } from '../../domain/inquiry' import type { InquiryMessage } from '../../domain/inquiry'
@@ -34,7 +35,10 @@ export default function Anfragen() {
const preselectedId = searchParams.get('inquiry') const preselectedId = searchParams.get('inquiry')
const storeInquiries = useInquiryStore(s => s.sentInquiries)
const [inquiries, setInquiries] = useState(mockInquiries) const [inquiries, setInquiries] = useState(mockInquiries)
const allInquiries = [...storeInquiries, ...inquiries]
const [selectedId, setSelectedId] = useState<string | null>( const [selectedId, setSelectedId] = useState<string | null>(
preselectedId ?? mockInquiries[0]?.id ?? 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 [kiAlert, setKiAlert] = useState<{ title: string; stage: string } | null>(null)
const threadRef = useRef<HTMLDivElement>(null) const threadRef = useRef<HTMLDivElement>(null)
const filtered = inquiries.filter(inq => { const filtered = allInquiries.filter(inq => {
const q = search.toLowerCase() const q = search.toLowerCase()
const matchesSearch = !q || const matchesSearch = !q ||
inq.tenantName.toLowerCase().includes(q) || inq.tenantName.toLowerCase().includes(q) ||
@@ -55,8 +59,8 @@ export default function Anfragen() {
return matchesSearch && matchesStatus return matchesSearch && matchesStatus
}) })
const selected = inquiries.find(i => i.id === selectedId) ?? null const selected = allInquiries.find(i => i.id === selectedId) ?? null
const totalUnread = inquiries.reduce((sum, i) => sum + i.unreadCount, 0) const totalUnread = allInquiries.reduce((sum, i) => sum + i.unreadCount, 0)
// Pipeline link for currently selected inquiry // Pipeline link for currently selected inquiry
const linkedPipelineItem = selected?.propertyId const linkedPipelineItem = selected?.propertyId
+13 -4
View File
@@ -1,9 +1,11 @@
import { useState } from 'react' import { useState } from 'react'
import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog'
import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material' import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material'
import { AlertTriangle, ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react' import { AlertTriangle, ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react'
import { useNavigate, useParams } from 'react-router' import { useNavigate, useParams } from 'react-router'
import { useCompareStore } from '../../stores/compareStore' import { useCompareStore } from '../../stores/compareStore'
import { usePipelineStore } from '../../stores/pipelineStore' import { usePipelineStore } from '../../stores/pipelineStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { AddToPipelineDialog } from '../../components/shortlist' import { AddToPipelineDialog } from '../../components/shortlist'
import { MatchReasonList } from '../../components/match-card/MatchReasonList' import { MatchReasonList } from '../../components/match-card/MatchReasonList'
import { PropertyMap } from '../../components/shared' import { PropertyMap } from '../../components/shared'
@@ -46,6 +48,7 @@ export default function MatchDetail() {
const navigate = useNavigate() const navigate = useNavigate()
const { addToCompare } = useCompareStore() const { addToCompare } = useCompareStore()
const { openSavedDialog } = usePipelineStore() const { openSavedDialog } = usePipelineStore()
const { openInquiryDialog } = useInquiryStore()
const [showFullAnalysis, setShowFullAnalysis] = useState(false) const [showFullAnalysis, setShowFullAnalysis] = useState(false)
const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '') const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '')
@@ -126,6 +129,7 @@ export default function MatchDetail() {
return ( return (
<Box sx={{ bgcolor: DS_BG.page, minHeight: '100vh' }}> <Box sx={{ bgcolor: DS_BG.page, minHeight: '100vh' }}>
<AddToPipelineDialog /> <AddToPipelineDialog />
<InquiryQuickDialog />
{/* Sticky back nav */} {/* Sticky back nav */}
<Box sx={{ <Box sx={{
@@ -164,7 +168,7 @@ export default function MatchDetail() {
/> />
{/* ── Main content ── */} {/* ── 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? ── */} {/* ── Section A: Warum dieser Match? ── */}
<Paper sx={{ p: 2.5 }}> <Paper sx={{ p: 2.5 }}>
@@ -196,9 +200,14 @@ export default function MatchDetail() {
<NextActionsPanel <NextActionsPanel
match={match} match={match}
onCompare={handleCompare} onCompare={handleCompare}
onShortlist={handleShortlist} onPipeline={handleShortlist}
onReview={() => {}} onInquire={() => openInquiryDialog({
onReject={() => {}} 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 ── */} {/* ── Full analysis toggle ── */}
+10
View File
@@ -9,7 +9,9 @@ import {
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core' import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'
import { TrendingUp } from 'lucide-react' import { TrendingUp } from 'lucide-react'
import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline'
import { useInquiryStore } from '../../stores/inquiryStore'
import { AddToPipelineDialog } from '../../components/shortlist' import { AddToPipelineDialog } from '../../components/shortlist'
import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog'
import type { PipelineItem, PipelineStage } from '../../domain/pipeline' import type { PipelineItem, PipelineStage } from '../../domain/pipeline'
import { STAGES } from '../../components/pipeline/pipelineConstants' import { STAGES } from '../../components/pipeline/pipelineConstants'
import { DraggableCard } from '../../components/pipeline/PipelineCard' import { DraggableCard } from '../../components/pipeline/PipelineCard'
@@ -22,6 +24,7 @@ export default function Pipeline() {
const navigate = useNavigate() const navigate = useNavigate()
const { data: items = [] } = usePipelineItems() const { data: items = [] } = usePipelineItems()
const { mutate: moveStage } = useMoveStage() const { mutate: moveStage } = useMoveStage()
const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
const [selectedItem, setSelectedItem] = useState<PipelineItem | null>(null) const [selectedItem, setSelectedItem] = useState<PipelineItem | null>(null)
const [activeId, setActiveId] = useState<string | null>(null) const [activeId, setActiveId] = useState<string | null>(null)
const [overId, setOverId] = useState<string | null>(null) const [overId, setOverId] = useState<string | null>(null)
@@ -66,6 +69,7 @@ export default function Pipeline() {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<AddToPipelineDialog /> <AddToPipelineDialog />
<InquiryQuickDialog />
{/* Header */} {/* Header */}
<Box sx={{ <Box sx={{
@@ -123,6 +127,12 @@ export default function Pipeline() {
selectedId={syncedSelected?.id ?? null} selectedId={syncedSelected?.id ?? null}
onSelect={handleSelect} onSelect={handleSelect}
onChatClick={(inquiryId) => navigate(`/demand/anfragen?inquiry=${inquiryId}`)} 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} isOver={isOver}
/> />
</Box> </Box>
+2
View File
@@ -14,6 +14,7 @@ import {
UnifiedResultFeed, UnifiedResultFeed,
} from '../../components/results' } from '../../components/results'
import { AddToPipelineDialog } from '../../components/shortlist' import { AddToPipelineDialog } from '../../components/shortlist'
import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog'
import { ErrorState } from '../../components/ui' import { ErrorState } from '../../components/ui'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import type { UnifiedMatchResult } from '../../domain/unifiedResult' import type { UnifiedMatchResult } from '../../domain/unifiedResult'
@@ -101,6 +102,7 @@ export default function Results() {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<AddToPipelineDialog /> <AddToPipelineDialog />
<InquiryQuickDialog />
<ResultFeedHeader <ResultFeedHeader
total={sorted.length} total={sorted.length}
platformCount={platformCount} platformCount={platformCount}
+67
View File
@@ -0,0 +1,67 @@
import { create } from 'zustand'
import type { Inquiry, InquiryMessage } from '../domain/inquiry'
export interface PendingInquiry {
propertyTitle: string
location: string
areaLabel?: string
rentLabel?: string
matchScore: number
matchId?: string
propertyId?: string
}
interface InquiryStore {
// Dialog state
dialogOpen: boolean
pendingInquiry: PendingInquiry | null
openInquiryDialog: (item: PendingInquiry) => void
closeInquiryDialog: () => void
// Sent inquiries (in-memory, persists for the session)
sentInquiries: Inquiry[]
addInquiry: (item: PendingInquiry, message: string) => void
}
export const useInquiryStore = create<InquiryStore>((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] }))
},
}))