feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system

This commit is contained in:
Matthias Hochmeister
2026-03-28 15:19:41 +01:00
parent a1cda5be51
commit 0c2ea829aa
42 changed files with 4804 additions and 201 deletions

View File

@@ -112,6 +112,12 @@ const PERMISSION_SUB_GROUPS: Record<string, Record<string, string[]>> = {
'Bearbeiten': ['create', 'change_status', 'edit', 'delete'],
'Admin': ['edit_settings'],
},
checklisten: {
'Ansehen': ['view'],
'Ausführen': ['execute', 'approve'],
'Verwaltung': ['manage_templates'],
'Widget': ['widget'],
},
admin: {
'Allgemein': ['view', 'write'],
'Services': ['view_services', 'edit_services'],

View File

@@ -0,0 +1,92 @@
import { Card, CardContent, Typography, Box, Chip, Skeleton } from '@mui/material';
import { AssignmentTurnedIn, Warning } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { checklistenApi } from '../../services/checklisten';
function ChecklistWidget() {
const navigate = useNavigate();
const { data: overdue, isLoading, isError } = useQuery({
queryKey: ['checklist-overdue'],
queryFn: checklistenApi.getOverdue,
refetchInterval: 5 * 60 * 1000,
retry: 1,
});
const overdueItems = overdue ?? [];
if (isLoading) {
return (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>Checklisten</Typography>
<Skeleton variant="rectangular" height={40} />
</CardContent>
</Card>
);
}
if (isError) {
return (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>Checklisten</Typography>
<Typography variant="body2" color="text.secondary">
Checklisten konnten nicht geladen werden.
</Typography>
</CardContent>
</Card>
);
}
return (
<Card sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} onClick={() => navigate('/checklisten')}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6">Checklisten</Typography>
{overdueItems.length > 0 && (
<Chip
icon={<Warning />}
label={overdueItems.length}
color="error"
size="small"
/>
)}
</Box>
<AssignmentTurnedIn fontSize="small" color="action" />
</Box>
{overdueItems.length === 0 ? (
<Typography variant="body2" color="text.secondary">Alle Checklisten aktuell</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{overdueItems.slice(0, 5).map((item) => {
const days = Math.ceil((Date.now() - new Date(item.naechste_faellig_am).getTime()) / 86400000);
return (
<Box key={`${item.fahrzeug_id}-${item.vorlage_id}`} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2" noWrap sx={{ maxWidth: '60%' }}>
{item.fahrzeug_name}
</Typography>
<Chip
label={`${item.vorlage_name} \u2013 ${days > 0 ? `${days}d` : 'heute'}`}
color={days > 7 ? 'error' : days > 0 ? 'warning' : 'info'}
size="small"
variant="outlined"
/>
</Box>
);
})}
{overdueItems.length > 5 && (
<Typography variant="caption" color="text.secondary">
+ {overdueItems.length - 5} weitere
</Typography>
)}
</Box>
)}
</CardContent>
</Card>
);
}
export default ChecklistWidget;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Box, IconButton } from '@mui/material';
import { DragIndicator } from '@mui/icons-material';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface SortableWidgetProps {
id: string;
editMode: boolean;
children: React.ReactNode;
}
export default function SortableWidget({ id, editMode, children }: SortableWidgetProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, disabled: !editMode });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
position: 'relative',
};
return (
<Box ref={setNodeRef} style={style}>
{editMode && (
<IconButton
size="small"
{...attributes}
{...listeners}
sx={{
position: 'absolute',
top: 4,
left: 4,
zIndex: 10,
cursor: 'grab',
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
'&:hover': { bgcolor: 'action.hover' },
}}
>
<DragIndicator fontSize="small" />
</IconButton>
)}
{children}
</Box>
);
}

View File

@@ -22,3 +22,5 @@ export { default as BestellungenWidget } from './BestellungenWidget';
export { default as AusruestungsanfrageWidget } from './AusruestungsanfrageWidget';
export { default as IssueQuickAddWidget } from './IssueQuickAddWidget';
export { default as IssueOverviewWidget } from './IssueOverviewWidget';
export { default as ChecklistWidget } from './ChecklistWidget';
export { default as SortableWidget } from './SortableWidget';

View File

