Files
Lageplan/src/components/journal/rapport-dialog.tsx
Pepe Ziberi 5917fa88ad v1.3.0: Refactoring Phase 3+4, Symbol-Verwaltung Redesign, Schlauch-Labels Fix
- Refactoring: Error Boundaries, apiFetch Wrapper, Socket Status-Tracking
- Refactoring: UI Kontrast (theme-aware colors), unused imports bereinigt
- Symbol-Verwaltung: Neues Split-Panel (Meine Symbole + Bibliothek)
- Symbol-Verwaltung: Umbenennen (TLF rot/blau), Duplikate erlaubt
- Symbol-Verwaltung: Karten-Sidebar zeigt eigene Symbole bevorzugt
- Schlauch-Labels: Groessere Schrift (13px/10px), verschiebbar (Drag)
- Schema: TenantSymbol customName, sortOrder, unique constraint entfernt
- Open Source Referenz entfernt (kostenloses Projekt)
2026-02-25 00:06:39 +01:00

237 lines
11 KiB
TypeScript

'use client'
import { useState, MutableRefObject } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { FileText, Loader2 } from 'lucide-react'
interface RapportDialogProps {
projectId: string
rapportForm: Record<string, any>
setRapportForm: React.Dispatch<React.SetStateAction<Record<string, any>>>
mapRef?: MutableRefObject<any>
mapScreenshot?: string
onClose: () => void
onRapportCreated: (link: string) => void
}
export function RapportDialog({
projectId,
rapportForm,
setRapportForm,
mapRef,
mapScreenshot: preCapuredScreenshot,
onClose,
onRapportCreated,
}: RapportDialogProps) {
const [creatingRapport, setCreatingRapport] = useState(false)
const handleCreate = async () => {
if (!projectId) return
setCreatingRapport(true)
try {
// Capture map screenshot — compress to JPEG and resize for smaller payload
let mapScreenshot = ''
const rawScreenshot = preCapuredScreenshot || ''
if (!rawScreenshot) {
try {
if (mapRef?.current) {
const canvas = mapRef.current.getCanvas()
if (canvas) {
// Resize to max 2400px wide and convert to JPEG
const maxW = 2400
const ratio = Math.min(1, maxW / canvas.width)
const offscreen = document.createElement('canvas')
offscreen.width = Math.round(canvas.width * ratio)
offscreen.height = Math.round(canvas.height * ratio)
const ctx = offscreen.getContext('2d')
if (ctx) {
ctx.drawImage(canvas, 0, 0, offscreen.width, offscreen.height)
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.85)
}
}
}
} catch (e) { console.warn('Map screenshot failed:', e) }
} else if (rawScreenshot.length > 800000) {
// Compress pre-captured screenshot if too large
try {
const img = new Image()
img.src = rawScreenshot
await new Promise(r => { img.onload = r; img.onerror = r })
const maxW = 2400
const ratio = Math.min(1, maxW / img.naturalWidth)
const offscreen = document.createElement('canvas')
offscreen.width = Math.round(img.naturalWidth * ratio)
offscreen.height = Math.round(img.naturalHeight * ratio)
const ctx = offscreen.getContext('2d')
if (ctx) {
ctx.drawImage(img, 0, 0, offscreen.width, offscreen.height)
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.85)
}
} catch { mapScreenshot = rawScreenshot }
} else {
mapScreenshot = rawScreenshot
}
// Convert logo URL to base64 for PDF rendering
let logoDataUri = ''
if (rapportForm.logoUrl) {
try {
const logoRes = await fetch(rapportForm.logoUrl)
if (logoRes.ok) {
const blob = await logoRes.blob()
logoDataUri = await new Promise<string>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.readAsDataURL(blob)
})
}
} catch (e) { console.warn('Logo fetch failed:', e) }
}
const rapportData = { ...rapportForm, mapScreenshot, logoUrl: logoDataUri || rapportForm.logoUrl }
console.log('[Rapport] Sending request, body size ~', JSON.stringify({ projectId, data: rapportData }).length, 'bytes')
const res = await fetch('/api/rapports', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId, data: rapportData }),
})
if (res.ok) {
const result = await res.json()
onRapportCreated(`/rapport/${result.token}`)
onClose()
window.open(`/rapport/${result.token}`, '_blank')
} else {
const errData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
console.error('[Rapport] API error:', res.status, errData)
alert(`Rapport-Fehler: ${errData.error || 'Unbekannter Fehler (Status ' + res.status + ')'}`)
}
} catch (err: any) {
console.error('Error creating rapport:', err)
alert('Rapport-Fehler: ' + (err?.message || 'Netzwerkfehler'))
} finally {
setCreatingRapport(false)
}
}
return (
<div className="fixed inset-0 z-50 bg-black/50 flex items-start justify-center overflow-auto py-8 print:hidden">
<div className="bg-card rounded-lg shadow-2xl w-full max-w-2xl mx-4">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="text-lg font-bold flex items-center gap-2">
<FileText className="w-5 h-5" />
Einsatzrapport erstellen
</h3>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground text-xl leading-none">&times;</button>
</div>
<div className="p-4 space-y-4 max-h-[70vh] overflow-auto">
{/* Organisation */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Organisation</label>
<Input value={rapportForm.organisation || ''} onChange={e => setRapportForm(f => ({ ...f, organisation: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Abteilung</label>
<Input value={rapportForm.abteilung || ''} onChange={e => setRapportForm(f => ({ ...f, abteilung: e.target.value }))} />
</div>
</div>
{/* Einsatzdaten */}
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Datum</label>
<Input value={rapportForm.datum || ''} onChange={e => setRapportForm(f => ({ ...f, datum: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Uhrzeit</label>
<Input value={rapportForm.uhrzeit || ''} onChange={e => setRapportForm(f => ({ ...f, uhrzeit: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Einsatz-Nr.</label>
<Input value={rapportForm.einsatzNr || ''} onChange={e => setRapportForm(f => ({ ...f, einsatzNr: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Priorität</label>
<Input value={rapportForm.prioritaet || ''} onChange={e => setRapportForm(f => ({ ...f, prioritaet: e.target.value }))} placeholder="z.B. Hoch" />
</div>
</div>
{/* Ort */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Einsatzort / Adresse</label>
<Input value={rapportForm.einsatzort || ''} onChange={e => setRapportForm(f => ({ ...f, einsatzort: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Objekt / Gebäude</label>
<Input value={rapportForm.objekt || ''} onChange={e => setRapportForm(f => ({ ...f, objekt: e.target.value }))} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Alarmierungsart</label>
<Input value={rapportForm.alarmierungsart || ''} onChange={e => setRapportForm(f => ({ ...f, alarmierungsart: e.target.value }))} />
</div>
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Stichwort / Meldebild</label>
<Input value={rapportForm.stichwort || ''} onChange={e => setRapportForm(f => ({ ...f, stichwort: e.target.value }))} />
</div>
{/* Zeitverlauf */}
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase mb-1 block">Zeitverlauf</label>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] text-muted-foreground">Alarm</label>
<Input type="time" className="text-sm h-8" value={rapportForm.zeitAlarm || ''} onChange={e => setRapportForm(f => ({ ...f, zeitAlarm: e.target.value }))} />
</div>
<div>
<label className="text-[10px] text-muted-foreground">Eintreffen</label>
<Input type="time" className="text-sm h-8" value={rapportForm.zeitEintreffen || ''} onChange={e => setRapportForm(f => ({ ...f, zeitEintreffen: e.target.value }))} />
</div>
</div>
</div>
{/* Lagebild */}
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Lage bei Eintreffen</label>
<textarea className="w-full border rounded-md px-3 py-2 text-sm min-h-[60px] resize-y" value={rapportForm.lageEintreffen || ''} onChange={e => setRapportForm(f => ({ ...f, lageEintreffen: e.target.value }))} />
</div>
{/* Massnahmen (read-only, from journal) */}
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Massnahmen (aus Journal)</label>
<div className="border rounded-md p-2 bg-muted text-sm max-h-32 overflow-auto">
{Array.isArray(rapportForm.massnahmen) && rapportForm.massnahmen.length > 0 ? (
rapportForm.massnahmen.map((m: string, i: number) => <div key={i} className="py-0.5"> {m}</div>)
) : <span className="text-muted-foreground">Keine Einträge</span>}
</div>
</div>
{/* Bemerkungen */}
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Bemerkungen</label>
<textarea className="w-full border rounded-md px-3 py-2 text-sm min-h-[60px] resize-y" value={rapportForm.bemerkungen || ''} onChange={e => setRapportForm(f => ({ ...f, bemerkungen: e.target.value }))} />
</div>
{/* Unterschriften */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Einsatzleiter/in</label>
<Input value={rapportForm.einsatzleiter || ''} onChange={e => setRapportForm(f => ({ ...f, einsatzleiter: e.target.value }))} />
</div>
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Rapporteur</label>
<Input value={rapportForm.rapporteur || ''} onChange={e => setRapportForm(f => ({ ...f, rapporteur: e.target.value }))} />
</div>
</div>
</div>
<div className="flex items-center justify-end gap-3 p-4 border-t">
<Button variant="outline" size="sm" onClick={onClose}>Abbrechen</Button>
<Button
size="sm"
disabled={creatingRapport}
onClick={handleCreate}
>
{creatingRapport ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <FileText className="w-4 h-4 mr-1.5" />}
Rapport generieren
</Button>
</div>
</div>
</div>
)
}