feat: checklist multi-type assignments, tab layouts for Fahrzeuge/Ausruestung, admin cleanup
- Migration 074: convert checklist vorlage single FK fields to junction tables (vorlage_fahrzeug_typen, vorlage_fahrzeuge, vorlage_ausruestung_typen, vorlage_ausruestungen) - Backend checklist service: multi-type create/update/query with array fields - Backend cleanup service: add checklist-history and reset-checklist-history targets - Frontend types/service: singular FK fields replaced with arrays (fahrzeug_typ_ids, etc.) - Frontend Checklisten.tsx: multi-select Autocomplete pickers for all assignment types - Fahrzeuge.tsx/Ausruestung.tsx: add tab layout (Uebersicht + Einstellungen), inline type CRUD - FahrzeugEinstellungen/AusruestungEinstellungen: replaced with redirects to tab URLs - Sidebar: add Uebersicht sub-items, update Einstellungen paths to tab URLs - DataManagementTab: add checklist-history cleanup and reset sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,11 @@ import {
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
@@ -16,30 +21,43 @@ import {
|
||||
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,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Error as ErrorIcon,
|
||||
LinkRounded,
|
||||
PauseCircle,
|
||||
RemoveCircle,
|
||||
Save,
|
||||
Search,
|
||||
Settings,
|
||||
Star,
|
||||
Warning,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
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 } from '../services/ausruestungTypen';
|
||||
import { ausruestungTypenApi, AusruestungTyp } from '../services/ausruestungTypen';
|
||||
import {
|
||||
AusruestungListItem,
|
||||
AusruestungKategorie,
|
||||
@@ -48,6 +66,7 @@ import {
|
||||
EquipmentStats,
|
||||
} from '../types/equipment.types';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||
|
||||
// ── Status chip config ────────────────────────────────────────────────────────
|
||||
@@ -233,10 +252,248 @@ const EquipmentCard: React.FC<EquipmentCardProps> = ({ item, onClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// ── 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<AusruestungTyp | null>(null);
|
||||
const [formName, setFormName] = useState('');
|
||||
const [formBeschreibung, setFormBeschreibung] = useState('');
|
||||
const [formIcon, setFormIcon] = useState('');
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deletingTyp, setDeletingTyp] = useState<AusruestungTyp | null>(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 (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>Ausrüstungstypen</Typography>
|
||||
|
||||
{isLoading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
Typen konnten nicht geladen werden.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<Paper variant="outlined">
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Beschreibung</TableCell>
|
||||
<TableCell>Icon</TableCell>
|
||||
<TableCell align="right">Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{typen.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center">
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
|
||||
Noch keine Typen vorhanden.
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{typen.map((typ) => (
|
||||
<TableRow key={typ.id}>
|
||||
<TableCell>{typ.name}</TableCell>
|
||||
<TableCell>{typ.beschreibung || '---'}</TableCell>
|
||||
<TableCell>{typ.icon || '---'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Bearbeiten">
|
||||
<IconButton size="small" onClick={() => openEditDialog(typ)}>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Löschen">
|
||||
<IconButton size="small" color="error" onClick={() => openDeleteDialog(typ)}>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box sx={{ p: 1.5, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button variant="contained" size="small" startIcon={<AddIcon />} onClick={openAddDialog}>
|
||||
Neuer Typ
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Add/Edit dialog */}
|
||||
<Dialog open={dialogOpen} onClose={closeDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{editingTyp ? 'Typ bearbeiten' : 'Neuen Typ erstellen'}
|
||||
<IconButton onClick={closeDialog} size="small"><Close /></IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Name *"
|
||||
fullWidth
|
||||
value={formName}
|
||||
onChange={(e) => setFormName(e.target.value)}
|
||||
sx={{ mt: 1, mb: 2 }}
|
||||
inputProps={{ maxLength: 100 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Beschreibung"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
value={formBeschreibung}
|
||||
onChange={(e) => setFormBeschreibung(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Icon (MUI Icon-Name)"
|
||||
fullWidth
|
||||
value={formIcon}
|
||||
onChange={(e) => setFormIcon(e.target.value)}
|
||||
placeholder="z.B. Build, LocalFireDepartment"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !formName.trim()}
|
||||
startIcon={isSaving ? <CircularProgress size={16} /> : <Save />}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={deleteDialogOpen} onClose={() => !deleteMutation.isPending && setDeleteDialogOpen(false)}>
|
||||
<DialogTitle>Typ löschen</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Möchten Sie den Typ "{deletingTyp?.name}" wirklich löschen?
|
||||
Geräte, denen dieser Typ zugeordnet ist, verlieren die Zuordnung.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)} disabled={deleteMutation.isPending}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={() => deletingTyp && deleteMutation.mutate(deletingTyp.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
startIcon={deleteMutation.isPending ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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');
|
||||
|
||||
@@ -285,7 +542,6 @@ function Ausruestung() {
|
||||
// Client-side filtering
|
||||
const filtered = useMemo(() => {
|
||||
return equipment.filter((item) => {
|
||||
// Text search
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase();
|
||||
const matches =
|
||||
@@ -295,37 +551,16 @@ function Ausruestung() {
|
||||
(item.hersteller?.toLowerCase().includes(q) ?? false);
|
||||
if (!matches) return false;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategory && item.kategorie_id !== selectedCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
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 (!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 (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;
|
||||
}
|
||||
if (item.pruefung_tage_bis_faelligkeit === null || item.pruefung_tage_bis_faelligkeit > 30) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [equipment, search, selectedCategory, selectedTyp, selectedStatus, nurWichtige, pruefungFaellig]);
|
||||
@@ -338,19 +573,12 @@ function Ausruestung() {
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
|
||||
Ausrüstungsverwaltung
|
||||
</Typography>
|
||||
{canManageTypes && (
|
||||
<Tooltip title="Einstellungen">
|
||||
<IconButton onClick={() => navigate('/ausruestung/einstellungen')} size="small">
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
{!loading && stats && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
|
||||
@@ -374,158 +602,175 @@ function Ausruestung() {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Overdue alert */}
|
||||
{hasOverdue && (
|
||||
<Alert severity="error" sx={{ mb: 2 }} icon={<Warning />}>
|
||||
<strong>Achtung:</strong> Mindestens eine Ausrüstung hat eine überfällige Prüfungsfrist.
|
||||
</Alert>
|
||||
)}
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(_e, v) => setSearchParams({ tab: String(v) })}
|
||||
sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Tab label="Übersicht" />
|
||||
{canManageTypes && <Tab label="Einstellungen" />}
|
||||
</Tabs>
|
||||
|
||||
{/* Filter controls */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||
<TextField
|
||||
placeholder="Suchen (Bezeichnung, Seriennr., Inventarnr., Hersteller...)"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
size="small"
|
||||
sx={{ minWidth: 280, flexGrow: 1, maxWidth: 480 }}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Search />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{tab === 0 && (
|
||||
<>
|
||||
{/* Overdue alert */}
|
||||
{hasOverdue && (
|
||||
<Alert severity="error" sx={{ mb: 2 }} icon={<Warning />}>
|
||||
<strong>Achtung:</strong> Mindestens eine Ausrüstung hat eine überfällige Prüfungsfrist.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Kategorie</InputLabel>
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
label="Kategorie"
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Kategorien</MenuItem>
|
||||
{categories.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Typ</InputLabel>
|
||||
<Select
|
||||
value={selectedTyp}
|
||||
label="Typ"
|
||||
onChange={(e) => setSelectedTyp(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Typen</MenuItem>
|
||||
{typen.map((t) => (
|
||||
<MenuItem key={t.id} value={String(t.id)}>
|
||||
{t.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
label="Status"
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Status</MenuItem>
|
||||
{Object.values(AusruestungStatus).map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{AusruestungStatusLabel[s]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={nurWichtige}
|
||||
onChange={(e) => setNurWichtige(e.target.checked)}
|
||||
{/* Filter controls */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||
<TextField
|
||||
placeholder="Suchen (Bezeichnung, Seriennr., Inventarnr., Hersteller...)"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
size="small"
|
||||
sx={{ minWidth: 280, flexGrow: 1, maxWidth: 480 }}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Search />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Nur wichtige</Typography>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={pruefungFaellig}
|
||||
onChange={(e) => setPruefungFaellig(e.target.checked)}
|
||||
size="small"
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Kategorie</InputLabel>
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
label="Kategorie"
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Kategorien</MenuItem>
|
||||
{categories.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Typ</InputLabel>
|
||||
<Select
|
||||
value={selectedTyp}
|
||||
label="Typ"
|
||||
onChange={(e) => setSelectedTyp(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Typen</MenuItem>
|
||||
{typen.map((t) => (
|
||||
<MenuItem key={t.id} value={String(t.id)}>
|
||||
{t.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
label="Status"
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Alle Status</MenuItem>
|
||||
{Object.values(AusruestungStatus).map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{AusruestungStatusLabel[s]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={nurWichtige}
|
||||
onChange={(e) => setNurWichtige(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Nur wichtige</Typography>}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Prüfung fällig</Typography>}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={pruefungFaellig}
|
||||
onChange={(e) => setPruefungFaellig(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Prüfung fällig</Typography>}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Error state */}
|
||||
{!loading && error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2 }}
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={fetchData}>
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Empty states */}
|
||||
{!loading && !error && filtered.length === 0 && (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Build sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{equipment.length === 0
|
||||
? 'Keine Ausrüstung vorhanden'
|
||||
: 'Keine Ausrüstung gefunden'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{/* Error state */}
|
||||
{!loading && error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2 }}
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={fetchData}>
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Equipment grid */}
|
||||
{!loading && !error && filtered.length > 0 && (
|
||||
<Grid container spacing={3}>
|
||||
{filtered.map((item) => (
|
||||
<Grid item key={item.id} xs={12} sm={6} md={4} lg={3}>
|
||||
<EquipmentCard
|
||||
item={item}
|
||||
onClick={(id) => navigate(`/ausruestung/${id}`)}
|
||||
/>
|
||||
{/* Empty states */}
|
||||
{!loading && !error && filtered.length === 0 && (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Build sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{equipment.length === 0
|
||||
? 'Keine Ausrüstung vorhanden'
|
||||
: 'Keine Ausrüstung gefunden'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Equipment grid */}
|
||||
{!loading && !error && filtered.length > 0 && (
|
||||
<Grid container spacing={3}>
|
||||
{filtered.map((item) => (
|
||||
<Grid item key={item.id} xs={12} sm={6} md={4} lg={3}>
|
||||
<EquipmentCard
|
||||
item={item}
|
||||
onClick={(id) => navigate(`/ausruestung/${id}`)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* FAB for adding new equipment */}
|
||||
{canManageEquipment && (
|
||||
<ChatAwareFab
|
||||
color="primary"
|
||||
aria-label="Ausrüstung hinzufügen"
|
||||
onClick={() => navigate('/ausruestung/neu')}
|
||||
>
|
||||
<Add />
|
||||
</ChatAwareFab>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* FAB for adding new equipment */}
|
||||
{canManageEquipment && (
|
||||
<ChatAwareFab
|
||||
color="primary"
|
||||
aria-label="Ausrüstung hinzufügen"
|
||||
onClick={() => navigate('/ausruestung/neu')}
|
||||
>
|
||||
<Add />
|
||||
</ChatAwareFab>
|
||||
{tab === 1 && canManageTypes && (
|
||||
<AusruestungTypenSettings />
|
||||
)}
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
|
||||
Reference in New Issue
Block a user