@@ -0,0 +1,252 @@
import React, { useState } from 'react';
import {
Alert,
Box,
Button,
Chip,
CircularProgress,
FormControlLabel,
IconButton,
Paper,
Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
} from '@mui/material';
import { Add, Delete as DeleteIcon, PlayArrow, Warning } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { usePermissionContext } from '../../contexts/PermissionContext';
import { useNotification } from '../../contexts/NotificationContext';
import { checklistenApi } from '../../services/checklisten';
import { CHECKLIST_STATUS_LABELS, CHECKLIST_STATUS_COLORS } from '../../types/checklist.types';
import type { CreateFahrzeugItemPayload } from '../../types/checklist.types';
// ── Helpers ──
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '\u2013';
// ══════════════════════════════════════════════════════════════════════════════
// Component
// ══════════════════════════════════════════════════════════════════════════════
interface FahrzeugChecklistTabProps {
fahrzeugId: string;
}
const FahrzeugChecklistTab: React.FC<FahrzeugChecklistTabProps> = ({ fahrzeugId }) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { hasPermission } = usePermissionContext();
const { showSuccess, showError } = useNotification();
const canManage = hasPermission('checklisten:manage_templates');
const canExecute = hasPermission('checklisten:execute');
// ── Queries ──
const { data: vehicleItems = [], isLoading: itemsLoading } = useQuery({
queryKey: ['checklisten-fahrzeug-items', fahrzeugId],
queryFn: () => checklistenApi.getVehicleItems(fahrzeugId),
});
const { data: templates = [], isLoading: templatesLoading } = useQuery({
queryKey: ['checklisten-fahrzeug-checklisten', fahrzeugId],
queryFn: () => checklistenApi.getChecklistenForVehicle(fahrzeugId),
});
const { data: executions = [], isLoading: executionsLoading } = useQuery({
queryKey: ['checklisten-ausfuehrungen', { fahrzeug_id: fahrzeugId }],
queryFn: () => checklistenApi.getExecutions({ fahrzeug_id: fahrzeugId }),
});
const { data: overdue = [] } = useQuery({
queryKey: ['checklisten-faellig'],
queryFn: checklistenApi.getOverdue,
});
const vehicleOverdue = overdue.filter((f) => f.fahrzeug_id === fahrzeugId);
// ── Vehicle items management ──
const [newItem, setNewItem] = useState<CreateFahrzeugItemPayload>({ bezeichnung: '', pflicht: false, sort_order: 0 });
const addItemMutation = useMutation({
mutationFn: (data: CreateFahrzeugItemPayload) => checklistenApi.addVehicleItem(fahrzeugId, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-fahrzeug-items', fahrzeugId] }); setNewItem({ bezeichnung: '', pflicht: false, sort_order: 0 }); showSuccess('Item hinzugef\u00fcgt'); },
onError: () => showError('Fehler beim Hinzuf\u00fcgen'),
});
const deleteItemMutation = useMutation({
mutationFn: (id: number) => checklistenApi.deleteVehicleItem(id),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['checklisten-fahrzeug-items', fahrzeugId] }); showSuccess('Item entfernt'); },
onError: () => showError('Fehler beim Entfernen'),
});
const toggleItemMutation = useMutation({
mutationFn: ({ id, aktiv }: { id: number; aktiv: boolean }) => checklistenApi.updateVehicleItem(id, { aktiv }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['checklisten-fahrzeug-items', fahrzeugId] }),
onError: () => showError('Fehler beim Aktualisieren'),
});
return (
<Box>
{/* Section 1: Vehicle-specific items */}
{canManage && (
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 1.5 }}>Fahrzeugspezifische Checklisten-Items</Typography>
{itemsLoading ? (
<CircularProgress size={24} />
) : (
<>
<TableContainer component={Paper} variant="outlined" sx={{ mb: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Bezeichnung</TableCell>
<TableCell align="center">Pflicht</TableCell>
<TableCell align="center">Aktiv</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{vehicleItems.length === 0 ? (
<TableRow><TableCell colSpan={4} align="center">Keine fahrzeugspezifischen Items</TableCell></TableRow>
) : (
vehicleItems.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.bezeichnung}</TableCell>
<TableCell align="center">{item.pflicht ? <Chip label="Pflicht" size="small" color="warning" /> : '\u2013'}</TableCell>
<TableCell align="center">
<Switch
size="small"
checked={item.aktiv}
onChange={() => toggleItemMutation.mutate({ id: item.id, aktiv: !item.aktiv })}
/>
</TableCell>
<TableCell align="right">
<IconButton size="small" color="error" onClick={() => deleteItemMutation.mutate(item.id)}>
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField size="small" placeholder="Neues Item..." value={newItem.bezeichnung} onChange={(e) => setNewItem((n) => ({ ...n, bezeichnung: e.target.value }))} sx={{ flexGrow: 1 }} />
<FormControlLabel control={<Switch size="small" checked={newItem.pflicht} onChange={(e) => setNewItem((n) => ({ ...n, pflicht: e.target.checked }))} />} label="Pflicht" />
<Button size="small" variant="outlined" startIcon={<Add />} disabled={!newItem.bezeichnung.trim() || addItemMutation.isPending} onClick={() => addItemMutation.mutate(newItem)}>
Hinzuf\u00fcgen
</Button>
</Box>
</>
)}
</Box>
)}
{/* Section 2: Applicable templates */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 1.5 }}>Anwendbare Vorlagen</Typography>
{templatesLoading ? (
<CircularProgress size={24} />
) : templates.length === 0 ? (
<Typography color="text.secondary">Keine Vorlagen f\u00fcr dieses Fahrzeug.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Vorlage</TableCell>
<TableCell>Intervall</TableCell>
<TableCell>N\u00e4chste F\u00e4lligkeit</TableCell>
<TableCell align="right">Aktion</TableCell>
</TableRow>
</TableHead>
<TableBody>
{templates.map((t) => {
const due = vehicleOverdue.find((f) => f.vorlage_id === t.id);
return (
<TableRow key={t.id}>
<TableCell>{t.name}</TableCell>
<TableCell>
{t.intervall === 'weekly' ? 'W\u00f6chentlich' : t.intervall === 'monthly' ? 'Monatlich' : t.intervall === 'yearly' ? 'J\u00e4hrlich' : t.intervall === 'custom' ? `${t.intervall_tage ?? '?'} Tage` : '\u2013'}
</TableCell>
<TableCell>
{due ? (
<Chip icon={<Warning />} label={`F\u00e4llig: ${formatDate(due.naechste_faellig_am)}`} color="error" size="small" />
) : (
<Typography variant="body2" color="text.secondary">Aktuell</Typography>
)}
</TableCell>
<TableCell align="right">
{canExecute && (
<Button
size="small"
variant="outlined"
startIcon={<PlayArrow />}
onClick={() => navigate(`/checklisten/ausfuehrung/new?fahrzeug=${fahrzeugId}&vorlage=${t.id}`)}
>
Ausf\u00fchren
</Button>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
{/* Section 3: Recent executions */}
<Box>
<Typography variant="h6" sx={{ mb: 1.5 }}>Letzte Ausf\u00fchrungen</Typography>
{executionsLoading ? (
<CircularProgress size={24} />
) : executions.length === 0 ? (
<Typography color="text.secondary">Noch keine Ausf\u00fchrungen f\u00fcr dieses Fahrzeug.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Datum</TableCell>
<TableCell>Vorlage</TableCell>
<TableCell>Status</TableCell>
<TableCell>Ausgef\u00fchrt von</TableCell>
<TableCell>Freigegeben von</TableCell>
</TableRow>
</TableHead>
<TableBody>
{executions.slice(0, 20).map((e) => (
<TableRow key={e.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/checklisten/ausfuehrung/${e.id}`)}>
<TableCell>{formatDate(e.ausgefuehrt_am ?? e.created_at)}</TableCell>
<TableCell>{e.vorlage_name ?? '\u2013'}</TableCell>
<TableCell>
<Chip label={CHECKLIST_STATUS_LABELS[e.status]} color={CHECKLIST_STATUS_COLORS[e.status]} size="small" />
</TableCell>
<TableCell>{e.ausgefuehrt_von_name ?? '\u2013'}</TableCell>
<TableCell>{e.freigegeben_von_name ?? '\u2013'}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</Box>
);
};
export default FahrzeugChecklistTab;

View File

@@ -0,0 +1,328 @@
import { useMemo } from 'react';
import {
Box, Paper, Typography, Chip, Avatar,
} from '@mui/material';
import {
BugReport, FiberNew, HelpOutline,
AccessTime as AccessTimeIcon,
} from '@mui/icons-material';
import {
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
closestCorners,
} from '@dnd-kit/core';
import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useState } from 'react';
import type { Issue, IssueStatusDef, IssuePriorityDef } from '../../types/issue.types';
// ── Helpers ──
const PRIO_COLORS: Record<string, string> = {
hoch: '#d32f2f', mittel: '#ed6c02', niedrig: '#9e9e9e',
};
const MUI_THEME_COLORS: Record<string, string> = {
default: '#9e9e9e', primary: '#1976d2', secondary: '#9c27b0',
error: '#d32f2f', info: '#0288d1', success: '#2e7d32', warning: '#ed6c02',
};
const ICON_MAP: Record<string, JSX.Element> = {
BugReport: <BugReport fontSize="small" />,
FiberNew: <FiberNew fontSize="small" />,
HelpOutline: <HelpOutline fontSize="small" />,
};
function getPrioColor(priorities: IssuePriorityDef[], key: string) {
return priorities.find(p => p.schluessel === key)?.farbe ?? PRIO_COLORS[key] ?? '#9e9e9e';
}
function isOverdue(issue: Issue): boolean {
if (!issue.faellig_am) return false;
return new Date(issue.faellig_am) < new Date();
}
// ── Kanban Card ──
function KanbanCard({
issue,
priorities,
onClick,
isDragging,
}: {
issue: Issue;
priorities: IssuePriorityDef[];
onClick: (id: number) => void;
isDragging?: boolean;
}) {
const prioColor = getPrioColor(priorities, issue.prioritaet);
const icon = ICON_MAP[issue.typ_icon || ''] || <HelpOutline fontSize="small" />;
const overdue = isOverdue(issue);
return (
<Paper
variant="outlined"
onClick={() => onClick(issue.id)}
sx={{
p: 1.5,
cursor: 'pointer',
borderLeft: `3px solid ${prioColor}`,
opacity: isDragging ? 0.5 : 1,
'&:hover': { bgcolor: 'action.hover' },
display: 'flex',
flexDirection: 'column',
gap: 0.5,
}}
>
{/* Title row with type icon */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5 }}>
<Box sx={{ display: 'inline-flex', mt: 0.25, color: `${issue.typ_farbe || 'action'}.main` }}>
{icon}
</Box>
<Typography variant="body2" sx={{ fontWeight: 500, flex: 1, lineHeight: 1.3 }}>
{issue.titel}
</Typography>
</Box>
{/* Bottom row: assignee + overdue */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 0.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{issue.zugewiesen_an_name && (
<Avatar sx={{ width: 20, height: 20, fontSize: 10 }}>
{issue.zugewiesen_an_name.charAt(0).toUpperCase()}
</Avatar>
)}
<Typography variant="caption" color="text.secondary">
{issue.zugewiesen_an_name || ''}
</Typography>
</Box>
{overdue && (
<Chip
icon={<AccessTimeIcon sx={{ fontSize: 14 }} />}
label="Überfällig"
size="small"
color="error"
variant="outlined"
sx={{ height: 20, '& .MuiChip-label': { px: 0.5, fontSize: 11 } }}
/>
)}
</Box>
</Paper>
);
}
// ── Sortable wrapper ──
function SortableCard({
issue,
priorities,
onClick,
}: {
issue: Issue;
priorities: IssuePriorityDef[];
onClick: (id: number) => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: issue.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<Box ref={setNodeRef} style={style} {...attributes} {...listeners}>
<KanbanCard issue={issue} priorities={priorities} onClick={onClick} isDragging={isDragging} />
</Box>
);
}
// ── Column ──
function KanbanColumn({
statusDef,
issues,
priorities,
onClick,
}: {
statusDef: IssueStatusDef;
issues: Issue[];
priorities: IssuePriorityDef[];
onClick: (id: number) => void;
}) {
const chipColor = MUI_THEME_COLORS[statusDef.farbe] ?? statusDef.farbe ?? '#9e9e9e';
return (
<Box
sx={{
minWidth: 260,
maxWidth: 320,
flex: '1 1 260px',
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.default',
borderRadius: 1,
overflow: 'hidden',
}}
>
{/* Column header */}
<Box sx={{ px: 1.5, py: 1, display: 'flex', alignItems: 'center', gap: 1, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: chipColor, flexShrink: 0 }} />
<Typography variant="subtitle2" sx={{ flex: 1 }}>{statusDef.bezeichnung}</Typography>
<Typography variant="caption" color="text.secondary">{issues.length}</Typography>
</Box>
{/* Card list */}
<SortableContext items={issues.map(i => i.id)} strategy={verticalListSortingStrategy}>
<Box
sx={{
p: 1,
display: 'flex',
flexDirection: 'column',
gap: 1,
minHeight: 100,
overflowY: 'auto',
flex: 1,
}}
>
{issues.length === 0 && (
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
Keine Issues
</Typography>
)}
{issues.map((issue) => (
<SortableCard key={issue.id} issue={issue} priorities={priorities} onClick={onClick} />
))}
</Box>
</SortableContext>
</Box>
);
}
// ── Main Board ──
interface KanbanBoardProps {
issues: Issue[];
statuses: IssueStatusDef[];
priorities: IssuePriorityDef[];
onNavigate: (issueId: number) => void;
onStatusChange: (issueId: number, newStatus: string) => void;
}
export default function KanbanBoard({
issues,
statuses,
priorities,
onNavigate,
onStatusChange,
}: KanbanBoardProps) {
const [activeId, setActiveId] = useState<number | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
// Active statuses sorted by sort_order
const activeStatuses = useMemo(
() => statuses.filter(s => s.aktiv).sort((a, b) => a.sort_order - b.sort_order),
[statuses],
);
// Group issues by status
const issuesByStatus = useMemo(() => {
const map: Record<string, Issue[]> = {};
for (const s of activeStatuses) map[s.schluessel] = [];
for (const issue of issues) {
if (map[issue.status]) map[issue.status].push(issue);
}
return map;
}, [issues, activeStatuses]);
const activeIssue = activeId ? issues.find(i => i.id === activeId) : null;
// Find which column (status) an issue id belongs to
function findStatusForIssue(id: number): string | undefined {
for (const [status, items] of Object.entries(issuesByStatus)) {
if (items.some(i => i.id === id)) return status;
}
return undefined;
}
function handleDragStart(event: DragStartEvent) {
setActiveId(event.active.id as number);
}
function handleDragEnd(event: DragEndEvent) {
setActiveId(null);
const { active, over } = event;
if (!over) return;
const activeIssueId = active.id as number;
const overId = over.id;
// Determine target status: if dropped over a column (status key) or over another card
let targetStatus: string | undefined;
// Check if overId is a status key (column droppable)
if (typeof overId === 'string' && activeStatuses.some(s => s.schluessel === overId)) {
targetStatus = overId;
} else {
// Dropped over another card — find that card's status
targetStatus = findStatusForIssue(overId as number);
}
if (!targetStatus) return;
const currentStatus = findStatusForIssue(activeIssueId);
if (currentStatus === targetStatus) return;
onStatusChange(activeIssueId, targetStatus);
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<Box
sx={{
display: 'flex',
gap: 1.5,
overflowX: 'auto',
pb: 1,
minHeight: 400,
}}
>
{activeStatuses.map((s) => (
<KanbanColumn
key={s.schluessel}
statusDef={s}
issues={issuesByStatus[s.schluessel] || []}
priorities={priorities}
onClick={onNavigate}
/>
))}
</Box>
<DragOverlay>
{activeIssue ? (
<KanbanCard issue={activeIssue} priorities={priorities} onClick={() => {}} />
) : null}
</DragOverlay>
</DndContext>
);
}

View File

@@ -29,6 +29,7 @@ import {
BugReport,
BookOnline,
Forum,
AssignmentTurnedIn,
} from '@mui/icons-material';
import { useNavigate, useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
@@ -136,6 +137,12 @@ const baseNavigationItems: NavigationItem[] = [
// subItems computed dynamically in navigationItems useMemo
permission: 'ausruestungsanfrage:view',
},
{
text: 'Checklisten',
icon: <AssignmentTurnedIn />,
path: '/checklisten',
permission: 'checklisten:view',
},
{
text: 'Issues',
icon: <BugReport />,