import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { Alert, Box, Button, Card, CardContent, Checkbox, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fab, FormControl, FormControlLabel, Grid, InputAdornment, InputLabel, MenuItem, Select, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, Check, Close, Edit, Delete, Search, } from '@mui/icons-material'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { atemschutzApi } from '../services/atemschutz'; import { membersService } from '../services/members'; import { useNotification } from '../contexts/NotificationContext'; import type { AtemschutzUebersicht, AtemschutzStats, CreateAtemschutzPayload, UpdateAtemschutzPayload, UntersuchungErgebnis, } from '../types/atemschutz.types'; import { UntersuchungErgebnisLabel } from '../types/atemschutz.types'; import type { MemberListItem } from '../types/member.types'; // ── Helpers ────────────────────────────────────────────────────────────────── function formatDate(iso: string | null): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }); } function getDisplayName(item: AtemschutzUebersicht): string { if (item.user_family_name || item.user_given_name) { return [item.user_family_name, item.user_given_name].filter(Boolean).join(', '); } return item.user_name || item.user_email; } type ValidityColor = 'success.main' | 'error.main' | 'warning.main' | 'text.secondary'; function getValidityColor( gueltigBis: string | null, tageRest: number | null, soonThresholdDays: number ): ValidityColor { if (!gueltigBis || tageRest === null) return 'text.secondary'; if (tageRest < 0) return 'error.main'; if (tageRest <= soonThresholdDays) return 'warning.main'; return 'success.main'; } // ── Initial form state ─────────────────────────────────────────────────────── interface AtemschutzFormState { user_id: string; atemschutz_lehrgang: boolean; lehrgang_datum: string; untersuchung_datum: string; untersuchung_gueltig_bis: string; untersuchung_ergebnis: UntersuchungErgebnis | ''; leistungstest_datum: string; leistungstest_gueltig_bis: string; leistungstest_bestanden: boolean; bemerkung: string; } const EMPTY_FORM: AtemschutzFormState = { user_id: '', atemschutz_lehrgang: false, lehrgang_datum: '', untersuchung_datum: '', untersuchung_gueltig_bis: '', untersuchung_ergebnis: '', leistungstest_datum: '', leistungstest_gueltig_bis: '', leistungstest_bestanden: false, bemerkung: '', }; // ── Stats Card ─────────────────────────────────────────────────────────────── interface StatCardProps { label: string; value: number; color?: string; bgcolor?: string; } const StatCard: React.FC = ({ label, value, color, bgcolor }) => ( {value} {label} ); // ── Main Page ──────────────────────────────────────────────────────────────── function Atemschutz() { const notification = useNotification(); // Data state const [traeger, setTraeger] = useState([]); const [stats, setStats] = useState(null); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Filter state const [search, setSearch] = useState(''); // Dialog state const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState({ ...EMPTY_FORM }); const [dialogLoading, setDialogLoading] = useState(false); const [dialogError, setDialogError] = useState(null); // Delete confirmation const [deleteId, setDeleteId] = useState(null); const [deleteLoading, setDeleteLoading] = useState(false); // ── Data loading ───────────────────────────────────────────────────────── const fetchData = useCallback(async () => { try { setLoading(true); setError(null); const [traegerData, statsData, membersData] = await Promise.all([ atemschutzApi.getAll(), atemschutzApi.getStats(), membersService.getMembers({ pageSize: 500 }), ]); setTraeger(traegerData); setStats(statsData); setMembers(membersData.items); } catch { setError('Atemschutzdaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.'); } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); // ── Filtering ──────────────────────────────────────────────────────────── const filtered = useMemo(() => { if (!search.trim()) return traeger; const q = search.toLowerCase(); return traeger.filter((item) => { const name = getDisplayName(item).toLowerCase(); const email = item.user_email.toLowerCase(); const dienstgrad = (item.dienstgrad || '').toLowerCase(); return name.includes(q) || email.includes(q) || dienstgrad.includes(q); }); }, [traeger, search]); // Members who do not already have an Atemschutz record const availableMembers = useMemo(() => { const existingUserIds = new Set(traeger.map((t) => t.user_id)); return members.filter((m) => !existingUserIds.has(m.id)); }, [members, traeger]); // ── Dialog handlers ────────────────────────────────────────────────────── const handleOpenCreate = () => { setEditingId(null); setForm({ ...EMPTY_FORM }); setDialogError(null); setDialogOpen(true); }; const handleOpenEdit = (item: AtemschutzUebersicht) => { setEditingId(item.id); setForm({ user_id: item.user_id, atemschutz_lehrgang: item.atemschutz_lehrgang, lehrgang_datum: item.lehrgang_datum || '', untersuchung_datum: item.untersuchung_datum || '', untersuchung_gueltig_bis: item.untersuchung_gueltig_bis || '', untersuchung_ergebnis: item.untersuchung_ergebnis || '', leistungstest_datum: item.leistungstest_datum || '', leistungstest_gueltig_bis: item.leistungstest_gueltig_bis || '', leistungstest_bestanden: item.leistungstest_bestanden || false, bemerkung: item.bemerkung || '', }); setDialogError(null); setDialogOpen(true); }; const handleDialogClose = () => { setDialogOpen(false); setEditingId(null); setForm({ ...EMPTY_FORM }); setDialogError(null); }; const handleFormChange = ( field: keyof AtemschutzFormState, value: string | boolean ) => { setForm((prev) => ({ ...prev, [field]: value })); }; const handleSubmit = async () => { setDialogError(null); if (!editingId && !form.user_id) { setDialogError('Bitte ein Mitglied auswählen.'); return; } setDialogLoading(true); try { if (editingId) { const payload: UpdateAtemschutzPayload = { atemschutz_lehrgang: form.atemschutz_lehrgang, lehrgang_datum: form.lehrgang_datum || undefined, untersuchung_datum: form.untersuchung_datum || undefined, untersuchung_gueltig_bis: form.untersuchung_gueltig_bis || undefined, untersuchung_ergebnis: (form.untersuchung_ergebnis as UntersuchungErgebnis) || undefined, leistungstest_datum: form.leistungstest_datum || undefined, leistungstest_gueltig_bis: form.leistungstest_gueltig_bis || undefined, leistungstest_bestanden: form.leistungstest_bestanden, bemerkung: form.bemerkung || undefined, }; await atemschutzApi.update(editingId, payload); notification.showSuccess('Atemschutzträger erfolgreich aktualisiert.'); } else { const payload: CreateAtemschutzPayload = { user_id: form.user_id, atemschutz_lehrgang: form.atemschutz_lehrgang, lehrgang_datum: form.lehrgang_datum || undefined, untersuchung_datum: form.untersuchung_datum || undefined, untersuchung_gueltig_bis: form.untersuchung_gueltig_bis || undefined, untersuchung_ergebnis: (form.untersuchung_ergebnis as UntersuchungErgebnis) || undefined, leistungstest_datum: form.leistungstest_datum || undefined, leistungstest_gueltig_bis: form.leistungstest_gueltig_bis || undefined, leistungstest_bestanden: form.leistungstest_bestanden, bemerkung: form.bemerkung || undefined, }; await atemschutzApi.create(payload); notification.showSuccess('Atemschutzträger erfolgreich angelegt.'); } handleDialogClose(); fetchData(); } catch (err: any) { const msg = err?.message || 'Ein Fehler ist aufgetreten.'; setDialogError(msg); notification.showError(msg); } finally { setDialogLoading(false); } }; // ── Delete handlers ────────────────────────────────────────────────────── const handleDeleteConfirm = async () => { if (!deleteId) return; setDeleteLoading(true); try { await atemschutzApi.delete(deleteId); notification.showSuccess('Atemschutzträger erfolgreich gelöscht.'); setDeleteId(null); fetchData(); } catch (err: any) { notification.showError(err?.message || 'Löschen fehlgeschlagen.'); } finally { setDeleteLoading(false); } }; // ── Render ─────────────────────────────────────────────────────────────── return ( {/* Header */} Atemschutzverwaltung {!loading && stats && ( {stats.total} Gesamt {'·'} {stats.einsatzbereit} Einsatzbereit {'·'} 0 ? 'error.main' : 'text.secondary'} fontWeight={stats.untersuchungAbgelaufen > 0 ? 600 : 400} > {stats.untersuchungAbgelaufen} Untersuchung abgelaufen )} {/* Stats cards */} {!loading && stats && ( )} {/* Search bar */} setSearch(e.target.value)} size="small" sx={{ minWidth: 280, maxWidth: 480, width: '100%' }} InputProps={{ startAdornment: ( ), }} /> {/* Loading state */} {loading && ( )} {/* Error state */} {!loading && error && ( Erneut versuchen } > {error} )} {/* Empty state */} {!loading && !error && filtered.length === 0 && ( {traeger.length === 0 ? 'Keine Atemschutzträger vorhanden' : 'Keine Ergebnisse gefunden'} )} {/* Table */} {!loading && !error && filtered.length > 0 && ( Name Dienstgrad Lehrgang Untersuchung gültig bis Leistungstest gültig bis Status Aktionen {filtered.map((item) => { const untersuchungColor = getValidityColor( item.untersuchung_gueltig_bis, item.untersuchung_tage_rest, 90 ); const leistungstestColor = getValidityColor( item.leistungstest_gueltig_bis, item.leistungstest_tage_rest, 30 ); return ( {getDisplayName(item)} {item.user_email} {item.dienstgrad || '—'} {item.atemschutz_lehrgang ? ( ) : ( )} {formatDate(item.untersuchung_gueltig_bis)} {formatDate(item.leistungstest_gueltig_bis)} ); })}
)} {/* FAB to create */} {/* ── Add / Edit Dialog ───────────────────────────────────────────── */} {editingId ? 'Atemschutzträger bearbeiten' : 'Neuen Atemschutzträger anlegen'} {dialogError && ( {dialogError} )} {/* User selection (only when creating) */} {!editingId && ( Mitglied )} {/* Lehrgang */} Lehrgang handleFormChange('atemschutz_lehrgang', e.target.checked)} /> } label="Lehrgang absolviert" /> handleFormChange('lehrgang_datum', e.target.value)} InputLabelProps={{ shrink: true }} /> {/* Untersuchung */} Untersuchung handleFormChange('untersuchung_datum', e.target.value)} InputLabelProps={{ shrink: true }} /> handleFormChange('untersuchung_gueltig_bis', e.target.value)} InputLabelProps={{ shrink: true }} /> Ergebnis {/* Leistungstest */} Leistungstest handleFormChange('leistungstest_datum', e.target.value)} InputLabelProps={{ shrink: true }} /> handleFormChange('leistungstest_gueltig_bis', e.target.value)} InputLabelProps={{ shrink: true }} /> handleFormChange('leistungstest_bestanden', e.target.checked)} /> } label="Leistungstest bestanden" /> {/* Bemerkung */} handleFormChange('bemerkung', e.target.value)} /> {/* ── Delete Confirmation Dialog ──────────────────────────────────── */} setDeleteId(null)}> Atemschutzträger löschen Soll dieser Atemschutzträger-Eintrag wirklich gelöscht werden? Diese Aktion kann nicht rückgängig gemacht werden.
); } export default Atemschutz;