import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { Alert, Box, Button, Card, CardActionArea, CardContent, Chip, CircularProgress, Container, FormControl, FormControlLabel, Grid, IconButton, InputAdornment, InputLabel, MenuItem, Paper, Select, Switch, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, Add as AddIcon, Build, CheckCircle, Delete, Edit, Error as ErrorIcon, LinkRounded, PauseCircle, RemoveCircle, Search, Star, Warning, } from '@mui/icons-material'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { equipmentApi } from '../services/equipment'; import { ausruestungTypenApi, AusruestungTyp } from '../services/ausruestungTypen'; import { AusruestungListItem, AusruestungKategorie, AusruestungStatus, AusruestungStatusLabel, EquipmentStats, } from '../types/equipment.types'; import { usePermissions } from '../hooks/usePermissions'; import { useNotification } from '../contexts/NotificationContext'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { ConfirmDialog, FormDialog } from '../components/templates'; // ── Status chip config ──────────────────────────────────────────────────────── const STATUS_CONFIG: Record< AusruestungStatus, { color: 'success' | 'warning' | 'error' | 'default'; icon: React.ReactElement } > = { [AusruestungStatus.Einsatzbereit]: { color: 'success', icon: }, [AusruestungStatus.Beschaedigt]: { color: 'error', icon: }, [AusruestungStatus.InWartung]: { color: 'warning', icon: }, [AusruestungStatus.AusserDienst]: { color: 'default', icon: }, }; // ── Inspection badge helpers ────────────────────────────────────────────────── type InspBadgeColor = 'success' | 'warning' | 'error' | 'default'; function inspBadgeColor(tage: number | null): InspBadgeColor { if (tage === null) return 'default'; if (tage < 0) return 'error'; if (tage <= 30) return 'warning'; return 'success'; } function inspBadgeLabel(tage: number | null, faelligAm: string | null): string { if (faelligAm === null) return ''; const date = new Date(faelligAm).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', }); if (tage === null) return `Prüfung: ${date}`; if (tage < 0) return `ÜBERFÄLLIG (${date})`; if (tage === 0) return `Prüfung: heute`; return `Prüfung: ${date}`; } function inspTooltipTitle(tage: number | null, faelligAm: string | null): string { if (!faelligAm) return 'Keine Prüfung geplant'; const date = new Date(faelligAm).toLocaleDateString('de-DE'); if (tage !== null && tage < 0) { return `Prüfung seit ${Math.abs(tage)} Tagen überfällig!`; } if (tage !== null && tage === 0) { return 'Prüfung heute fällig'; } if (tage !== null) { return `Nächste Prüfung am ${date} (in ${tage} Tagen)`; } return `Nächste Prüfung am ${date}`; } // ── Equipment Card ──────────────────────────────────────────────────────────── interface EquipmentCardProps { item: AusruestungListItem; onClick: (id: string) => void; } const EquipmentCard: React.FC = ({ item, onClick }) => { const status = item.status as AusruestungStatus; const statusCfg = STATUS_CONFIG[status] ?? STATUS_CONFIG[AusruestungStatus.Einsatzbereit]; const isBeschaedigt = status === AusruestungStatus.Beschaedigt; const pruefungLabel = inspBadgeLabel(item.pruefung_tage_bis_faelligkeit, item.naechste_pruefung_am); const pruefungColor = inspBadgeColor(item.pruefung_tage_bis_faelligkeit); const pruefungTooltip = inspTooltipTitle(item.pruefung_tage_bis_faelligkeit, item.naechste_pruefung_am); return ( {item.ist_wichtig && ( )} onClick(item.id)} sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }} > {item.bezeichnung} {item.typen?.map((t) => ( ))} {/* Location */} {item.fahrzeug_bezeichnung ? ( {item.fahrzeug_bezeichnung} {item.fahrzeug_kurzname && ` (${item.fahrzeug_kurzname})`} ) : ( {item.standort} )} {/* Serial number */} {item.seriennummer && ( SN: {item.seriennummer} )} {/* Status chip */} {/* Inspection badge */} {pruefungLabel && ( : undefined} sx={{ fontSize: '0.7rem' }} /> )} ); }; // ── Ausrüstungstypen-Verwaltung (Einstellungen Tab) ────────────────────────── function AusruestungTypenSettings() { const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const { data: typen = [], isLoading, isError } = useQuery({ queryKey: ['ausruestungTypen'], queryFn: ausruestungTypenApi.getAll, }); const [dialogOpen, setDialogOpen] = useState(false); const [editingTyp, setEditingTyp] = useState(null); const [formName, setFormName] = useState(''); const [formBeschreibung, setFormBeschreibung] = useState(''); const [formIcon, setFormIcon] = useState(''); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deletingTyp, setDeletingTyp] = useState(null); const createMutation = useMutation({ mutationFn: (data: { name: string; beschreibung?: string; icon?: string }) => ausruestungTypenApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungTypen'] }); showSuccess('Typ erstellt'); closeDialog(); }, onError: () => showError('Typ konnte nicht erstellt werden'), }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial<{ name: string; beschreibung: string; icon: string }> }) => ausruestungTypenApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungTypen'] }); showSuccess('Typ aktualisiert'); closeDialog(); }, onError: () => showError('Typ konnte nicht aktualisiert werden'), }); const deleteMutation = useMutation({ mutationFn: (id: number) => ausruestungTypenApi.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungTypen'] }); showSuccess('Typ gelöscht'); setDeleteDialogOpen(false); setDeletingTyp(null); }, onError: () => showError('Typ konnte nicht gelöscht werden. Möglicherweise ist er noch Geräten zugeordnet.'), }); const openAddDialog = () => { setEditingTyp(null); setFormName(''); setFormBeschreibung(''); setFormIcon(''); setDialogOpen(true); }; const openEditDialog = (typ: AusruestungTyp) => { setEditingTyp(typ); setFormName(typ.name); setFormBeschreibung(typ.beschreibung ?? ''); setFormIcon(typ.icon ?? ''); setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingTyp(null); }; const handleSave = () => { if (!formName.trim()) return; const data = { name: formName.trim(), beschreibung: formBeschreibung.trim() || undefined, icon: formIcon.trim() || undefined, }; if (editingTyp) { updateMutation.mutate({ id: editingTyp.id, data }); } else { createMutation.mutate(data); } }; const openDeleteDialog = (typ: AusruestungTyp) => { setDeletingTyp(typ); setDeleteDialogOpen(true); }; const isSaving = createMutation.isPending || updateMutation.isPending; return ( Ausrüstungstypen {isLoading && ( )} {isError && ( Typen konnten nicht geladen werden. )} {!isLoading && !isError && ( Name Beschreibung Icon Aktionen {typen.length === 0 && ( Noch keine Typen vorhanden. )} {typen.map((typ) => ( {typ.name} {typ.beschreibung || '---'} {typ.icon || '---'} openEditDialog(typ)}> openDeleteDialog(typ)}> ))}
)} {/* Add/Edit dialog */} setFormName(e.target.value)} inputProps={{ maxLength: 100 }} /> setFormBeschreibung(e.target.value)} /> setFormIcon(e.target.value)} placeholder="z.B. Build, LocalFireDepartment" /> {/* Delete confirmation dialog */} setDeleteDialogOpen(false)} onConfirm={() => deletingTyp && deleteMutation.mutate(deletingTyp.id)} title="Typ löschen" message={<>Möchten Sie den Typ "{deletingTyp?.name}" wirklich löschen? Geräte, denen dieser Typ zugeordnet ist, verlieren die Zuordnung.} confirmLabel="Löschen" confirmColor="error" isLoading={deleteMutation.isPending} />
); } // ── Main Page ───────────────────────────────────────────────────────────────── function Ausruestung() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const tab = parseInt(searchParams.get('tab') ?? '0', 10); const { canManageEquipment, hasPermission } = usePermissions(); const canManageTypes = hasPermission('ausruestung:manage_types'); // Data state const [equipment, setEquipment] = useState([]); const [categories, setCategories] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Equipment types for filter const { data: typen = [] } = useQuery({ queryKey: ['ausruestungTypen'], queryFn: ausruestungTypenApi.getAll, }); // Filter state const [search, setSearch] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [selectedTyp, setSelectedTyp] = useState(''); const [selectedStatus, setSelectedStatus] = useState(''); const [nurWichtige, setNurWichtige] = useState(false); const [pruefungFaellig, setPruefungFaellig] = useState(false); const fetchData = useCallback(async () => { try { setLoading(true); setError(null); const [equipmentData, categoriesData, statsData] = await Promise.all([ equipmentApi.getAll(), equipmentApi.getCategories(), equipmentApi.getStats(), ]); setEquipment(equipmentData); setCategories(categoriesData); setStats(statsData); } catch { setError('Ausrüstung konnte nicht geladen werden. Bitte versuchen Sie es erneut.'); } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); // Client-side filtering const filtered = useMemo(() => { return equipment.filter((item) => { if (search.trim()) { const q = search.toLowerCase(); const matches = item.bezeichnung.toLowerCase().includes(q) || (item.seriennummer?.toLowerCase().includes(q) ?? false) || (item.inventarnummer?.toLowerCase().includes(q) ?? false) || (item.hersteller?.toLowerCase().includes(q) ?? false); if (!matches) return false; } if (selectedCategory && item.kategorie_id !== selectedCategory) return false; if (selectedTyp) { const typId = parseInt(selectedTyp, 10); if (!item.typen?.some((t) => t.id === typId)) return false; } if (selectedStatus && item.status !== selectedStatus) return false; if (nurWichtige && !item.ist_wichtig) return false; if (pruefungFaellig) { if (item.pruefung_tage_bis_faelligkeit === null || item.pruefung_tage_bis_faelligkeit > 30) return false; } return true; }); }, [equipment, search, selectedCategory, selectedTyp, selectedStatus, nurWichtige, pruefungFaellig]); const hasOverdue = equipment.some( (item) => item.pruefung_tage_bis_faelligkeit !== null && item.pruefung_tage_bis_faelligkeit < 0 ); return ( {/* Header */} Ausrüstungsverwaltung {!loading && stats && ( {stats.total} Gesamt {'·'} {stats.einsatzbereit} Einsatzbereit {'·'} {stats.beschaedigt} Beschädigt {'·'} 0 ? 'warning.main' : 'text.secondary'} fontWeight={stats.inspectionsDue > 0 ? 600 : 400}> {stats.inspectionsDue + stats.inspectionsOverdue} Prüfung fällig )} setSearchParams({ tab: String(v) })} sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }} > {canManageTypes && } {tab === 0 && ( <> {/* Overdue alert */} {hasOverdue && ( }> Achtung: Mindestens eine Ausrüstung hat eine überfällige Prüfungsfrist. )} {/* Filter controls */} setSearch(e.target.value)} size="small" sx={{ minWidth: 280, flexGrow: 1, maxWidth: 480 }} InputProps={{ startAdornment: ( ), }} /> Kategorie Typ Status setNurWichtige(e.target.checked)} size="small" /> } label={Nur wichtige} /> setPruefungFaellig(e.target.checked)} size="small" /> } label={Prüfung fällig} /> {/* Loading state */} {loading && ( )} {/* Error state */} {!loading && error && ( Erneut versuchen } > {error} )} {/* Empty states */} {!loading && !error && filtered.length === 0 && ( {equipment.length === 0 ? 'Keine Ausrüstung vorhanden' : 'Keine Ausrüstung gefunden'} )} {/* Equipment grid */} {!loading && !error && filtered.length > 0 && ( {filtered.map((item) => ( navigate(`/ausruestung/${id}`)} /> ))} )} {/* FAB for adding new equipment */} {canManageEquipment && ( navigate('/ausruestung/neu')} > )} )} {tab === 1 && canManageTypes && ( )} ); } export default Ausruestung;