feat: F030 Anfragencenter — Aktive & Latente Anfragen + Angebot-Wizard

Neuer Menüpunkt «Anfragencenter» (ehemals «Eingehende Bedarfe»):

Aktive Anfragen:
- Split-View: Anfrageliste (Liste/Grid-Toggle) + Chat-Detailansicht
- Chat-Verlauf mit Sender-Styling (Mieter links, Supply rechts)
- Antwortformular mit Betreff, Nachricht, Anhänge, Statuswechsel
- Zugehörige Property Card neben dem Chat

Latente Anfragen:
- Drei-Spalten-Layout: Need-Liste | Need-Detail | Eigene Objekte
- Need Cards mit AI-Zusammenfassung, Must-Haves, Präferenzen
- Eigene Portfolio-Objekte sortiert nach deterministischem Match-Score
- Checkbox-Selektion für Angebotsauswahl

Angebot-Wizard (4 Schritte):
- Schritt 1: Objekte auswählen (mit Score-Vorschau)
- Schritt 2: PDF-Vorschau + editierbare Textfelder
- Schritt 3: Angebot prüfen & bestätigen
- Schritt 4: Nachricht an Suchenden mit KI-generierter Mail

Architektur:
- Domain: Inquiry, LatentNeed, OfferDraft Types
- Provider: IInquiryProvider, ILatentNeedProvider, IOfferProvider + Mockups
- Services: inquiryService, latentNeedService, offerService
- Hooks: useInquiries, useLatentNeeds, useOffers (TanStack Query)
- Store: offerWizardStore (Zustand, lokaler Wizard-State)
- 27 neue Komponenten, alle Loading/Empty/Error States

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-18 18:03:05 +02:00
parent 05ac0083b2
commit c4c29ef7d3
51 changed files with 4000 additions and 1 deletions
@@ -0,0 +1,152 @@
import { Box, Typography } from '@mui/material'
import { Paperclip } from 'lucide-react'
import type { InquiryMessage } from '../../domain/inquiry'
import { formatInquiryDate, formatFileSize } from './inquiryUtils'
interface InquiryMessageBubbleProps {
message: InquiryMessage
}
export function InquiryMessageBubble({ message }: InquiryMessageBubbleProps) {
const isTenant = message.senderType === 'tenant'
const isSupply = message.senderType === 'supply_user'
const isSystemOrAi = message.senderType === 'system' || message.senderType === 'ai'
if (isSystemOrAi) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}>
<Typography
variant="caption"
sx={{
color: '#64748b',
fontStyle: 'italic',
bgcolor: '#f1f5f9',
px: 2,
py: 0.5,
borderRadius: 4,
fontSize: '0.75rem',
}}
>
{message.senderName}: {message.body}
</Typography>
</Box>
)
}
return (
<Box sx={{ display: 'flex', justifyContent: isTenant ? 'flex-start' : 'flex-end', mb: 1.5 }}>
<Box
sx={{
maxWidth: '75%',
bgcolor: isTenant ? '#f1f5f9' : '#1e3a5f',
color: isTenant ? '#0f172a' : 'white',
px: 1.75,
py: 1.25,
borderRadius: 2,
boxShadow: '0 1px 2px rgba(15,23,42,0.06)',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1.5, mb: 0.5 }}>
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
color: isTenant ? '#0f172a' : 'white',
}}
>
{message.senderName}
</Typography>
<Typography
variant="caption"
sx={{
fontSize: '0.7rem',
color: isTenant ? '#64748b' : 'rgba(255,255,255,0.7)',
}}
>
{formatInquiryDate(message.createdAt)}
</Typography>
</Box>
{message.subject && (
<Typography
variant="caption"
sx={{
display: 'block',
fontWeight: 600,
fontSize: '0.75rem',
color: isTenant ? '#334155' : 'rgba(255,255,255,0.85)',
mb: 0.5,
}}
>
{message.subject}
</Typography>
)}
<Typography
variant="body2"
sx={{
whiteSpace: 'pre-wrap',
fontSize: '0.85rem',
lineHeight: 1.5,
color: isTenant ? '#0f172a' : 'white',
}}
>
{message.body}
</Typography>
{message.attachments.length > 0 && (
<Box sx={{ mt: 1, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{message.attachments.map(att => (
<Box
key={att.id}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
bgcolor: isTenant ? 'white' : 'rgba(255,255,255,0.12)',
border: '1px solid',
borderColor: isTenant ? '#cbd5e1' : 'rgba(255,255,255,0.25)',
px: 1,
py: 0.5,
borderRadius: 1,
fontSize: '0.75rem',
}}
>
<Paperclip size={12} />
<Typography variant="caption" sx={{ fontSize: '0.75rem', fontWeight: 500 }}>
{att.fileName}
</Typography>
{att.fileSize && (
<Typography
variant="caption"
sx={{
fontSize: '0.7rem',
color: isTenant ? '#64748b' : 'rgba(255,255,255,0.65)',
ml: 'auto',
}}
>
{formatFileSize(att.fileSize)}
</Typography>
)}
</Box>
))}
</Box>
)}
{isSupply && (
<Typography
variant="caption"
sx={{
display: 'block',
fontSize: '0.65rem',
color: 'rgba(255,255,255,0.6)',
mt: 0.75,
textAlign: 'right',
}}
>
Wincasa AG
</Typography>
)}
</Box>
</Box>
)
}