feat: vehicle/equipment type system, equipment checklist support, and checklist overview redesign
This commit is contained in:
@@ -9,10 +9,6 @@ import {
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
@@ -22,12 +18,6 @@ import {
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
@@ -36,22 +26,20 @@ import {
|
||||
Add,
|
||||
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 DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { equipmentApi } from '../services/equipment';
|
||||
import { ausruestungTypenApi } from '../services/ausruestungTypen';
|
||||
import {
|
||||
AusruestungListItem,
|
||||
AusruestungKategorie,
|
||||
@@ -60,7 +48,6 @@ 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 ────────────────────────────────────────────────────────
|
||||
@@ -169,13 +156,23 @@ const EquipmentCard: React.FC<EquipmentCardProps> = ({ item, onClick }) => {
|
||||
<Typography variant="h6" component="div" lineHeight={1.2} noWrap>
|
||||
{item.bezeichnung}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={item.kategorie_kurzname}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '0.7rem' }}
|
||||
/>
|
||||
{item.typen?.map((t) => (
|
||||
<Chip
|
||||
key={t.id}
|
||||
label={t.name}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '0.7rem' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -236,186 +233,12 @@ const EquipmentCard: React.FC<EquipmentCardProps> = ({ item, onClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// ── Category Management Dialog ───────────────────────────────────────────────
|
||||
|
||||
interface CategoryDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
categories: AusruestungKategorie[];
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const CategoryManagementDialog: React.FC<CategoryDialogProps> = ({ open, onClose, categories, onRefresh }) => {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editKurzname, setEditKurzname] = useState('');
|
||||
const [editMotor, setEditMotor] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newKurzname, setNewKurzname] = useState('');
|
||||
const [newMotor, setNewMotor] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const startEdit = (cat: AusruestungKategorie) => {
|
||||
setEditingId(cat.id);
|
||||
setEditName(cat.name);
|
||||
setEditKurzname(cat.kurzname);
|
||||
setEditMotor(cat.motorisiert);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditName('');
|
||||
setEditKurzname('');
|
||||
setEditMotor(false);
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!editingId || !editName.trim() || !editKurzname.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await equipmentApi.updateCategory(editingId, { name: editName.trim(), kurzname: editKurzname.trim(), motorisiert: editMotor });
|
||||
showSuccess('Kategorie aktualisiert');
|
||||
cancelEdit();
|
||||
onRefresh();
|
||||
} catch {
|
||||
showError('Kategorie konnte nicht aktualisiert werden');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim() || !newKurzname.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await equipmentApi.createCategory({ name: newName.trim(), kurzname: newKurzname.trim(), motorisiert: newMotor });
|
||||
showSuccess('Kategorie erstellt');
|
||||
setNewName('');
|
||||
setNewKurzname('');
|
||||
setNewMotor(false);
|
||||
onRefresh();
|
||||
} catch {
|
||||
showError('Kategorie konnte nicht erstellt werden');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await equipmentApi.deleteCategory(id);
|
||||
showSuccess('Kategorie gelöscht');
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || 'Kategorie konnte nicht gelöscht werden';
|
||||
showError(msg);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
Kategorien verwalten
|
||||
<IconButton onClick={onClose} size="small"><Close /></IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Kurzname</TableCell>
|
||||
<TableCell>Motorisiert</TableCell>
|
||||
<TableCell align="right">Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{categories.map((cat) => (
|
||||
<TableRow key={cat.id}>
|
||||
{editingId === cat.id ? (
|
||||
<>
|
||||
<TableCell>
|
||||
<TextField size="small" value={editName} onChange={(e) => setEditName(e.target.value)} fullWidth />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" value={editKurzname} onChange={(e) => setEditKurzname(e.target.value)} fullWidth />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch checked={editMotor} onChange={(e) => setEditMotor(e.target.checked)} size="small" />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={saveEdit} disabled={saving || !editName.trim() || !editKurzname.trim()} color="primary">
|
||||
<Save fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={cancelEdit} disabled={saving}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell>{cat.name}</TableCell>
|
||||
<TableCell>{cat.kurzname}</TableCell>
|
||||
<TableCell>{cat.motorisiert ? 'Ja' : 'Nein'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => startEdit(cat)} disabled={saving}>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => handleDelete(cat.id)} disabled={saving} color="error">
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
{/* New category row */}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<TextField size="small" placeholder="Name" value={newName} onChange={(e) => setNewName(e.target.value)} fullWidth />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" placeholder="Kurzname" value={newKurzname} onChange={(e) => setNewKurzname(e.target.value)} fullWidth />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch checked={newMotor} onChange={(e) => setNewMotor(e.target.checked)} size="small" />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<Add />}
|
||||
onClick={handleCreate}
|
||||
disabled={saving || !newName.trim() || !newKurzname.trim()}
|
||||
>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Schließen</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function Ausruestung() {
|
||||
const navigate = useNavigate();
|
||||
const { canManageEquipment, hasPermission } = usePermissions();
|
||||
const canManageCategories = hasPermission('ausruestung:manage_categories');
|
||||
|
||||
// Category dialog state
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
||||
const canManageTypes = hasPermission('ausruestung:manage_types');
|
||||
|
||||
// Data state
|
||||
const [equipment, setEquipment] = useState<AusruestungListItem[]>([]);
|
||||
@@ -424,9 +247,16 @@ function Ausruestung() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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);
|
||||
@@ -471,6 +301,14 @@ function Ausruestung() {
|
||||
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;
|
||||
@@ -490,7 +328,7 @@ function Ausruestung() {
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [equipment, search, selectedCategory, selectedStatus, nurWichtige, pruefungFaellig]);
|
||||
}, [equipment, search, selectedCategory, selectedTyp, selectedStatus, nurWichtige, pruefungFaellig]);
|
||||
|
||||
const hasOverdue = equipment.some(
|
||||
(item) => item.pruefung_tage_bis_faelligkeit !== null && item.pruefung_tage_bis_faelligkeit < 0
|
||||
@@ -506,9 +344,9 @@ function Ausruestung() {
|
||||
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
|
||||
Ausrüstungsverwaltung
|
||||
</Typography>
|
||||
{canManageCategories && (
|
||||
<Tooltip title="Kategorien verwalten">
|
||||
<IconButton onClick={() => setCategoryDialogOpen(true)} size="small">
|
||||
{canManageTypes && (
|
||||
<Tooltip title="Einstellungen">
|
||||
<IconButton onClick={() => navigate('/ausruestung/einstellungen')} size="small">
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -576,6 +414,22 @@ function Ausruestung() {
|
||||
</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
|
||||
@@ -673,15 +527,6 @@ function Ausruestung() {
|
||||
<Add />
|
||||
</ChatAwareFab>
|
||||
)}
|
||||
{/* Category management dialog */}
|
||||
{canManageCategories && (
|
||||
<CategoryManagementDialog
|
||||
open={categoryDialogOpen}
|
||||
onClose={() => setCategoryDialogOpen(false)}
|
||||
categories={categories}
|
||||
onRefresh={fetchData}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user