feat: Anfragencenter Parts A–E — read/unread, prep wizard, offer flow, report preview

- Part A: Replace InquiryStatus badges with WhatsApp-style unread indicators
  (isRead, unreadCount, lastReadAt on Inquiry; markThreadAsRead in service + hook)
- Part B: RelatedPropertyCardPanel — reposition "Objekt ansehen", add "Weitere
  Matches" section via matchService.getAdditionalMatchesForInquiry
- Part C: PreparationWizard 4-step dialog — property selection, report generation
  progress, field editing + ReportObjectFieldSelector, PDF preview + finalize
- Part D: OfferCreationWizard 4-step dialog triggered from first tenant message —
  data capture, field editing, viewing appointments, PDF generation + attach to reply
- Part E: LatentInquiryReportPreview (2-page A4 doc), ReportObjectFieldSelector
  (5 accordion groups, 35+ optional fields), mapImageUrl on Property domain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-19 19:20:13 +02:00
parent 279b55d70e
commit b4d398270f
26 changed files with 1874 additions and 188 deletions
+38
View File
@@ -1,14 +1,17 @@
import { MockupMatchProvider } from '../provider/MockupMatchProvider'
import { MockupPropertyProvider } from '../provider/MockupPropertyProvider'
import { MockupNeedProvider } from '../provider/MockupNeedProvider'
import { MockupInquiryProvider } from '../provider/MockupInquiryProvider'
import type { MatchFilters } from '../provider/IMatchProvider'
import type { Match, PropertyNeedMatch } from '../domain/match'
import type { Need } from '../domain/need'
import type { Property } from '../domain/property'
import type { StrongMatchItem } from '../domain/dashboard'
import type { ScoreBreakdown } from '../domain/match'
import type { AdditionalPropertyMatch } from '../domain/additionalMatch'
import type { ListResponse, ItemResponse } from './types'
import { buildFullMatch, computeRankedMatches } from '../features/matching/rankingEngine'
import { ResultType } from '../domain/enums'
const provider = MockupMatchProvider
@@ -98,6 +101,41 @@ export const matchService = {
.filter((x): x is PropertyNeedMatch => x !== null)
},
async getAdditionalMatchesForInquiry(
inquiryId: string,
opts?: { minScore?: number; excludePropertyId?: string },
): Promise<AdditionalPropertyMatch[]> {
const minScore = opts?.minScore ?? 80
const inquiry = await MockupInquiryProvider.getById(inquiryId)
if (!inquiry) return []
const excludeId = opts?.excludePropertyId ?? inquiry.propertyId
const [allProperties, allMatches] = await Promise.all([
MockupPropertyProvider.getAll(),
provider.getAll(),
])
const portfolioProperties = allProperties.filter(
p => p.resultType === ResultType.VERIFIED_PORTFOLIO && p.id !== excludeId,
)
return portfolioProperties
.map(p => {
const match = allMatches.find(m => m.propertyId === p.id)
const score = match?.matchScore ?? 0
return { property: p, score }
})
.filter(({ score }) => score >= minScore)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(({ property: p, score }) => ({
propertyId: p.id,
title: p.title,
location: `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`,
areaSqm: p.areaSqm,
rentPricePerSqm: p.rentPricePerSqm,
matchScore: score,
imageUrl: p.images?.[0],
}))
},
async getStrongMatches(minScore = 80): Promise<StrongMatchItem[]> {
const [matches, properties] = await Promise.all([
provider.getAll(),