fix: replace toast with mock PDF generation dialog in Marktsignale tab

BerichtDialog now animates a progress bar (idle → generating → ready),
then triggers a blob download of Marktsignal-Bericht_{id}.pdf on confirm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-19 15:53:15 +02:00
parent 920ce8eb09
commit c0f50344e0
@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react'
import { Alert, Box, Button, Chip, CircularProgress, LinearProgress, Typography } from '@mui/material'
import { BarChart2, Building2, FileText, Lightbulb, TrendingUp } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Alert, Box, Button, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, LinearProgress, Typography } from '@mui/material'
import { BarChart2, Building2, CheckCircle, Download, FileText, Lightbulb, TrendingUp } from 'lucide-react'
import { marketReportService } from '../../services/marketReportService'
import { useToastStore } from '../../stores/toastStore'
import type { MarketReport, PropertyMarketSignal, SignalCategory } from '../../domain/marketReport'
interface Props {
@@ -94,10 +93,133 @@ function SignalCard({ signal }: { signal: PropertyMarketSignal }) {
)
}
// ── PDF generation dialog ─────────────────────────────────────────────────────
type PdfState = 'idle' | 'generating' | 'ready'
interface BerichtDialogProps {
open: boolean
onClose: () => void
report: MarketReport | null
propertyId: string
}
function BerichtDialog({ open, onClose, report, propertyId }: BerichtDialogProps) {
const [state, setState] = useState<PdfState>('idle')
const [progress, setProgress] = useState(0)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
if (!open) { setState('idle'); setProgress(0); return }
setState('generating')
setProgress(0)
let p = 0
timerRef.current = setInterval(() => {
p += Math.random() * 18 + 8
if (p >= 100) {
p = 100
clearInterval(timerRef.current!)
setTimeout(() => setState('ready'), 300)
}
setProgress(Math.min(100, p))
}, 200)
return () => { if (timerRef.current) clearInterval(timerRef.current) }
}, [open])
function handleDownload() {
const signals = report?.signals ?? []
const lines: string[] = [
'MARKTSIGNAL-BERICHT',
'====================',
`Objekt-ID: ${propertyId}`,
`Erstellt: ${new Date().toLocaleString('de-CH')}`,
'',
]
for (const s of signals) {
lines.push(`[${s.category.toUpperCase()}] ${s.title}`)
lines.push(s.body)
if (s.source) lines.push(`Quelle: ${s.source}`)
lines.push('')
}
const blob = new Blob([lines.join('\n')], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `Marktsignal-Bericht_${propertyId}.pdf`
a.click()
URL.revokeObjectURL(url)
onClose()
}
return (
<Dialog open={open} onClose={state === 'generating' ? undefined : onClose} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 700, fontSize: '1rem', pb: 1 }}>
Bericht erstellen
</DialogTitle>
<DialogContent>
{state === 'generating' && (
<Box sx={{ py: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
<CircularProgress size={20} />
<Typography variant="body2" sx={{ color: '#374151' }}>
PDF-Bericht wird generiert
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{ height: 6, borderRadius: 3, bgcolor: '#e2e8f0', '& .MuiLinearProgress-bar': { bgcolor: '#1e3a5f' } }}
/>
<Typography variant="caption" sx={{ color: '#94a3b8', mt: 0.75, display: 'block' }}>
{Math.round(progress)}% Marktsignale werden aufbereitet
</Typography>
</Box>
)}
{state === 'ready' && (
<Box sx={{ py: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
<CheckCircle size={20} color="#1a7a4a" />
<Typography variant="body2" sx={{ color: '#166534', fontWeight: 600 }}>
Bericht ist bereit
</Typography>
</Box>
<Box sx={{ p: 2, bgcolor: '#f8fafc', borderRadius: 1.5, border: '1px solid #e2e8f0', mb: 2 }}>
<Typography variant="caption" sx={{ color: '#64748b', display: 'block' }}>Dateiname</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
Marktsignal-Bericht_{propertyId}.pdf
</Typography>
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mt: 0.5 }}>
{report?.signals.length ?? 0} Signale · {new Date().toLocaleDateString('de-CH')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
fullWidth
variant="contained"
startIcon={<Download size={15} />}
onClick={handleDownload}
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
>
Herunterladen
</Button>
<Button fullWidth variant="outlined" onClick={onClose} sx={{ textTransform: 'none' }}>
Schliessen
</Button>
</Box>
</Box>
)}
</DialogContent>
</Dialog>
)
}
// ── Main tab component ────────────────────────────────────────────────────────
export function PropertyMarketSignalsTab({ propertyId }: Props) {
const [report, setReport] = useState<MarketReport | null>(null)
const [loading, setLoading] = useState(true)
const showToast = useToastStore(s => s.showToast)
const [dialogOpen, setDialogOpen] = useState(false)
useEffect(() => {
let cancelled = false