Files
dashboard/frontend/src/pages/Atemschutz.tsx

793 lines
30 KiB
TypeScript

import React, { useEffect, useState, useCallback, useMemo } from 'react';
import {
Alert,
Box,
Button,
Card,
CardContent,
Checkbox,
Chip,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
Grid,
InputAdornment,
InputLabel,
MenuItem,
Select,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import {
Add,
Check,
Close,
Edit,
Delete,
Search,
} from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { DataTable } from '../components/templates';
import type { Column } from '../components/templates';
import { atemschutzApi } from '../services/atemschutz';
import { membersService } from '../services/members';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { toGermanDate, fromGermanDate, isValidGermanDate } from '../utils/dateInput';
import type {
AtemschutzUebersicht,
AtemschutzStats,
CreateAtemschutzPayload,
UpdateAtemschutzPayload,
UntersuchungErgebnis,
} from '../types/atemschutz.types';
import { UntersuchungErgebnisLabel } from '../types/atemschutz.types';
import { ConfirmDialog } from '../components/templates';
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<StatCardProps> = ({ label, value, color, bgcolor }) => (
<Card sx={{ bgcolor: bgcolor || 'background.paper', height: '100%' }}>
<CardContent sx={{ textAlign: 'center', py: 2 }}>
<Typography variant="h3" fontWeight={700} color={color || 'text.primary'}>
{value}
</Typography>
<Typography variant="body2" color={color ? color : 'text.secondary'} sx={{ mt: 0.5 }}>
{label}
</Typography>
</CardContent>
</Card>
);
// ── Main Page ────────────────────────────────────────────────────────────────
function Atemschutz() {
const notification = useNotification();
const { hasPermission } = usePermissionContext();
const canViewAll = hasPermission('atemschutz:view');
const canWrite = hasPermission('atemschutz:create');
// Data state
const [traeger, setTraeger] = useState<AtemschutzUebersicht[]>([]);
const [stats, setStats] = useState<AtemschutzStats | null>(null);
const [members, setMembers] = useState<MemberListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Filter state
const [search, setSearch] = useState('');
// Dialog state
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [form, setForm] = useState<AtemschutzFormState>({ ...EMPTY_FORM });
const [dialogLoading, setDialogLoading] = useState(false);
const [dialogError, setDialogError] = useState<string | null>(null);
const [dateErrors, setDateErrors] = useState<Partial<Record<keyof AtemschutzFormState, string>>>({});
// Delete confirmation
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
// ── Data loading ─────────────────────────────────────────────────────────
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
if (canViewAll) {
const [traegerData, statsData, membersData] = await Promise.all([
atemschutzApi.getAll(),
atemschutzApi.getStats(),
membersService.getMembers({ pageSize: 500 }),
]);
setTraeger(traegerData);
setStats(statsData);
setMembers(membersData.items);
} else {
const traegerData = await atemschutzApi.getAll();
setTraeger(traegerData);
}
} catch {
setError('Atemschutzdaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.');
} finally {
setLoading(false);
}
}, [canViewAll]);
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();
return name.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: toGermanDate(item.lehrgang_datum),
untersuchung_datum: toGermanDate(item.untersuchung_datum),
untersuchung_gueltig_bis: toGermanDate(item.untersuchung_gueltig_bis),
untersuchung_ergebnis: item.untersuchung_ergebnis || '',
leistungstest_datum: toGermanDate(item.leistungstest_datum),
leistungstest_gueltig_bis: toGermanDate(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);
setDateErrors({});
};
const handleFormChange = (
field: keyof AtemschutzFormState,
value: string | boolean
) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
/** Normalize dates before submit: parse DD.MM.YYYY → YYYY-MM-DD for API */
const normalizeDate = (val: string | undefined): string | undefined => {
if (!val) return undefined;
const iso = fromGermanDate(val);
return iso || undefined;
};
const handleSubmit = async () => {
setDialogError(null);
setDateErrors({});
if (!editingId && !form.user_id) {
setDialogError('Bitte ein Mitglied auswählen.');
return;
}
// Validate date fields
const newDateErrors: Partial<Record<keyof AtemschutzFormState, string>> = {};
const dateFields: (keyof AtemschutzFormState)[] = [
'lehrgang_datum', 'untersuchung_datum', 'untersuchung_gueltig_bis',
'leistungstest_datum', 'leistungstest_gueltig_bis',
];
for (const field of dateFields) {
const val = form[field] as string;
if (val && !isValidGermanDate(val)) {
newDateErrors[field] = 'Ungültiges Datum (Format: TT.MM.JJJJ)';
}
}
if (form.untersuchung_datum && form.untersuchung_gueltig_bis &&
isValidGermanDate(form.untersuchung_datum) && isValidGermanDate(form.untersuchung_gueltig_bis)) {
const from = new Date(fromGermanDate(form.untersuchung_datum)!);
const to = new Date(fromGermanDate(form.untersuchung_gueltig_bis)!);
if (to < from) {
newDateErrors['untersuchung_gueltig_bis'] = 'Muss nach dem Untersuchungsdatum liegen';
}
}
if (form.leistungstest_datum && form.leistungstest_gueltig_bis &&
isValidGermanDate(form.leistungstest_datum) && isValidGermanDate(form.leistungstest_gueltig_bis)) {
const from = new Date(fromGermanDate(form.leistungstest_datum)!);
const to = new Date(fromGermanDate(form.leistungstest_gueltig_bis)!);
if (to < from) {
newDateErrors['leistungstest_gueltig_bis'] = 'Muss nach dem Leistungstestdatum liegen';
}
}
if (Object.keys(newDateErrors).length > 0) {
setDateErrors(newDateErrors);
return;
}
setDialogLoading(true);
try {
if (editingId) {
const payload: UpdateAtemschutzPayload = {
atemschutz_lehrgang: form.atemschutz_lehrgang,
lehrgang_datum: normalizeDate(form.lehrgang_datum || undefined),
untersuchung_datum: normalizeDate(form.untersuchung_datum || undefined),
untersuchung_gueltig_bis: normalizeDate(form.untersuchung_gueltig_bis || undefined),
untersuchung_ergebnis: (form.untersuchung_ergebnis as UntersuchungErgebnis) || null,
leistungstest_datum: normalizeDate(form.leistungstest_datum || undefined),
leistungstest_gueltig_bis: normalizeDate(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: normalizeDate(form.lehrgang_datum || undefined),
untersuchung_datum: normalizeDate(form.untersuchung_datum || undefined),
untersuchung_gueltig_bis: normalizeDate(form.untersuchung_gueltig_bis || undefined),
untersuchung_ergebnis: (form.untersuchung_ergebnis as UntersuchungErgebnis) || undefined,
leistungstest_datum: normalizeDate(form.leistungstest_datum || undefined),
leistungstest_gueltig_bis: normalizeDate(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 (
<DashboardLayout>
<Container maxWidth="lg">
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
Atemschutzverwaltung
</Typography>
{!loading && !canViewAll && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Dein persönlicher Atemschutz-Status
</Typography>
)}
{!loading && stats && canViewAll && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
<Typography variant="body2" color="text.secondary">
{stats.total} Gesamt
</Typography>
<Typography variant="body2" color="text.secondary">{'·'}</Typography>
<Typography variant="body2" color="success.main" fontWeight={600}>
{stats.einsatzbereit} Einsatzbereit
</Typography>
<Typography variant="body2" color="text.secondary">{'·'}</Typography>
<Typography
variant="body2"
color={stats.untersuchungAbgelaufen > 0 ? 'error.main' : 'text.secondary'}
fontWeight={stats.untersuchungAbgelaufen > 0 ? 600 : 400}
>
{stats.untersuchungAbgelaufen} Untersuchung abgelaufen
</Typography>
</Box>
)}
</Box>
</Box>
{/* Stats cards */}
{!loading && stats && canViewAll && (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6} sm={3}>
<StatCard
label="Einsatzbereit"
value={stats.einsatzbereit}
color="#fff"
bgcolor="success.main"
/>
</Grid>
<Grid item xs={6} sm={3}>
<StatCard label="Lehrgang absolviert" value={stats.mitLehrgang} />
</Grid>
<Grid item xs={6} sm={3}>
<StatCard label="Untersuchung gültig" value={stats.untersuchungGueltig} />
</Grid>
<Grid item xs={6} sm={3}>
<StatCard label="Leistungstest gültig" value={stats.leistungstestGueltig} />
</Grid>
</Grid>
)}
{/* Search bar */}
{canViewAll && (
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Suchen (Name, E-Mail, Dienstgrad...)"
value={search}
onChange={(e) => setSearch(e.target.value)}
size="small"
sx={{ minWidth: 280, maxWidth: 480, width: '100%' }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
/>
</Box>
)}
{/* Loading state */}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
)}
{/* Error state */}
{!loading && error && (
<Alert
severity="error"
sx={{ mb: 2 }}
action={
<Button color="inherit" size="small" onClick={fetchData}>
Erneut versuchen
</Button>
}
>
{error}
</Alert>
)}
{/* Empty state */}
{!loading && !error && filtered.length === 0 && (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary">
{traeger.length === 0
? 'Keine Atemschutzträger vorhanden'
: 'Keine Ergebnisse gefunden'}
</Typography>
</Box>
)}
{/* Table */}
{!loading && !error && filtered.length > 0 && (() => {
const columns: Column<AtemschutzUebersicht>[] = [
{ key: 'user_name', label: 'Name', render: (item) => (
<Typography variant="body2" fontWeight={500}>{getDisplayName(item)}</Typography>
)},
{ key: 'atemschutz_lehrgang', label: 'Lehrgang', align: 'center', render: (item) => (
item.atemschutz_lehrgang ? (
<Tooltip title={item.lehrgang_datum ? `Lehrgang am ${formatDate(item.lehrgang_datum)}` : 'Lehrgang absolviert'}>
<Check color="success" fontSize="small" />
</Tooltip>
) : (
<Close color="disabled" fontSize="small" />
)
)},
{ key: 'untersuchung_gueltig_bis', label: 'Untersuchung gültig bis', render: (item) => (
<Tooltip title={
item.untersuchung_tage_rest !== null
? item.untersuchung_tage_rest < 0
? `Seit ${Math.abs(item.untersuchung_tage_rest)} Tagen abgelaufen`
: `Noch ${item.untersuchung_tage_rest} Tage gültig`
: 'Keine Untersuchung eingetragen'
}>
<Typography variant="body2" color={getValidityColor(item.untersuchung_gueltig_bis, item.untersuchung_tage_rest, 90)} fontWeight={500}>
{formatDate(item.untersuchung_gueltig_bis)}
</Typography>
</Tooltip>
)},
{ key: 'leistungstest_gueltig_bis', label: 'Leistungstest gültig bis', render: (item) => (
<Tooltip title={
item.leistungstest_tage_rest !== null
? item.leistungstest_tage_rest < 0
? `Seit ${Math.abs(item.leistungstest_tage_rest)} Tagen abgelaufen`
: `Noch ${item.leistungstest_tage_rest} Tage gültig`
: 'Kein Leistungstest eingetragen'
}>
<Typography variant="body2" color={getValidityColor(item.leistungstest_gueltig_bis, item.leistungstest_tage_rest, 30)} fontWeight={500}>
{formatDate(item.leistungstest_gueltig_bis)}
</Typography>
</Tooltip>
)},
{ key: 'einsatzbereit', label: 'Status', align: 'center', render: (item) => (
<Chip
label={item.einsatzbereit ? 'Einsatzbereit' : 'Nicht einsatzbereit'}
color={item.einsatzbereit ? 'success' : 'error'}
size="small"
variant="filled"
/>
)},
];
if (canWrite) {
columns.push({
key: 'actions', label: 'Aktionen', align: 'right', sortable: false, searchable: false, render: (item) => (
<>
<Tooltip title="Bearbeiten">
<Button size="small" onClick={(e) => { e.stopPropagation(); handleOpenEdit(item); }} sx={{ minWidth: 'auto', mr: 0.5 }}>
<Edit fontSize="small" />
</Button>
</Tooltip>
<Tooltip title="Löschen">
<Button size="small" color="error" onClick={(e) => { e.stopPropagation(); setDeleteId(item.id); }} sx={{ minWidth: 'auto' }}>
<Delete fontSize="small" />
</Button>
</Tooltip>
</>
),
});
}
return (
<DataTable
columns={columns}
data={filtered}
rowKey={(item) => item.id}
emptyMessage={traeger.length === 0 ? 'Keine Atemschutzträger vorhanden' : 'Keine Ergebnisse gefunden'}
searchEnabled={false}
paginationEnabled={false}
/>
);
})()}
{/* FAB to create */}
{canWrite && (
<ChatAwareFab
color="primary"
aria-label="Atemschutzträger hinzufügen"
onClick={handleOpenCreate}
>
<Add />
</ChatAwareFab>
)}
{/* ── Add / Edit Dialog ───────────────────────────────────────────── */}
<Dialog
open={dialogOpen}
onClose={handleDialogClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>
{editingId ? 'Atemschutzträger bearbeiten' : 'Neuen Atemschutzträger anlegen'}
</DialogTitle>
<DialogContent>
{dialogError && (
<Alert severity="error" sx={{ mb: 2, mt: 1 }}>
{dialogError}
</Alert>
)}
<Grid container spacing={2} sx={{ mt: 0.5 }}>
{/* User selection (only when creating) */}
{!editingId && (
<Grid item xs={12}>
<FormControl fullWidth size="small" required>
<InputLabel>Mitglied</InputLabel>
<Select
value={form.user_id}
label="Mitglied"
onChange={(e) => handleFormChange('user_id', e.target.value)}
>
{availableMembers.map((m) => {
const displayName = [m.family_name, m.given_name]
.filter(Boolean)
.join(', ') || m.name || m.email;
return (
<MenuItem key={m.id} value={m.id}>
{displayName}
{m.dienstgrad ? ` (${m.dienstgrad})` : ''}
</MenuItem>
);
})}
{availableMembers.length === 0 && (
<MenuItem disabled value="">
Keine verfügbaren Mitglieder
</MenuItem>
)}
</Select>
</FormControl>
</Grid>
)}
{/* Lehrgang */}
<Grid item xs={12}>
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
Lehrgang
</Typography>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Checkbox
checked={form.atemschutz_lehrgang}
onChange={(e) => handleFormChange('atemschutz_lehrgang', e.target.checked)}
/>
}
label="Lehrgang absolviert"
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Lehrgang Datum"
size="small"
fullWidth
placeholder="TT.MM.JJJJ"
value={form.lehrgang_datum}
onChange={(e) => handleFormChange('lehrgang_datum', e.target.value)}
error={!!dateErrors.lehrgang_datum}
helperText={dateErrors.lehrgang_datum ?? 'Format: 01.03.2025'}
InputLabelProps={{ shrink: true }}
/>
</Grid>
{/* Untersuchung */}
<Grid item xs={12}>
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1, mt: 1 }}>
Untersuchung
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label="Untersuchung Datum"
size="small"
fullWidth
placeholder="TT.MM.JJJJ"
value={form.untersuchung_datum}
onChange={(e) => handleFormChange('untersuchung_datum', e.target.value)}
error={!!dateErrors.untersuchung_datum}
helperText={dateErrors.untersuchung_datum ?? 'Format: 08.02.2023'}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Gültig bis"
size="small"
fullWidth
placeholder="TT.MM.JJJJ"
value={form.untersuchung_gueltig_bis}
onChange={(e) => handleFormChange('untersuchung_gueltig_bis', e.target.value)}
error={!!dateErrors.untersuchung_gueltig_bis}
helperText={dateErrors.untersuchung_gueltig_bis ?? 'Format: 08.02.2028'}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth size="small">
<InputLabel>Ergebnis</InputLabel>
<Select
value={form.untersuchung_ergebnis}
label="Ergebnis"
onChange={(e) => handleFormChange('untersuchung_ergebnis', e.target.value)}
>
<MenuItem value=""> Nicht angegeben </MenuItem>
{(Object.keys(UntersuchungErgebnisLabel) as UntersuchungErgebnis[]).map(
(key) => (
<MenuItem key={key} value={key}>
{UntersuchungErgebnisLabel[key]}
</MenuItem>
)
)}
</Select>
</FormControl>
</Grid>
{/* Leistungstest */}
<Grid item xs={12}>
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1, mt: 1 }}>
Leistungstest
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label="Leistungstest Datum"
size="small"
fullWidth
placeholder="TT.MM.JJJJ"
value={form.leistungstest_datum}
onChange={(e) => handleFormChange('leistungstest_datum', e.target.value)}
error={!!dateErrors.leistungstest_datum}
helperText={dateErrors.leistungstest_datum ?? 'Format: 25.08.2025'}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Gültig bis"
size="small"
fullWidth
placeholder="TT.MM.JJJJ"
value={form.leistungstest_gueltig_bis}
onChange={(e) => handleFormChange('leistungstest_gueltig_bis', e.target.value)}
error={!!dateErrors.leistungstest_gueltig_bis}
helperText={dateErrors.leistungstest_gueltig_bis ?? 'Format: 25.08.2026'}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Checkbox
checked={form.leistungstest_bestanden}
onChange={(e) => handleFormChange('leistungstest_bestanden', e.target.checked)}
/>
}
label="Leistungstest bestanden"
/>
</Grid>
{/* Bemerkung */}
<Grid item xs={12}>
<TextField
label="Bemerkung"
multiline
rows={3}
size="small"
fullWidth
value={form.bemerkung}
onChange={(e) => handleFormChange('bemerkung', e.target.value)}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose} disabled={dialogLoading}>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={dialogLoading}
startIcon={dialogLoading ? <CircularProgress size={16} /> : undefined}
>
{editingId ? 'Speichern' : 'Anlegen'}
</Button>
</DialogActions>
</Dialog>
{/* ── Delete Confirmation Dialog ──────────────────────────────────── */}
<ConfirmDialog
open={deleteId !== null}
onClose={() => setDeleteId(null)}
onConfirm={handleDeleteConfirm}
title="Atemschutzträger löschen"
message="Soll dieser Atemschutzträger-Eintrag wirklich gelöscht werden? Diese Aktion kann nicht rückgängig gemacht werden."
confirmLabel="Löschen"
confirmColor="error"
isLoading={deleteLoading}
/>
</Container>
</DashboardLayout>
);
}
export default Atemschutz;