Files
property-match/src/components/anfragencenter/PreparationWizard.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

295 lines
11 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import {
Box, Button, Checkbox, CircularProgress, Dialog, DialogContent,
FormControlLabel, LinearProgress, Stack, Step, StepLabel, Stepper,
TextField, Typography,
} from '@mui/material'
import { Download, Send, X } from 'lucide-react'
import type { Inquiry } from '../../domain/inquiry'
import type { InquiryPreparationReportDraft, ReportObjectFieldSelection } from '../../domain/inquiryReport'
import { useCreateInquiryReport, useUpdateInquiryReport, useFinalizeInquiryReport } from '../../hooks/useInquiryReport'
import { useToastStore } from '../../stores/toastStore'
import { ReportObjectFieldSelector } from './ReportObjectFieldSelector'
import { LatentInquiryReportPreview } from './LatentInquiryReportPreview'
import { useProperties } from '../../hooks/useProperties'
import { ResultType } from '../../domain/enums'
interface Props {
inquiryId: string
inquiry: Inquiry
onClose: () => void
}
const STEPS = ['Objekte wählen', 'Bericht erstellen', 'Prüfen & bearbeiten', 'Finalisieren']
export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
const [step, setStep] = useState(0)
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [draft, setDraft] = useState<InquiryPreparationReportDraft | null>(null)
const [generating, setGenerating] = useState(false)
const [progress, setProgress] = useState(0)
const { data: allProperties = [] } = useProperties()
const showToast = useToastStore(s => s.showToast)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const createInquiryReport = useCreateInquiryReport()
const updateInquiryReport = useUpdateInquiryReport()
const finalizeInquiryReport = useFinalizeInquiryReport()
const finalizing = finalizeInquiryReport.isPending
const portfolioProps = allProperties.filter(p => p.resultType === ResultType.VERIFIED_PORTFOLIO)
const toggleProperty = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id],
)
}
const handleGenerate = () => {
setStep(1)
setGenerating(true)
setProgress(0)
let p = 0
timerRef.current = setInterval(() => {
p += Math.random() * 20 + 10
if (p >= 100) {
clearInterval(timerRef.current!)
setProgress(100)
setGenerating(false)
createInquiryReport.mutate(
{ inquiryId, selectedPropertyIds: selectedIds },
{
onSuccess: (d) => {
setDraft(d)
setStep(2)
},
},
)
} else {
setProgress(Math.min(100, p))
}
}, 200)
}
useEffect(() => () => { if (timerRef.current) clearInterval(timerRef.current) }, [])
const handleUpdateField = (fieldId: string, value: string) => {
if (!draft) return
setDraft({
...draft,
editableFields: draft.editableFields.map(f => f.id === fieldId ? { ...f, value } : f),
})
}
const handleUpdateFieldSelection = (propertyId: string, sel: ReportObjectFieldSelection) => {
if (!draft) return
setDraft({
...draft,
fieldSelections: draft.fieldSelections.map(fs => fs.propertyId === propertyId ? sel : fs),
})
}
const handleFinalize = () => {
if (!draft) return
updateInquiryReport.mutate(
{ draftId: draft.id, data: { editableFields: draft.editableFields, fieldSelections: draft.fieldSelections } },
{
onSuccess: () => {
finalizeInquiryReport.mutate(draft.id, {
onSuccess: (finalized) => {
setDraft(finalized)
setStep(3)
},
})
},
},
)
}
return (
<Dialog open fullWidth maxWidth="lg" slotProps={{ paper: { sx: { height: '90vh', display: 'flex', flexDirection: 'column' } } }}>
<Box sx={{ px: 3, pt: 2.5, pb: 1.5, borderBottom: '1px solid #e2e8f0', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Vorbereitung starten</Typography>
<Button size="small" onClick={onClose} sx={{ minWidth: 0, p: 0.5 }}><X size={18} /></Button>
</Box>
<Box sx={{ px: 3, py: 2, flexShrink: 0 }}>
<Stepper activeStep={step} alternativeLabel>
{STEPS.map(label => (
<Step key={label}><StepLabel>{label}</StepLabel></Step>
))}
</Stepper>
</Box>
<DialogContent sx={{ flex: 1, overflow: 'auto', px: 3 }}>
{/* Step 0: Select properties */}
{step === 0 && (
<Box>
<Typography variant="body2" sx={{ color: '#64748b', mb: 2 }}>
Wählen Sie die Objekte aus Ihrem Portfolio, die Sie dem Interessenten vorstellen möchten:
</Typography>
<Stack spacing={1}>
{portfolioProps.map(p => (
<FormControlLabel
key={p.id}
control={
<Checkbox
checked={selectedIds.includes(p.id)}
onChange={() => toggleProperty(p.id)}
/>
}
label={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{p.title}</Typography>
<Typography variant="caption" sx={{ color: '#64748b' }}>
{p.location.city} · {p.areaSqm.toLocaleString('de-CH')} m² · CHF {p.rentPricePerSqm}/m²/Jahr
</Typography>
</Box>
}
sx={{ border: '1px solid #e2e8f0', borderRadius: 1, p: 1, m: 0, alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0 } }}
/>
))}
</Stack>
</Box>
)}
{/* Step 1: Generating */}
{step === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 3 }}>
{generating ? (
<>
<CircularProgress size={48} sx={{ color: '#152642' }} />
<Typography variant="body1" sx={{ fontWeight: 600, color: '#152642' }}>
Bericht wird erstellt
</Typography>
<Box sx={{ width: '100%', maxWidth: 400 }}>
<LinearProgress variant="determinate" value={progress} sx={{ height: 6, borderRadius: 3 }} />
<Typography variant="caption" sx={{ color: '#64748b', display: 'block', textAlign: 'center', mt: 1 }}>
{Math.round(progress)}%
</Typography>
</Box>
</>
) : (
<Typography variant="body1">Bericht erstellt. Weiterleitung</Typography>
)}
</Box>
)}
{/* Step 2: Review & Edit */}
{step === 2 && draft && (
<Box sx={{ display: 'flex', gap: 3 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 2 }}>Berichtsfelder bearbeiten</Typography>
<Stack spacing={2} sx={{ mb: 3 }}>
{draft.editableFields.map(f => (
<TextField
key={f.id}
label={f.label}
value={f.value}
onChange={e => handleUpdateField(f.id, e.target.value)}
multiline={f.fieldType === 'textarea'}
rows={f.fieldType === 'textarea' ? 3 : 1}
size="small"
fullWidth
/>
))}
</Stack>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1.5 }}>Felder pro Objekt</Typography>
<Stack spacing={2}>
{draft.fieldSelections.map(fs => {
const prop = allProperties.find(p => p.id === fs.propertyId)
if (!prop) return null
return (
<ReportObjectFieldSelector
key={fs.propertyId}
propertyTitle={prop.title}
value={fs}
onChange={sel => handleUpdateFieldSelection(fs.propertyId, sel)}
/>
)
})}
</Stack>
</Box>
<Box sx={{ width: 380, flexShrink: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 2 }}>Vorschau</Typography>
<Box sx={{ overflow: 'auto', maxHeight: 600 }}>
<LatentInquiryReportPreview draft={draft} inquiry={inquiry} properties={allProperties} />
</Box>
</Box>
</Box>
)}
{/* Step 3: Finalized */}
{step === 3 && draft && (
<Box sx={{ display: 'flex', gap: 3 }}>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<LatentInquiryReportPreview draft={draft} inquiry={inquiry} properties={allProperties} />
</Box>
<Box sx={{ width: 200, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}>
<Button
variant="contained"
fullWidth
startIcon={<Download size={16} />}
onClick={() => showToast('PDF wird heruntergeladen…', 'info')}
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
>
Herunterladen
</Button>
<Button
variant="outlined"
fullWidth
startIcon={<Send size={16} />}
onClick={() => {
showToast('Bericht als Anhang hinzugefügt', 'success')
onClose()
}}
sx={{ textTransform: 'none' }}
>
Als Anhang senden
</Button>
</Box>
</Box>
)}
</DialogContent>
{/* Footer navigation */}
<Box sx={{ px: 3, py: 2, borderTop: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', flexShrink: 0 }}>
<Button onClick={onClose} sx={{ textTransform: 'none', color: '#64748b' }}>
Abbrechen
</Button>
<Box sx={{ display: 'flex', gap: 1 }}>
{step === 2 && (
<Button variant="outlined" onClick={() => setStep(0)} sx={{ textTransform: 'none' }}>
Zurück
</Button>
)}
{step === 0 && (
<Button
variant="contained"
disabled={selectedIds.length === 0}
onClick={handleGenerate}
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
>
Weiter
</Button>
)}
{step === 2 && (
<Button
variant="contained"
disabled={finalizing}
onClick={handleFinalize}
startIcon={finalizing ? <CircularProgress size={14} sx={{ color: 'white' }} /> : undefined}
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
>
Finalisieren
</Button>
)}
</Box>
</Box>
</Dialog>
)
}