Files
property-match/src/components/anfragencenter/OfferChatComposer.tsx
T
Benjamin Sutter da50f3b5ea feat: premium redesign — DM Serif, refined palette, flat score badges
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette
  (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent),
  flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography
- Card components: white-background cards, DM Serif score numbers, max-2 badge
  chips with +N overflow tooltip, editorial score badge positioning
- Layout shell: gold left-accent nav active state, 64px top bar, outlined
  workspace chip, DM Serif page titles
- Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar
  (simplified chip styles), DecisionContextPanel (dot metrics, no left accent)
- Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4,
  #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:26:30 +02:00

201 lines
7.1 KiB
TypeScript

import { Box, Button, CircularProgress, IconButton, TextField, Typography } from '@mui/material'
import { ArrowLeft, FileText, Paperclip, Send, X } from 'lucide-react'
import { useOfferWizardStore } from '../../stores/offerWizardStore'
import { useSendOffer } from '../../hooks/useOffers'
import { useToastStore } from '../../stores/toastStore'
import { AiOfferEmailButton } from './AiOfferEmailButton'
import { mockProperties } from '../../mock-data/properties'
import { deterministicMatchScore } from './latentNeedUtils'
import { formatFileSize } from './inquiryUtils'
export function OfferChatComposer() {
const subject = useOfferWizardStore(s => s.messageSubject)
const body = useOfferWizardStore(s => s.messageDraft)
const setSubject = useOfferWizardStore(s => s.setMessageSubject)
const setBody = useOfferWizardStore(s => s.setMessageDraft)
const attachments = useOfferWizardStore(s => s.attachments)
const addAttachment = useOfferWizardStore(s => s.addAttachment)
const removeAttachment = useOfferWizardStore(s => s.removeAttachment)
const offerDraftId = useOfferWizardStore(s => s.offerDraftId)
const selectedPropertyIds = useOfferWizardStore(s => s.selectedPropertyIds)
const needId = useOfferWizardStore(s => s.selectedNeedId)
const needTitle = useOfferWizardStore(s => s.needTitle)
const setStep = useOfferWizardStore(s => s.setStep)
const reset = useOfferWizardStore(s => s.reset)
const sendOffer = useSendOffer()
const showToast = useToastStore(s => s.showToast)
const propertyTitles = selectedPropertyIds.map(id => {
const p = mockProperties.find(pp => pp.id === id)
return p?.title ?? id
})
const scores = needId
? selectedPropertyIds.map(id => deterministicMatchScore(id, needId))
: []
const handleAiGenerated = (newSubject: string, newBody: string) => {
setSubject(newSubject)
setBody(newBody)
showToast('KI-Vorschlag eingefügt', 'success')
}
const handleAddAttachment = () => {
addAttachment({
id: crypto.randomUUID(),
fileName: `Anhang_${attachments.length + 1}.pdf`,
fileType: 'application/pdf',
fileSize: 200_000 + Math.floor(Math.random() * 600_000),
})
}
const handleSend = async () => {
if (!offerDraftId) return
if (!body.trim()) {
showToast('Bitte Nachricht eingeben', 'warning')
return
}
const res = await sendOffer.mutateAsync(offerDraftId)
if (res.error) {
showToast(`Fehler: ${res.error}`, 'error')
return
}
showToast('Angebot erfolgreich gesendet', 'success')
reset()
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box sx={{ flex: 1, overflowY: 'auto', p: 3, bgcolor: '#f8fafc' }}>
<Box sx={{ maxWidth: 720, mx: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box>
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5 }}>
Empfänger-Kontext
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', color: '#1e293b', mt: 0.5 }}>
Bedarf: <strong>{needTitle}</strong> · {selectedPropertyIds.length} Objekt
{selectedPropertyIds.length === 1 ? '' : 'e'} im Angebot
</Typography>
</Box>
<TextField
size="small"
label="Betreff"
fullWidth
value={subject}
onChange={e => setSubject(e.target.value)}
/>
<TextField
size="small"
label="Nachricht"
fullWidth
value={body}
onChange={e => setBody(e.target.value)}
multiline
rows={8}
/>
{attachments.length > 0 && (
<Box>
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, mb: 0.75, display: 'block' }}>
Anhänge
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{attachments.map(a => (
<Box
key={a.id}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
bgcolor: 'white',
border: '1px solid #cbd5e1',
borderRadius: 1,
px: 1,
py: 0.75,
}}
>
<FileText size={14} color="#152642" />
<Typography variant="caption" sx={{ fontSize: '0.8rem', fontWeight: 500 }}>
{a.fileName}
</Typography>
{a.generated && (
<Box
sx={{
px: 0.75,
py: 0.125,
bgcolor: '#e0e7ff',
color: '#3730a3',
fontWeight: 700,
fontSize: '0.65rem',
borderRadius: 1,
}}
>
PDF
</Box>
)}
<Typography variant="caption" sx={{ fontSize: '0.75rem', color: '#64748b', ml: 'auto' }}>
{formatFileSize(a.fileSize)}
</Typography>
<IconButton size="small" onClick={() => removeAttachment(a.id)} sx={{ p: 0.25 }}>
<X size={12} />
</IconButton>
</Box>
))}
</Box>
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Button
size="small"
startIcon={<Paperclip size={14} />}
onClick={handleAddAttachment}
sx={{ textTransform: 'none' }}
>
Anhang
</Button>
<AiOfferEmailButton
needTitle={needTitle}
selectedProperties={propertyTitles}
matchScores={scores}
onGenerated={handleAiGenerated}
/>
</Box>
</Box>
</Box>
<Box
sx={{
p: 2,
borderTop: '1px solid #e2e8f0',
bgcolor: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0,
}}
>
<Button startIcon={<ArrowLeft size={14} />} onClick={() => setStep('checked')} sx={{ textTransform: 'none' }}>
Zurück
</Button>
<Button
variant="contained"
startIcon={sendOffer.isPending ? <CircularProgress size={14} sx={{ color: 'white' }} /> : <Send size={14} />}
onClick={handleSend}
disabled={sendOffer.isPending || !body.trim()}
sx={{
textTransform: 'none',
bgcolor: '#16a34a',
fontWeight: 600,
'&:hover': { bgcolor: '#15803d' },
}}
>
Absenden
</Button>
</Box>
</Box>
)
}