b0f675c319
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 <img> 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 <noreply@anthropic.com>
217 lines
6.7 KiB
TypeScript
217 lines
6.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import {
|
|
Box,
|
|
Button,
|
|
CircularProgress,
|
|
IconButton,
|
|
TextField,
|
|
Typography,
|
|
} from '@mui/material'
|
|
import { Paperclip, Send, Sparkles, X } from 'lucide-react'
|
|
import { useSendInquiryReply } from '../../hooks/useInquiries'
|
|
import { useToastStore } from '../../stores/toastStore'
|
|
import type { Attachment } from '../../domain/inquiry'
|
|
import { formatFileSize } from './inquiryUtils'
|
|
|
|
interface InquiryReplyComposerProps {
|
|
inquiryId: string
|
|
defaultSubject: string
|
|
tenantName?: string
|
|
onSent?: () => void
|
|
pendingAttachment?: Attachment | null
|
|
onPendingAttachmentConsumed?: () => void
|
|
}
|
|
|
|
export function InquiryReplyComposer({
|
|
inquiryId,
|
|
defaultSubject,
|
|
tenantName,
|
|
onSent,
|
|
pendingAttachment,
|
|
onPendingAttachmentConsumed,
|
|
}: InquiryReplyComposerProps) {
|
|
const [subject, setSubject] = useState(
|
|
defaultSubject.startsWith('Re:') ? defaultSubject : `Re: ${defaultSubject}`,
|
|
)
|
|
const [body, setBody] = useState('')
|
|
const [attachments, setAttachments] = useState<Attachment[]>([])
|
|
const [isAIDraft, setIsAIDraft] = useState(false)
|
|
|
|
const sendReply = useSendInquiryReply()
|
|
const showToast = useToastStore(s => s.showToast)
|
|
|
|
useEffect(() => {
|
|
if (pendingAttachment) {
|
|
setAttachments(prev => {
|
|
if (prev.find(a => a.id === pendingAttachment.id)) return prev
|
|
return [...prev, pendingAttachment]
|
|
})
|
|
onPendingAttachmentConsumed?.()
|
|
}
|
|
}, [pendingAttachment, onPendingAttachmentConsumed])
|
|
|
|
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 => [
|
|
...prev,
|
|
{
|
|
id: crypto.randomUUID(),
|
|
fileName: name,
|
|
fileType: 'application/pdf',
|
|
fileSize: 240_000 + Math.floor(Math.random() * 800_000),
|
|
},
|
|
])
|
|
}
|
|
|
|
const handleRemoveAttachment = (id: string) => {
|
|
setAttachments(prev => prev.filter(a => a.id !== id))
|
|
}
|
|
|
|
const handleSend = async () => {
|
|
if (!body.trim()) {
|
|
showToast('Bitte geben Sie eine Nachricht ein', 'warning')
|
|
return
|
|
}
|
|
const result = await sendReply.mutateAsync({
|
|
inquiryId,
|
|
payload: { subject, body, attachments },
|
|
})
|
|
if (result.error) {
|
|
showToast(`Fehler: ${result.error}`, 'error')
|
|
return
|
|
}
|
|
showToast('Antwort gesendet', 'success')
|
|
setBody('')
|
|
setAttachments([])
|
|
setIsAIDraft(false)
|
|
onSent?.()
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
borderTop: '1px solid #e2e8f0',
|
|
bgcolor: '#f8fafc',
|
|
p: 2,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 1.25,
|
|
}}
|
|
>
|
|
<TextField
|
|
size="small"
|
|
label="Betreff"
|
|
value={subject}
|
|
onChange={e => setSubject(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
|
|
{isAIDraft && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
|
<Sparkles size={12} color="#7c3aed" />
|
|
<Typography variant="caption" sx={{ color: '#7c3aed', fontSize: '0.7rem', fontWeight: 500 }}>
|
|
KI-Entwurf — bitte prüfen und anpassen
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<TextField
|
|
size="small"
|
|
label="Nachricht"
|
|
value={body}
|
|
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 && (
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
|
|
{attachments.map(a => (
|
|
<Box
|
|
key={a.id}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 0.75,
|
|
border: '1px solid',
|
|
borderColor: a.generated ? '#bfdbfe' : '#cbd5e1',
|
|
borderRadius: 1,
|
|
px: 1,
|
|
py: 0.5,
|
|
fontSize: '0.75rem',
|
|
bgcolor: a.generated ? '#eff6ff' : 'white',
|
|
}}
|
|
>
|
|
<Paperclip size={12} color={a.generated ? '#2563eb' : undefined} />
|
|
<Typography variant="caption" sx={{ fontSize: '0.75rem', color: a.generated ? '#1d4ed8' : undefined }}>
|
|
{a.fileName}
|
|
</Typography>
|
|
{a.fileSize && (
|
|
<Typography variant="caption" sx={{ fontSize: '0.7rem', color: '#64748b' }}>
|
|
{formatFileSize(a.fileSize)}
|
|
</Typography>
|
|
)}
|
|
<IconButton size="small" onClick={() => handleRemoveAttachment(a.id)} sx={{ p: 0.25 }}>
|
|
<X size={12} />
|
|
</IconButton>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: 'space-between' }}>
|
|
<Box sx={{ display: 'flex', gap: 0.75 }}>
|
|
<Button
|
|
size="small"
|
|
startIcon={<Sparkles size={13} />}
|
|
onClick={handleAIDraft}
|
|
sx={{
|
|
textTransform: 'none',
|
|
fontSize: '0.75rem',
|
|
color: '#7c3aed',
|
|
borderColor: '#ddd6fe',
|
|
'&:hover': { bgcolor: '#f5f3ff', borderColor: '#c4b5fd' },
|
|
}}
|
|
variant="outlined"
|
|
>
|
|
KI-Entwurf
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
startIcon={<Paperclip size={13} />}
|
|
onClick={handleAddMockAttachment}
|
|
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
|
>
|
|
Anhang
|
|
</Button>
|
|
</Box>
|
|
<Button
|
|
variant="contained"
|
|
size="small"
|
|
startIcon={
|
|
sending ? <CircularProgress size={14} sx={{ color: 'white' }} /> : <Send size={14} />
|
|
}
|
|
onClick={handleSend}
|
|
disabled={sending || !body.trim()}
|
|
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
|
>
|
|
Antwort senden
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|