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
}
onClick={() => setCreateOpen(true)}
>
Neuer Einsatz
{/* 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;