import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Container, Typography, Button, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, TextField, Grid, Card, CardContent, Skeleton, IconButton, Tooltip, Alert, Stack, } from '@mui/material'; import { Add as AddIcon, LocalFireDepartment, Build, Warning, CheckCircle, Refresh, FilterList, } from '@mui/icons-material'; import { format, parseISO } from 'date-fns'; import { de } from 'date-fns/locale'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import IncidentStatsChart from '../components/incidents/IncidentStatsChart'; import { incidentsApi, EinsatzListItem, EinsatzStats, EinsatzArt, EinsatzStatus, EINSATZ_ARTEN, EINSATZ_ART_LABELS, EINSATZ_STATUS_LABELS, } from '../services/incidents'; import CreateEinsatzDialog from '../components/incidents/CreateEinsatzDialog'; // --------------------------------------------------------------------------- // COLOUR MAP for Einsatzart chips // --------------------------------------------------------------------------- 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< EinsatzStatus, 'warning' | 'success' | 'default' > = { aktiv: 'warning', abgeschlossen: 'success', archiviert: 'default', }; // --------------------------------------------------------------------------- // HELPER // --------------------------------------------------------------------------- function formatDE(iso: string, fmt = 'dd.MM.yyyy HH:mm'): string { try { return format(parseISO(iso), fmt, { locale: de }); } catch { return iso; } } function durationLabel(min: number | null): string { if (min === null || min < 0) return '—'; if (min < 60) return `${min} min`; const h = Math.floor(min / 60); const m = min % 60; return m === 0 ? `${h} h` : `${h} h ${m} min`; } // --------------------------------------------------------------------------- // STATS SUMMARY BAR // --------------------------------------------------------------------------- interface StatsSummaryProps { stats: EinsatzStats | null; loading: boolean; } function StatsSummaryBar({ stats, loading }: StatsSummaryProps) { const items = [ { label: 'Einsätze gesamt (Jahr)', value: stats ? String(stats.gesamt) : '—', icon: , }, { label: 'Häufigste Einsatzart', value: stats?.haeufigste_art ? EINSATZ_ART_LABELS[stats.haeufigste_art] : '—', icon: , }, { label: 'Ø Hilfsfrist', value: stats?.avg_hilfsfrist_min !== null && stats?.avg_hilfsfrist_min !== undefined ? `${stats.avg_hilfsfrist_min} min` : '—', icon: , }, { label: 'Abgeschlossen', value: stats ? String(stats.abgeschlossen) : '—', icon: , }, ]; if (loading) { return ( {[0, 1, 2, 3].map((i) => ( ))} ); } return ( {items.map((item) => ( {item.icon} {item.label} {item.value} ))} ); } // --------------------------------------------------------------------------- // MAIN PAGE // --------------------------------------------------------------------------- function Einsaetze() { const navigate = useNavigate(); // List state const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(25); const [listLoading, setListLoading] = useState(true); const [listError, setListError] = useState(null); // Filters const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [selectedArts, setSelectedArts] = useState([]); // Stats const [stats, setStats] = useState(null); const [statsLoading, setStatsLoading] = useState(true); // Dialog const [createOpen, setCreateOpen] = useState(false); // ------------------------------------------------------------------------- // DATA FETCHING // ------------------------------------------------------------------------- const fetchList = useCallback(async () => { setListLoading(true); setListError(null); try { const filters: Record = { limit: rowsPerPage, offset: page * rowsPerPage, }; if (dateFrom) filters.dateFrom = new Date(dateFrom).toISOString(); if (dateTo) { // Set to end of day for dateTo const end = new Date(dateTo); end.setHours(23, 59, 59, 999); filters.dateTo = end.toISOString(); } if (selectedArts.length === 1) filters.einsatzArt = selectedArts[0]; const result = await incidentsApi.getAll(filters as Parameters[0]); setItems(result.items); setTotal(result.total); } catch (err) { setListError('Fehler beim Laden der Einsätze. Bitte Seite neu laden.'); } finally { setListLoading(false); } }, [page, rowsPerPage, dateFrom, dateTo, selectedArts]); const fetchStats = useCallback(async () => { setStatsLoading(true); try { const s = await incidentsApi.getStats(); setStats(s); } catch { // Stats failure is non-critical } finally { setStatsLoading(false); } }, []); useEffect(() => { fetchList(); }, [fetchList]); useEffect(() => { fetchStats(); }, [fetchStats]); // ------------------------------------------------------------------------- // HANDLERS // ------------------------------------------------------------------------- const handlePageChange = (_: unknown, newPage: number) => setPage(newPage); const handleRowsPerPageChange = (e: React.ChangeEvent) => { setRowsPerPage(parseInt(e.target.value, 10)); setPage(0); }; const toggleArtFilter = (art: EinsatzArt) => { setSelectedArts((prev) => prev.includes(art) ? prev.filter((a) => a !== art) : [...prev, art] ); setPage(0); }; const handleResetFilters = () => { setDateFrom(''); setDateTo(''); setSelectedArts([]); setPage(0); }; const handleRowClick = (id: string) => { navigate(`/einsaetze/${id}`); }; const handleCreateSuccess = () => { setCreateOpen(false); fetchList(); fetchStats(); }; // ------------------------------------------------------------------------- // RENDER // ------------------------------------------------------------------------- return ( {/* Page header */} Einsatzübersicht Einsatzprotokoll — Feuerwehr Rems {/* KPI summary cards */} {/* Charts */} {/* Filters */} Filter {(dateFrom || dateTo || selectedArts.length > 0) && ( )} { setDateFrom(e.target.value); setPage(0); }} InputLabelProps={{ shrink: true }} size="small" fullWidth inputProps={{ 'aria-label': 'Von-Datum' }} /> { setDateTo(e.target.value); setPage(0); }} InputLabelProps={{ shrink: true }} size="small" fullWidth inputProps={{ 'aria-label': 'Bis-Datum' }} /> {/* Einsatzart chips */} {EINSATZ_ARTEN.map((art) => ( toggleArtFilter(art)} clickable /> ))} {/* Error state */} {listError && ( setListError(null)}> {listError} )} {/* Incident table */} Datum / Uhrzeit Nr. Einsatzart Stichwort Ort Hilfsfrist Dauer Status Einsatzleiter Kräfte {listLoading ? Array.from({ length: rowsPerPage > 10 ? 10 : rowsPerPage }).map((_, i) => ( {Array.from({ length: 10 }).map((__, j) => ( ))} )) : items.length === 0 ? ( Keine Einsätze gefunden ) : items.map((row) => ( handleRowClick(row.id)} sx={{ cursor: 'pointer' }} > {formatDE(row.alarm_time)} {row.einsatz_nr} {row.einsatz_stichwort ?? '—'} {[row.strasse, row.ort].filter(Boolean).join(', ') || '—'} {durationLabel(row.hilfsfrist_min)} {durationLabel(row.dauer_min)} {row.einsatzleiter_name ?? '—'} {row.personal_count > 0 ? row.personal_count : '—'} ))}
`${from}–${to} von ${count !== -1 ? count : `mehr als ${to}`}` } />
{/* Create dialog */} setCreateOpen(false)} onSuccess={handleCreateSuccess} />
); } export default Einsaetze;