refactor: split PropertyDetail, Anfragen, AISearch god components

PropertyDetail.tsx: 565→118 lines
- Removed duplicate constants (import from MatchDetailPropertyDetails)
- PropertyDetailPublicSections: Preis/Hauptangaben/Eigenschaften/Wegzeit/Einheiten/Beschreibung/Quelle sections
- PropertyContactForm: Verwaltung kontaktieren form

Anfragen.tsx: 481→320 lines
- anfragenKiDetection.ts: STAGE_ORDER, STAGE_LABELS, KI_RULES, detectKiStage
- AnfragenMessageBubble: chat message bubble component
- AnfragenInquiryItem: inquiry list row component

AISearch.tsx: 398→342 lines
- needSearchMapper.ts: generateSummary + buildNeedInput pure functions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 00:55:55 +02:00
parent 4d4ea6d2ff
commit ae82d0e6a0
9 changed files with 663 additions and 689 deletions
@@ -0,0 +1,70 @@
import { Box, Typography } from '@mui/material'
import { Bot, Building2, FileText } from 'lucide-react'
import type { InquiryMessage } from '../../domain/inquiry'
export function AnfragenMessageBubble({ msg }: { msg: InquiryMessage }) {
const isOwnMessage = msg.senderType === 'tenant'
const isAI = msg.senderType === 'ai'
const time = new Date(msg.createdAt).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })
const date = new Date(msg.createdAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit' })
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: isOwnMessage ? 'flex-end' : 'flex-start', mb: 0.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.375 }}>
{!isOwnMessage && isAI && <Bot size={11} color="#7c3aed" />}
{!isOwnMessage && msg.senderType === 'supply_user' && <Building2 size={11} color="#64748b" />}
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{msg.senderName} · {date} {time}
</Typography>
</Box>
<Box
sx={{
maxWidth: { xs: '88%', md: '72%' },
px: 2, py: 1.25,
borderRadius: isOwnMessage ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
bgcolor: isOwnMessage ? '#1e3a5f' : isAI ? '#f5f3ff' : 'white',
border: isOwnMessage ? 'none' : isAI ? '1px solid #ddd6fe' : '1px solid #e2e8f0',
boxShadow: isOwnMessage ? '0 2px 6px rgba(30,58,95,0.2)' : '0 1px 2px rgba(0,0,0,0.06)',
}}
>
<Typography
variant="body2"
sx={{
color: isOwnMessage ? 'white' : isAI ? '#5b21b6' : '#1e293b',
whiteSpace: 'pre-wrap', lineHeight: 1.65, fontSize: '0.875rem',
}}
>
{msg.body}
</Typography>
{msg.attachments.length > 0 && (
<Box sx={{ mt: 1, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{msg.attachments.map(att => (
<Box
key={att.id}
sx={{
display: 'flex', alignItems: 'center', gap: 1,
px: 1.5, py: 0.75,
bgcolor: isOwnMessage ? 'rgba(255,255,255,0.12)' : '#f1f5f9',
borderRadius: 1.5, cursor: 'pointer',
'&:hover': { bgcolor: isOwnMessage ? 'rgba(255,255,255,0.2)' : '#e2e8f0' },
}}
>
<FileText size={13} color={isOwnMessage ? 'rgba(255,255,255,0.75)' : '#64748b'} />
<Typography variant="caption" sx={{ color: isOwnMessage ? 'rgba(255,255,255,0.9)' : '#475569', flex: 1 }} noWrap>
{att.fileName}
</Typography>
{att.fileSize && (
<Typography variant="caption" sx={{ color: isOwnMessage ? 'rgba(255,255,255,0.5)' : '#94a3b8', flexShrink: 0 }}>
{att.fileSize < 1024 * 1024
? `${Math.round(att.fileSize / 1024)} KB`
: `${(att.fileSize / 1024 / 1024).toFixed(1)} MB`}
</Typography>
)}
</Box>
))}
</Box>
)}
</Box>
</Box>
)
}