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,
Select,
Switch,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import {
Add,
Build,
CheckCircle,
Error as ErrorIcon,
LinkRounded,
PauseCircle,
RemoveCircle,
Search,
Settings,
Star,
Warning,
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { equipmentApi } from '../services/equipment';
import { ausruestungTypenApi } from '../services/ausruestungTypen';
import {
AusruestungListItem,
AusruestungKategorie,
AusruestungStatus,
AusruestungStatusLabel,
EquipmentStats,
} from '../types/equipment.types';
import { usePermissions } from '../hooks/usePermissions';
import ChatAwareFab from '../components/shared/ChatAwareFab';
// ── 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' }}
/>
)}
);
};
// ── Main Page ─────────────────────────────────────────────────────────────────
function Ausruestung() {
const navigate = useNavigate();
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) => {
// Text search
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;
}
// Category filter
if (selectedCategory && item.kategorie_id !== selectedCategory) {
return false;
}
// Type filter
if (selectedTyp) {
const typId = parseInt(selectedTyp, 10);
if (!item.typen?.some((t) => t.id === typId)) {
return false;
}
}
// Status filter
if (selectedStatus && item.status !== selectedStatus) {
return false;
}
// Nur wichtige
if (nurWichtige && !item.ist_wichtig) {
return false;
}
// Prüfung fällig (within 30 days or overdue)
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
{canManageTypes && (
navigate('/ausruestung/einstellungen')} size="small">
)}
{!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
)}
{/* 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')}
>
)}
);
}
export default Ausruestung;