a825672197
- Properties page: collapsible decision/filter strip (localStorage state), slim single-row Datenpflege status bar (chips + button), 4-column card grid - PropertyIntelligenceCard: reduced image height (160→120px), tighter body padding - OfferWizard (latente Anfragen): new "Felder wählen" step between property selection and PDF preview; uses ReportObjectFieldSelector per property - MockPdfPreview: adds second PDF page showing selected properties side by side with photo, title, location and all chosen hard/soft fact fields - offerWizardStore: adds select_fields step type and fieldSelections state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { Box, Button, CircularProgress, Typography } from '@mui/material'
|
|
import { ArrowLeft, Send } from 'lucide-react'
|
|
import { useOfferWizardStore } from '../../stores/offerWizardStore'
|
|
import { useUpdateOfferField, useGeneratePdfPreview, useMarkOfferChecked } from '../../hooks/useOffers'
|
|
import { useToastStore } from '../../stores/toastStore'
|
|
import { MockPdfPreview } from './MockPdfPreview'
|
|
import { EditableOfferFieldList } from './EditableOfferFieldList'
|
|
import type { OfferDraft, OfferEditableField } from '../../domain/offer'
|
|
|
|
export function OfferPdfReviewStep() {
|
|
const offerDraftId = useOfferWizardStore(s => s.offerDraftId)
|
|
const editableFieldsStore = useOfferWizardStore(s => s.editableFields)
|
|
const updateField = useOfferWizardStore(s => s.updateField)
|
|
const setStep = useOfferWizardStore(s => s.setStep)
|
|
const setPdfReady = useOfferWizardStore(s => s.setPdfReady)
|
|
const pdfReady = useOfferWizardStore(s => s.pdfPreviewReady)
|
|
|
|
const [draft, setDraft] = useState<OfferDraft | null>(null)
|
|
const [loadingDraft, setLoadingDraft] = useState(true)
|
|
|
|
const updateFieldMut = useUpdateOfferField()
|
|
const genPreview = useGeneratePdfPreview()
|
|
const markChecked = useMarkOfferChecked()
|
|
const showToast = useToastStore(s => s.showToast)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
async function load() {
|
|
if (!offerDraftId) return
|
|
setLoadingDraft(true)
|
|
const draftRes = await import('../../provider/MockupOfferProvider').then(m =>
|
|
m.MockupOfferProvider.getById(offerDraftId),
|
|
)
|
|
if (cancelled) return
|
|
setDraft(draftRes)
|
|
setLoadingDraft(false)
|
|
|
|
// Trigger PDF generation once
|
|
if (draftRes && !pdfReady) {
|
|
const res = await genPreview.mutateAsync(offerDraftId)
|
|
if (!cancelled && res.data) setPdfReady()
|
|
}
|
|
}
|
|
void load()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [offerDraftId])
|
|
|
|
const fallback: Record<string, string> = useMemo(() => {
|
|
const out: Record<string, string> = {}
|
|
if (draft) {
|
|
for (const f of draft.editableFields) out[f.id] = f.value
|
|
}
|
|
return out
|
|
}, [draft])
|
|
|
|
const fields: OfferEditableField[] = draft?.editableFields ?? []
|
|
|
|
const handleFieldChange = async (id: string, value: string) => {
|
|
updateField(id, value)
|
|
if (offerDraftId) {
|
|
await updateFieldMut.mutateAsync({ offerDraftId, fieldId: id, value })
|
|
}
|
|
}
|
|
|
|
const fieldSelections = useOfferWizardStore(s => s.fieldSelections)
|
|
|
|
const needTitle = useOfferWizardStore(s => s.needTitle)
|
|
const addAttachment = useOfferWizardStore(s => s.addAttachment)
|
|
const setMessageDraft = useOfferWizardStore(s => s.setMessageDraft)
|
|
const setMessageSubject = useOfferWizardStore(s => s.setMessageSubject)
|
|
|
|
const handleNext = async () => {
|
|
if (!offerDraftId) return
|
|
if (!pdfReady) {
|
|
showToast('PDF-Vorschau wird noch generiert...', 'info')
|
|
return
|
|
}
|
|
const res = await markChecked.mutateAsync(offerDraftId)
|
|
if (res.error) {
|
|
showToast(`Fehler: ${res.error}`, 'error')
|
|
return
|
|
}
|
|
addAttachment({
|
|
id: crypto.randomUUID(),
|
|
fileName: `Angebot_${needTitle.replace(/\s+/g, '_')}.pdf`,
|
|
fileType: 'application/pdf',
|
|
fileSize: 320_000,
|
|
generated: true,
|
|
})
|
|
setMessageSubject(`Passende Gewerbeflächen zu Ihrer Anfrage: ${needTitle}`)
|
|
setMessageDraft(
|
|
`Sehr geehrte Damen und Herren,\n\nbitte finden Sie anbei unser Angebot zu Ihrem Bedarf "${needTitle}". Gerne stehen wir für Rückfragen und Besichtigungstermine zur Verfügung.\n\nFreundliche Grüsse\nWincasa AG`,
|
|
)
|
|
setStep('send')
|
|
}
|
|
|
|
if (loadingDraft || !draft) {
|
|
return (
|
|
<Box sx={{ p: 4, display: 'flex', justifyContent: 'center' }}>
|
|
<CircularProgress size={24} />
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
|
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden', bgcolor: '#f1f5f9' }}>
|
|
<Box sx={{ flex: 1.4, p: 2, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
|
{!pdfReady ? (
|
|
<Box
|
|
sx={{
|
|
flex: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 2,
|
|
bgcolor: 'white',
|
|
borderRadius: 1.5,
|
|
border: '1px dashed #cbd5e1',
|
|
}}
|
|
>
|
|
<CircularProgress size={24} />
|
|
<Typography variant="body2" color="text.secondary">
|
|
PDF-Vorschau wird generiert...
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<MockPdfPreview fields={editableFieldsStore} fallbackFields={fallback} fieldSelections={fieldSelections} />
|
|
)}
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
width: 360,
|
|
minWidth: 360,
|
|
flexShrink: 0,
|
|
p: 2,
|
|
bgcolor: 'white',
|
|
borderLeft: '1px solid #e2e8f0',
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', mb: 1.5, fontSize: '0.9rem' }}>
|
|
Inhalte bearbeiten
|
|
</Typography>
|
|
<EditableOfferFieldList
|
|
fields={fields}
|
|
values={editableFieldsStore}
|
|
onChange={handleFieldChange}
|
|
/>
|
|
</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('select_properties')}
|
|
sx={{ textTransform: 'none' }}
|
|
>
|
|
Zurück
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
endIcon={markChecked.isPending ? <CircularProgress size={14} sx={{ color: 'white' }} /> : <Send size={14} />}
|
|
onClick={handleNext}
|
|
disabled={!pdfReady || markChecked.isPending}
|
|
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
|
>
|
|
{markChecked.isPending ? 'Wird geprüft…' : 'Angebot senden →'}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|