import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Box, Container, Typography, Button, Chip, Card, CardContent, Grid, Divider, Avatar, Skeleton, Alert, Stack, Tooltip, Paper, TextField, } from '@mui/material'; import { ArrowBack, Edit, Save, Cancel, LocalFireDepartment, AccessTime, DirectionsCar, People, LocationOn, Description, PictureAsPdf, } from '@mui/icons-material'; import { format, parseISO, differenceInMinutes } from 'date-fns'; import { de } from 'date-fns/locale'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { incidentsApi, EinsatzDetail as EinsatzDetailType, EinsatzStatus, EINSATZ_ART_LABELS, EINSATZ_STATUS_LABELS, EinsatzArt, } from '../services/incidents'; import { useNotification } from '../contexts/NotificationContext'; // --------------------------------------------------------------------------- // COLOUR MAPS // --------------------------------------------------------------------------- const ART_CHIP_COLOR: Record< EinsatzArt, 'error' | 'primary' | 'secondary' | 'warning' | 'success' | 'default' | 'info' > = { Brand: 'error', THL: 'primary', ABC: 'secondary', BMA: 'warning', Hilfeleistung: 'success', Fehlalarm: 'default', Brandsicherheitswache: 'info', }; const STATUS_CHIP_COLOR: Record = { aktiv: 'warning', abgeschlossen: 'success', archiviert: 'default', }; // --------------------------------------------------------------------------- // HELPERS // --------------------------------------------------------------------------- function formatDE(iso: string | null | undefined, fmt = 'dd.MM.yyyy HH:mm'): string { if (!iso) return '—'; try { return format(parseISO(iso), fmt, { locale: de }); } catch { return iso; } } function minuteDiff(start: string | null, end: string | null): string { if (!start || !end) return '—'; try { const mins = differenceInMinutes(parseISO(end), parseISO(start)); if (mins < 0) return '—'; if (mins < 60) return `${mins} min`; const h = Math.floor(mins / 60); const m = mins % 60; return m === 0 ? `${h} h` : `${h} h ${m} min`; } catch { return '—'; } } function initials(givenName: string | null, familyName: string | null, name: string | null): string { if (givenName && familyName) return `${givenName[0]}${familyName[0]}`.toUpperCase(); if (name) return name.slice(0, 2).toUpperCase(); return '??'; } function displayName(p: EinsatzDetailType['personal'][0]): string { if (p.given_name && p.family_name) return `${p.given_name} ${p.family_name}`; if (p.name) return p.name; return p.email; } // --------------------------------------------------------------------------- // TIMELINE STEP // --------------------------------------------------------------------------- interface TimelineStepProps { label: string; time: string | null; duration?: string; isFirst?: boolean; } const TimelineStep: React.FC = ({ label, time, duration, isFirst }) => ( {!isFirst && ( )} {label} {time ? formatDE(time) : 'Nicht erfasst'} {duration && ( +{duration} )} ); // --------------------------------------------------------------------------- // MAIN PAGE // --------------------------------------------------------------------------- function EinsatzDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const notification = useNotification(); const [einsatz, setEinsatz] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Edit mode for bericht fields const [editing, setEditing] = useState(false); const [berichtKurz, setBerichtKurz] = useState(''); const [berichtText, setBerichtText] = useState(''); const [saving, setSaving] = useState(false); // ------------------------------------------------------------------------- // FETCH // ------------------------------------------------------------------------- const fetchEinsatz = useCallback(async () => { if (!id) return; setLoading(true); setError(null); try { const data = await incidentsApi.getById(id); setEinsatz(data); setBerichtKurz(data.bericht_kurz ?? ''); setBerichtText(data.bericht_text ?? ''); } catch (err) { setError('Einsatz konnte nicht geladen werden.'); } finally { setLoading(false); } }, [id]); useEffect(() => { fetchEinsatz(); }, [fetchEinsatz]); // ------------------------------------------------------------------------- // SAVE BERICHT // ------------------------------------------------------------------------- const handleSaveBericht = async () => { if (!id) return; setSaving(true); try { await incidentsApi.update(id, { bericht_kurz: berichtKurz || null, bericht_text: berichtText || null, }); notification.showSuccess('Einsatzbericht gespeichert'); setEditing(false); fetchEinsatz(); } catch (err) { notification.showError('Fehler beim Speichern des Berichts'); } finally { setSaving(false); } }; const handleCancelEdit = () => { setEditing(false); setBerichtKurz(einsatz?.bericht_kurz ?? ''); setBerichtText(einsatz?.bericht_text ?? ''); }; // ------------------------------------------------------------------------- // PDF EXPORT (placeholder) // ------------------------------------------------------------------------- const handleExportPdf = () => { notification.showInfo('PDF-Export wird in Kürze verfügbar sein.'); }; // ------------------------------------------------------------------------- // LOADING STATE // ------------------------------------------------------------------------- if (loading) { return ( {[0, 1, 2].map((i) => ( ))} ); } if (error || !einsatz) { return ( {error ?? 'Einsatz nicht gefunden.'} ); } const address = [einsatz.strasse, einsatz.hausnummer, einsatz.ort] .filter(Boolean) .join(' '); return ( {/* Back + Actions */} {!editing ? ( ) : ( <> )} {/* HEADER */} } label={EINSATZ_ART_LABELS[einsatz.einsatz_art]} color={ART_CHIP_COLOR[einsatz.einsatz_art]} sx={{ fontWeight: 600 }} /> {einsatz.einsatz_stichwort && ( {einsatz.einsatz_stichwort} )} Einsatz {einsatz.einsatz_nr} {address && ( {address} )} {/* LEFT COLUMN: Timeline + Vehicles */} {/* Timeline card */} Zeitlinie {/* Reversed order: last step first (top = Alarm) */} {(einsatz.hilfsfrist_min !== null || einsatz.dauer_min !== null) && ( <> {einsatz.hilfsfrist_min !== null && ( Hilfsfrist 10 ? 'error.main' : 'success.main'}> {einsatz.hilfsfrist_min} min )} {einsatz.dauer_min !== null && ( Gesamtdauer {einsatz.dauer_min < 60 ? `${einsatz.dauer_min} min` : `${Math.floor(einsatz.dauer_min / 60)} h ${einsatz.dauer_min % 60} min`} )} )} {/* Vehicles card */} Fahrzeuge {einsatz.fahrzeuge.length === 0 ? ( Keine Fahrzeuge zugewiesen ) : ( {einsatz.fahrzeuge.map((f) => ( {f.bezeichnung} {f.kennzeichen} {f.fahrzeug_typ ? ` · ${f.fahrzeug_typ}` : ''} {(f.ausrueck_time || f.einrueck_time) && ( {formatDE(f.ausrueck_time, 'HH:mm')} → {formatDE(f.einrueck_time, 'HH:mm')} )} ))} )} {/* RIGHT COLUMN: Personnel + Bericht */} {/* Personnel card */} Einsatzkräfte {einsatz.einsatzleiter_name && ( Einsatzleiter {einsatz.einsatzleiter_name} )} {einsatz.personal.length === 0 ? ( Keine Einsatzkräfte zugewiesen ) : ( {einsatz.personal.map((p) => ( {initials(p.given_name, p.family_name, p.name)} {displayName(p)} ))} )} {/* Bericht card */} Einsatzbericht {editing ? ( setBerichtKurz(e.target.value)} fullWidth multiline rows={2} inputProps={{ maxLength: 255 }} helperText={`${berichtKurz.length}/255`} /> setBerichtText(e.target.value)} fullWidth multiline rows={8} placeholder="Detaillierter Einsatzbericht..." helperText="Nur für Kommandant und Admin sichtbar" /> ) : ( Kurzbeschreibung {einsatz.bericht_kurz ?? ( Keine Kurzbeschreibung erfasst )} {einsatz.bericht_text !== undefined && ( Ausführlicher Bericht {einsatz.bericht_text ? ( {einsatz.bericht_text} ) : ( Kein Bericht erfasst )} )} )} {/* Footer meta */} Angelegt: {formatDE(einsatz.created_at, 'dd.MM.yyyy HH:mm')} {' · '} Zuletzt geändert: {formatDE(einsatz.updated_at, 'dd.MM.yyyy HH:mm')} {' · '} Alarmierung via {einsatz.alarmierung_art} ); } export default EinsatzDetail;