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)}>
))}
} onClick={openAddDialog}>
Neuer 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;