feat(messages): unified conversation inbox — connect demand ↔ supply, offers reach the seeker
Phase 1 of the messaging rework: one conversation/thread model (Inquiry) is the single source of truth, read perspectivally by both sides. - domain/inquiry: add kind (INQUIRY|OFFER), tenantOrgId (demand org), offeredPropertyIds - provider/service: InquiryFilters gains tenantOrgId+kind; new createInquiry (demand→supply) & createOffer (supply→demand) materialize threads; addMessage bumps unread; reply carries senderType (tenant vs supply_user) - hooks: useCreateInquiry, useCreateOffer; reply payload carries sender perspective - demand: InquiryQuickDialog → createInquiry via provider (session-based tenant/org, no hardcode); Anfragen.tsx reads via React Query (tenantOrgId) with Gesendet/Erhalten tabs, replies via provider, shows offered properties for OFFER threads; inquiryStore reduced to dialog-only (removes Zustand server-data island) - supply: OfferChatComposer send now materializes an OFFER conversation into the seeker's inbox (routed via latentNeed.tenantOrgId); Anfragencenter gains a "Gesendet" tab; ActiveInquiriesTab filters by perspective (org + kind) - seed: merge demand inquiries with tenantOrgId=org-mobimo + 2 OFFER seeds; provider seeds from both sets Closes the loop: a sent inquiry reaches the Verwaltung and a sent offer reaches the Nachfrager — both answerable in one thread. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Box, IconButton, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { ArrowLeft, LayoutGrid, List as ListIcon, Inbox } from 'lucide-react'
|
||||
import { useActiveInquiries } from '../../hooks/useInquiries'
|
||||
import type { InquiryFilters } from '../../provider/IInquiryProvider'
|
||||
import { EmptyState } from '../ui'
|
||||
import { InquiryList } from './InquiryList'
|
||||
import { InquiryCardGrid } from './InquiryCardGrid'
|
||||
@@ -17,8 +18,14 @@ function loadViewMode(): ViewMode {
|
||||
return stored === 'grid' ? 'grid' : 'list'
|
||||
}
|
||||
|
||||
export function ActiveInquiriesTab() {
|
||||
const { data: inquiries = [], isLoading } = useActiveInquiries()
|
||||
interface Props {
|
||||
filters?: InquiryFilters
|
||||
emptyTitle?: string
|
||||
emptyDescription?: string
|
||||
}
|
||||
|
||||
export function ActiveInquiriesTab({ filters, emptyTitle, emptyDescription }: Props = {}) {
|
||||
const { data: inquiries = [], isLoading } = useActiveInquiries(filters)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [view, setView] = useState<ViewMode>(loadViewMode())
|
||||
const theme = useTheme()
|
||||
@@ -41,8 +48,8 @@ export function ActiveInquiriesTab() {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<Inbox size={40} />}
|
||||
title="Keine aktiven Anfragen"
|
||||
description="Sobald Interessenten Anfragen zu Ihren Objekten stellen, erscheinen diese hier."
|
||||
title={emptyTitle ?? 'Keine aktiven Anfragen'}
|
||||
description={emptyDescription ?? 'Sobald Interessenten Anfragen zu Ihren Objekten stellen, erscheinen diese hier.'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Box, Button, CircularProgress, IconButton, TextField, Typography } from
|
||||
import { ArrowLeft, FileText, Paperclip, Send, X } from 'lucide-react'
|
||||
import { useOfferWizardStore } from '../../stores/offerWizardStore'
|
||||
import { useSendOffer } from '../../hooks/useOffers'
|
||||
import { useCreateOffer } from '../../hooks/useInquiries'
|
||||
import { useLatentNeedById } from '../../hooks/useLatentNeeds'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { AiOfferEmailButton } from './AiOfferEmailButton'
|
||||
import { mockProperties } from '../../mock-data/properties'
|
||||
@@ -24,6 +27,9 @@ export function OfferChatComposer() {
|
||||
const reset = useOfferWizardStore(s => s.reset)
|
||||
|
||||
const sendOffer = useSendOffer()
|
||||
const createOffer = useCreateOffer()
|
||||
const { data: latentNeed } = useLatentNeedById(needId)
|
||||
const currentUser = useSessionStore(s => s.currentUser)
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
|
||||
const propertyTitles = selectedPropertyIds.map(id => {
|
||||
@@ -60,6 +66,19 @@ export function OfferChatComposer() {
|
||||
showToast(`Fehler: ${res.error}`, 'error')
|
||||
return
|
||||
}
|
||||
// Angebot als Konversation im Nachfrager-Postfach materialisieren
|
||||
await createOffer.mutateAsync({
|
||||
organizationId: currentUser?.organizationId ?? 'org-wincasa',
|
||||
tenantOrgId: latentNeed?.tenantOrgId ?? 'org-mobimo',
|
||||
needId: needId ?? '',
|
||||
propertyId: selectedPropertyIds[0] ?? '',
|
||||
offeredPropertyIds: selectedPropertyIds,
|
||||
subject,
|
||||
message: body,
|
||||
senderName: currentUser?.organizationName ?? 'Wincasa AG',
|
||||
recipientName: latentNeed?.tenantCompany,
|
||||
attachments,
|
||||
})
|
||||
showToast('Angebot erfolgreich gesendet', 'success')
|
||||
reset()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
import { Send } from 'lucide-react'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { useInquiryStore } from '../../stores/inquiryStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useMoveStage } from '../../hooks/usePipeline'
|
||||
import { useCreateInquiry } from '../../hooks/useInquiries'
|
||||
import { DS_BG, DS_BORDER, DS_TEXT } from '../../lib/ds'
|
||||
|
||||
function buildTemplate(propertyTitle: string, location: string, areaLabel?: string): string {
|
||||
@@ -19,7 +21,8 @@ export function InquiryQuickDialog() {
|
||||
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 currentUser = useSessionStore(s => s.currentUser)
|
||||
const createInquiry = useCreateInquiry()
|
||||
const { mutate: moveStage } = useMoveStage()
|
||||
|
||||
const [message, setMessage] = useState('')
|
||||
@@ -32,7 +35,18 @@ export function InquiryQuickDialog() {
|
||||
|
||||
function handleSend() {
|
||||
if (!pendingInquiry) return
|
||||
addInquiry(pendingInquiry, message)
|
||||
createInquiry.mutate({
|
||||
organizationId: 'org-wincasa', // Supply-Org des Objekts (Mock: Wincasa)
|
||||
tenantOrgId: currentUser?.organizationId ?? 'org-mobimo',
|
||||
propertyId: pendingInquiry.propertyId ?? 'unknown',
|
||||
tenantName: currentUser?.name ?? 'Demand User',
|
||||
tenantCompany: currentUser?.organizationName,
|
||||
tenantEmail: currentUser?.email,
|
||||
propertyAddress: pendingInquiry.propertyTitle,
|
||||
subject: `Anfrage: ${pendingInquiry.propertyTitle}`,
|
||||
message,
|
||||
matchScore: pendingInquiry.matchScore,
|
||||
})
|
||||
if (pendingInquiry.pipelineItemId) {
|
||||
moveStage({ id: pendingInquiry.pipelineItemId, stage: 'CONTACTED' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user