From b0f675c3197e8c58d2720fc09ab7eff6cecf040b Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Wed, 10 Jun 2026 23:43:29 +0200 Subject: [PATCH] refine(anfragencenter): property bar top, matches-only bottom, AI draft button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout: property context bar (44x44 thumbnail + title + key facts + Ansehen) sits between inquiry header and chat. Bottom strip now shows only the 2-card matches slider — no duplicate property info. Images use objectFit:cover via for consistent non-distorted rendering. Match score shown as badge overlay on image. Reply composer gains KI-Entwurf button (Sparkles, purple) that fills a salutation-prefixed template; an inline label marks it as AI-generated until the user edits. Co-Authored-By: Claude Sonnet 4.6 --- src/components/anfragencenter/InquiryChat.tsx | 1 + .../anfragencenter/InquiryDetailPanel.tsx | 90 ++++++- .../anfragencenter/InquiryReplyComposer.tsx | 62 ++++- .../RelatedPropertyCardPanel.tsx | 253 +++++++----------- 4 files changed, 234 insertions(+), 172 deletions(-) diff --git a/src/components/anfragencenter/InquiryChat.tsx b/src/components/anfragencenter/InquiryChat.tsx index 88bd2ae..3ce0e0c 100644 --- a/src/components/anfragencenter/InquiryChat.tsx +++ b/src/components/anfragencenter/InquiryChat.tsx @@ -56,6 +56,7 @@ export function InquiryChat({ diff --git a/src/components/anfragencenter/InquiryDetailPanel.tsx b/src/components/anfragencenter/InquiryDetailPanel.tsx index c87b2a4..2c197fc 100644 --- a/src/components/anfragencenter/InquiryDetailPanel.tsx +++ b/src/components/anfragencenter/InquiryDetailPanel.tsx @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react' import { Box, Button, CircularProgress, Typography } from '@mui/material' -import { ClipboardList } from 'lucide-react' +import { ArrowRight, Building2, ClipboardList, MapPin, Ruler } from 'lucide-react' +import { useNavigate } from 'react-router' import { useInquiryById, useMarkThreadAsRead } from '../../hooks/useInquiries' +import { usePropertyById } from '../../hooks/useProperties' import { InquiryChat } from './InquiryChat' import { RelatedPropertyCardPanel } from './RelatedPropertyCardPanel' import type { Attachment } from '../../domain/inquiry' @@ -12,7 +14,9 @@ interface InquiryDetailPanelProps { export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) { const { data: inquiry, isLoading } = useInquiryById(inquiryId) + const { data: property } = usePropertyById(inquiry?.propertyId ?? '') const markRead = useMarkThreadAsRead() + const navigate = useNavigate() const [preparationOpen, setPreparationOpen] = useState(false) const [offerWizardOpen, setOfferWizardOpen] = useState(false) const [pendingAttachment, setPendingAttachment] = useState(null) @@ -43,10 +47,13 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) { } const isLatentInquiry = !!inquiry.needId + const propertyImage = property?.images?.[0] return ( <> + + {/* Header: tenant + subject + action */} - + {/* Property context bar */} + + + {propertyImage ? ( + + ) : ( + + )} + + + + {property?.title ?? '—'} + + + + + {property?.location.city} + {property?.areaSqm ? ` · ${property.areaSqm.toLocaleString('de-CH')} m²` : ''} + {property?.rentPricePerSqm ? ` · CHF ${property.rentPricePerSqm}/m²/J.` : ''} + + + + + + + {/* Chat (flex: 1) */} + setOfferWizardOpen(true)} pendingAttachment={pendingAttachment} onPendingAttachmentConsumed={() => setPendingAttachment(null)} /> - - - + + + {/* Bottom: weitere passende Objekte */} + + diff --git a/src/components/anfragencenter/InquiryReplyComposer.tsx b/src/components/anfragencenter/InquiryReplyComposer.tsx index 38615a9..7980363 100644 --- a/src/components/anfragencenter/InquiryReplyComposer.tsx +++ b/src/components/anfragencenter/InquiryReplyComposer.tsx @@ -7,7 +7,7 @@ import { TextField, Typography, } from '@mui/material' -import { Send, Paperclip, X } from 'lucide-react' +import { Paperclip, Send, Sparkles, X } from 'lucide-react' import { useSendInquiryReply } from '../../hooks/useInquiries' import { useToastStore } from '../../stores/toastStore' import type { Attachment } from '../../domain/inquiry' @@ -16,6 +16,7 @@ import { formatFileSize } from './inquiryUtils' interface InquiryReplyComposerProps { inquiryId: string defaultSubject: string + tenantName?: string onSent?: () => void pendingAttachment?: Attachment | null onPendingAttachmentConsumed?: () => void @@ -24,6 +25,7 @@ interface InquiryReplyComposerProps { export function InquiryReplyComposer({ inquiryId, defaultSubject, + tenantName, onSent, pendingAttachment, onPendingAttachmentConsumed, @@ -33,6 +35,7 @@ export function InquiryReplyComposer({ ) const [body, setBody] = useState('') const [attachments, setAttachments] = useState([]) + const [isAIDraft, setIsAIDraft] = useState(false) const sendReply = useSendInquiryReply() const showToast = useToastStore(s => s.showToast) @@ -49,6 +52,14 @@ export function InquiryReplyComposer({ const sending = sendReply.isPending + function handleAIDraft() { + const salutation = tenantName ? `Sehr geehrte/r ${tenantName}` : 'Sehr geehrte Damen und Herren' + setBody( + `${salutation},\n\nVielen Dank für Ihre Anfrage. Gerne bestätigen wir, dass das Objekt zum gewünschten Zeitpunkt verfügbar ist.\n\nWir würden Ihnen gerne einen Besichtigungstermin vorschlagen — bitte teilen Sie uns Ihre Verfügbarkeit mit, damit wir einen passenden Termin finden können.\n\nFür Rückfragen stehen wir Ihnen jederzeit gerne zur Verfügung.\n\nMit freundlichen Grüssen` + ) + setIsAIDraft(true) + } + const handleAddMockAttachment = () => { const name = `Anhang_${attachments.length + 1}.pdf` setAttachments(prev => [ @@ -82,6 +93,7 @@ export function InquiryReplyComposer({ showToast('Antwort gesendet', 'success') setBody('') setAttachments([]) + setIsAIDraft(false) onSent?.() } @@ -103,15 +115,26 @@ export function InquiryReplyComposer({ onChange={e => setSubject(e.target.value)} fullWidth /> + + {isAIDraft && ( + + + + KI-Entwurf — bitte prüfen und anpassen + + + )} + setBody(e.target.value)} + onChange={e => { setBody(e.target.value); setIsAIDraft(false) }} multiline rows={5} placeholder="Antwort verfassen..." fullWidth + sx={isAIDraft ? { '& .MuiOutlinedInput-root': { borderColor: '#ddd6fe' }, '& fieldset': { borderColor: '#ddd6fe !important' } } : {}} /> {attachments.length > 0 && ( @@ -149,15 +172,32 @@ export function InquiryReplyComposer({ )} - - + + + + + - - - - - {/* Vertical divider */} - - - {/* Right: match slider showing 2 at a time */} - - - - Weitere passende Objekte - - {additionalMatches.length > 2 && ( - - setSliderIndex(i => i - 1)} - sx={{ p: 0.25, color: canGoBack ? '#152642' : '#cbd5e1' }} - > - - - setSliderIndex(i => i + 1)} - sx={{ p: 0.25, color: canGoForward ? '#152642' : '#cbd5e1' }} - > - - - - )} - - - {loadingMatches ? ( - - - - ) : additionalMatches.length === 0 ? ( - - Keine weiteren Matches - - ) : ( - - {visibleMatches.map(m => ( - - ))} - - )} - - - ) - } - return ( - {!image && } + {image ? ( + + ) : ( + + )} @@ -277,36 +212,50 @@ function MatchSliderCard({ match }: { match: AdditionalPropertyMatch }) { }, }} > - - {!match.imageUrl && } + {/* Image with consistent aspect ratio */} + + {match.imageUrl ? ( + + ) : ( + + + + )} + {/* Score badge overlay */} + + {match.matchScore}% + - - - {match.title} - - - {match.matchScore}% - - + + {match.title} + - {match.location} + {match.location} · {match.areaSqm.toLocaleString('de-CH')} m² {match.reasons[0] && ( - + + {match.reasons[0]} )}