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:
Benjamin Sutter
2026-06-21 01:32:13 +02:00
parent 6206447dae
commit a8b54af0b8
13 changed files with 349 additions and 110 deletions
+26 -6
View File
@@ -1,6 +1,6 @@
import { MockupInquiryProvider } from '../provider/MockupInquiryProvider'
import type { InquiryFilters } from '../provider/IInquiryProvider'
import type { Inquiry, Attachment } from '../domain/inquiry'
import type { InquiryFilters, CreateInquiryInput, CreateOfferInput } from '../provider/IInquiryProvider'
import type { Inquiry, Attachment, InquiryMessage } from '../domain/inquiry'
import type { ListResponse, ItemResponse } from './types'
import { throwServiceError } from './errors'
@@ -10,6 +10,8 @@ export interface InquiryReplyPayload {
subject?: string
body: string
attachments?: Attachment[]
senderType?: InquiryMessage['senderType'] // default 'supply_user'
senderName?: string
}
export const inquiryService = {
@@ -31,14 +33,32 @@ export const inquiryService = {
}
},
async createInquiry(input: CreateInquiryInput): Promise<ItemResponse<Inquiry>> {
try {
const data = await provider.createInquiry(input)
return { data }
} catch (err) {
throwServiceError(err)
}
},
async createOffer(input: CreateOfferInput): Promise<ItemResponse<Inquiry>> {
try {
const data = await provider.createOffer(input)
return { data }
} catch (err) {
throwServiceError(err)
}
},
async sendInquiryReply(
inquiryId: string,
payload: InquiryReplyPayload,
): Promise<ItemResponse<Inquiry>> {
try {
const data = await provider.addMessage(inquiryId, {
senderType: 'supply_user',
senderName: 'Wincasa AG',
senderType: payload.senderType ?? 'supply_user',
senderName: payload.senderName ?? 'Wincasa AG',
subject: payload.subject,
body: payload.body,
attachments: payload.attachments ?? [],
@@ -58,9 +78,9 @@ export const inquiryService = {
}
},
async getUnreadCount(): Promise<ItemResponse<number>> {
async getUnreadCount(filters?: InquiryFilters): Promise<ItemResponse<number>> {
try {
const data = await provider.getUnreadCount()
const data = await provider.getUnreadCount(filters)
return { data }
} catch (err) {
throwServiceError(err)