refactor(sidebar): remove all dropdown sub-menus, flatten navigation to direct links
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Collapse,
|
|
||||||
Drawer,
|
Drawer,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
@@ -23,8 +22,6 @@ import {
|
|||||||
AdminPanelSettings,
|
AdminPanelSettings,
|
||||||
Settings,
|
Settings,
|
||||||
Menu as MenuIcon,
|
Menu as MenuIcon,
|
||||||
ExpandMore,
|
|
||||||
ExpandLess,
|
|
||||||
LocalShipping,
|
LocalShipping,
|
||||||
BugReport,
|
BugReport,
|
||||||
BookOnline,
|
BookOnline,
|
||||||
@@ -37,31 +34,17 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useLayout, DRAWER_WIDTH, DRAWER_WIDTH_COLLAPSED } from '../../contexts/LayoutContext';
|
import { useLayout, DRAWER_WIDTH, DRAWER_WIDTH_COLLAPSED } from '../../contexts/LayoutContext';
|
||||||
import { usePermissionContext } from '../../contexts/PermissionContext';
|
import { usePermissionContext } from '../../contexts/PermissionContext';
|
||||||
import { vehiclesApi } from '../../services/vehicles';
|
|
||||||
import { preferencesApi } from '../../services/settings';
|
import { preferencesApi } from '../../services/settings';
|
||||||
|
|
||||||
export { DRAWER_WIDTH, DRAWER_WIDTH_COLLAPSED };
|
export { DRAWER_WIDTH, DRAWER_WIDTH_COLLAPSED };
|
||||||
|
|
||||||
interface SubItem {
|
|
||||||
text: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavigationItem {
|
interface NavigationItem {
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
path: string;
|
path: string;
|
||||||
subItems?: SubItem[];
|
|
||||||
permission?: string;
|
permission?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminSubItems: SubItem[] = [
|
|
||||||
{ text: 'Information', path: '/admin?tab=0' },
|
|
||||||
{ text: 'System Mitteilungen', path: '/admin?tab=1' },
|
|
||||||
{ text: 'Tool Zugriff', path: '/admin?tab=2' },
|
|
||||||
{ text: 'Daten', path: '/admin?tab=3' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const baseNavigationItems: NavigationItem[] = [
|
const baseNavigationItems: NavigationItem[] = [
|
||||||
{
|
{
|
||||||
text: 'Dashboard',
|
text: 'Dashboard',
|
||||||
@@ -125,18 +108,12 @@ const baseNavigationItems: NavigationItem[] = [
|
|||||||
text: 'Bestellungen',
|
text: 'Bestellungen',
|
||||||
icon: <LocalShipping />,
|
icon: <LocalShipping />,
|
||||||
path: '/bestellungen',
|
path: '/bestellungen',
|
||||||
subItems: [
|
|
||||||
{ text: 'Übersicht', path: '/bestellungen?tab=0' },
|
|
||||||
{ text: 'Lieferanten', path: '/bestellungen?tab=1' },
|
|
||||||
{ text: 'Katalog', path: '/bestellungen?tab=2' },
|
|
||||||
],
|
|
||||||
permission: 'bestellungen:view',
|
permission: 'bestellungen:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Interne Bestellungen',
|
text: 'Interne Bestellungen',
|
||||||
icon: <Build />,
|
icon: <Build />,
|
||||||
path: '/ausruestungsanfrage',
|
path: '/ausruestungsanfrage',
|
||||||
// subItems computed dynamically in navigationItems useMemo
|
|
||||||
permission: 'ausruestungsanfrage:view',
|
permission: 'ausruestungsanfrage:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -149,19 +126,12 @@ const baseNavigationItems: NavigationItem[] = [
|
|||||||
text: 'Buchhaltung',
|
text: 'Buchhaltung',
|
||||||
icon: <AccountBalanceIcon />,
|
icon: <AccountBalanceIcon />,
|
||||||
path: '/buchhaltung',
|
path: '/buchhaltung',
|
||||||
subItems: [
|
|
||||||
{ text: 'Übersicht', path: '/buchhaltung?tab=0' },
|
|
||||||
{ text: 'Transaktionen', path: '/buchhaltung?tab=1' },
|
|
||||||
{ text: 'Konten', path: '/buchhaltung?tab=2' },
|
|
||||||
{ text: 'Haushaltspläne', path: '/haushaltsplan' },
|
|
||||||
],
|
|
||||||
permission: 'buchhaltung:view',
|
permission: 'buchhaltung:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Issues',
|
text: 'Issues',
|
||||||
icon: <BugReport />,
|
icon: <BugReport />,
|
||||||
path: '/issues',
|
path: '/issues',
|
||||||
// subItems computed dynamically in navigationItems useMemo
|
|
||||||
permission: 'issues:view_own',
|
permission: 'issues:view_own',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -170,7 +140,6 @@ const adminItem: NavigationItem = {
|
|||||||
text: 'Admin',
|
text: 'Admin',
|
||||||
icon: <AdminPanelSettings />,
|
icon: <AdminPanelSettings />,
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
subItems: adminSubItems,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminSettingsItem: NavigationItem = {
|
const adminSettingsItem: NavigationItem = {
|
||||||
@@ -190,13 +159,6 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
const { sidebarCollapsed, toggleSidebar } = useLayout();
|
const { sidebarCollapsed, toggleSidebar } = useLayout();
|
||||||
const { hasPermission } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
|
|
||||||
// Fetch vehicle list for dynamic dropdown sub-items
|
|
||||||
const { data: vehicleList } = useQuery({
|
|
||||||
queryKey: ['vehicles', 'sidebar'],
|
|
||||||
queryFn: () => vehiclesApi.getAll(),
|
|
||||||
staleTime: 2 * 60 * 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: preferences } = useQuery({
|
const { data: preferences } = useQuery({
|
||||||
queryKey: ['user-preferences'],
|
queryKey: ['user-preferences'],
|
||||||
queryFn: preferencesApi.get,
|
queryFn: preferencesApi.get,
|
||||||
@@ -205,88 +167,22 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
|
|
||||||
const menuOrder: string[] = (preferences?.menuOrder as string[] | undefined) ?? [];
|
const menuOrder: string[] = (preferences?.menuOrder as string[] | undefined) ?? [];
|
||||||
|
|
||||||
const vehicleSubItems: SubItem[] = useMemo(
|
|
||||||
() => {
|
|
||||||
const items: SubItem[] = [
|
|
||||||
{ text: 'Übersicht', path: '/fahrzeuge?tab=0' },
|
|
||||||
];
|
|
||||||
(vehicleList ?? []).forEach((v) => {
|
|
||||||
items.push({
|
|
||||||
text: v.bezeichnung ?? v.kurzname,
|
|
||||||
path: `/fahrzeuge/${v.id}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (hasPermission('fahrzeuge:edit')) {
|
|
||||||
items.push({ text: 'Einstellungen', path: '/fahrzeuge?tab=1' });
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
[vehicleList, hasPermission],
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigationItems = useMemo((): NavigationItem[] => {
|
const navigationItems = useMemo((): NavigationItem[] => {
|
||||||
const fahrzeugeItem: NavigationItem = {
|
|
||||||
text: 'Fahrzeuge',
|
|
||||||
icon: <DirectionsCar />,
|
|
||||||
path: '/fahrzeuge',
|
|
||||||
subItems: vehicleSubItems.length > 0 ? vehicleSubItems : undefined,
|
|
||||||
permission: 'fahrzeuge:view',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build Ausrüstungsanfrage sub-items dynamically based on permissions (tab order must match Ausruestungsanfrage.tsx)
|
|
||||||
const ausruestungSubItems: SubItem[] = [];
|
|
||||||
let ausruestungTabIdx = 0;
|
|
||||||
if (hasPermission('ausruestungsanfrage:create_request')) { ausruestungSubItems.push({ text: 'Meine Anfragen', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
|
||||||
if (hasPermission('ausruestungsanfrage:approve')) { ausruestungSubItems.push({ text: 'Alle Anfragen', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
|
||||||
if (hasPermission('ausruestungsanfrage:view')) { ausruestungSubItems.push({ text: 'Katalog', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
|
||||||
|
|
||||||
// Build Issues sub-items dynamically (tab order must match Issues.tsx)
|
|
||||||
const issuesSubItems: SubItem[] = [
|
|
||||||
{ text: 'Meine Issues', path: '/issues?tab=0' },
|
|
||||||
{ text: 'Zugewiesene', path: '/issues?tab=1' },
|
|
||||||
];
|
|
||||||
if (hasPermission('issues:view_all')) {
|
|
||||||
issuesSubItems.push({ text: 'Alle Issues', path: '/issues?tab=2' });
|
|
||||||
}
|
|
||||||
if (hasPermission('issues:edit_settings')) {
|
|
||||||
issuesSubItems.push({ text: 'Einstellungen', path: `/issues?tab=${issuesSubItems.length}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Checklisten sub-items dynamically (tab order must match Checklisten.tsx)
|
|
||||||
const checklistenSubItems: SubItem[] = [
|
|
||||||
{ text: 'Übersicht', path: '/checklisten?tab=0' },
|
|
||||||
];
|
|
||||||
if (hasPermission('checklisten:manage_templates')) {
|
|
||||||
checklistenSubItems.push({ text: 'Vorlagen', path: '/checklisten?tab=1' });
|
|
||||||
}
|
|
||||||
checklistenSubItems.push({ text: 'Historie', path: `/checklisten?tab=${checklistenSubItems.length}` });
|
|
||||||
|
|
||||||
const items = baseNavigationItems
|
const items = baseNavigationItems
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.path === '/fahrzeuge') return fahrzeugeItem;
|
// Ausrüstungsanfrage is visible if user has any of the three relevant permissions
|
||||||
if (item.path === '/ausruestung') {
|
|
||||||
const ausruestungSubs: SubItem[] = [
|
|
||||||
{ text: 'Übersicht', path: '/ausruestung?tab=0' },
|
|
||||||
];
|
|
||||||
if (hasPermission('ausruestung:manage_types')) {
|
|
||||||
ausruestungSubs.push({ text: 'Einstellungen', path: '/ausruestung?tab=1' });
|
|
||||||
}
|
|
||||||
return ausruestungSubs.length > 0 ? { ...item, subItems: ausruestungSubs } : item;
|
|
||||||
}
|
|
||||||
if (item.path === '/ausruestungsanfrage') {
|
if (item.path === '/ausruestungsanfrage') {
|
||||||
const canSeeAusruestung =
|
const canSee =
|
||||||
hasPermission('ausruestungsanfrage:view') ||
|
hasPermission('ausruestungsanfrage:view') ||
|
||||||
hasPermission('ausruestungsanfrage:create_request') ||
|
hasPermission('ausruestungsanfrage:create_request') ||
|
||||||
hasPermission('ausruestungsanfrage:approve');
|
hasPermission('ausruestungsanfrage:approve');
|
||||||
return { ...item, subItems: ausruestungSubItems, permission: canSeeAusruestung ? undefined : 'ausruestungsanfrage:view' };
|
return { ...item, permission: canSee ? undefined : 'ausruestungsanfrage:view' };
|
||||||
}
|
}
|
||||||
if (item.path === '/issues') return { ...item, subItems: issuesSubItems };
|
|
||||||
if (item.path === '/checklisten') return { ...item, subItems: checklistenSubItems };
|
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.filter((item) => !item.permission || hasPermission(item.permission));
|
.filter((item) => !item.permission || hasPermission(item.permission));
|
||||||
|
|
||||||
// Apply custom menu order: items in menuOrder are sorted to their index; rest keep relative order
|
// Apply custom menu order
|
||||||
if (menuOrder.length > 0) {
|
if (menuOrder.length > 0) {
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
const aIdx = menuOrder.indexOf(a.path);
|
const aIdx = menuOrder.indexOf(a.path);
|
||||||
@@ -299,20 +195,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return hasPermission('admin:view') ? [...items, adminItem, adminSettingsItem] : items;
|
return hasPermission('admin:view') ? [...items, adminItem, adminSettingsItem] : items;
|
||||||
}, [vehicleSubItems, hasPermission, menuOrder]);
|
}, [hasPermission, menuOrder]);
|
||||||
|
|
||||||
// Expand state for items with sub-items — auto-expand when route matches
|
|
||||||
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const isExpanded = (item: NavigationItem) => {
|
|
||||||
if (expandedItems[item.path] !== undefined) return expandedItems[item.path];
|
|
||||||
// Auto-expand when the current route matches
|
|
||||||
return location.pathname === item.path || location.pathname.startsWith(item.path + '/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleExpand = (path: string) => {
|
|
||||||
setExpandedItems((prev) => ({ ...prev, [path]: !isExpanded({ path } as NavigationItem) }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavigation = (path: string) => {
|
const handleNavigation = (path: string) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
@@ -330,98 +213,50 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
<List sx={{ flex: 1 }}>
|
<List sx={{ flex: 1 }}>
|
||||||
{navigationItems.map((item) => {
|
{navigationItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
const hasSubItems = item.subItems && item.subItems.length > 0;
|
|
||||||
const expanded = hasSubItems && isExpanded(item);
|
|
||||||
return (
|
return (
|
||||||
<Box key={item.text}>
|
<ListItem key={item.text} disablePadding>
|
||||||
<ListItem disablePadding>
|
<Tooltip
|
||||||
<Tooltip
|
title={item.text}
|
||||||
title={item.text}
|
placement="right"
|
||||||
placement="right"
|
arrow
|
||||||
arrow
|
disableHoverListener={!sidebarCollapsed}
|
||||||
disableHoverListener={!sidebarCollapsed}
|
>
|
||||||
>
|
<ListItemButton
|
||||||
<ListItemButton
|
selected={isActive}
|
||||||
selected={isActive}
|
onClick={() => handleNavigation(item.path)}
|
||||||
onClick={() => handleNavigation(item.path)}
|
aria-label={`Zu ${item.text} navigieren`}
|
||||||
aria-label={`Zu ${item.text} navigieren`}
|
sx={{
|
||||||
sx={{
|
justifyContent: sidebarCollapsed ? 'center' : 'initial',
|
||||||
justifyContent: sidebarCollapsed ? 'center' : 'initial',
|
'&.Mui-selected': {
|
||||||
'&.Mui-selected': {
|
backgroundColor: 'primary.light',
|
||||||
backgroundColor: 'primary.light',
|
color: 'primary.contrastText',
|
||||||
color: 'primary.contrastText',
|
'&:hover': {
|
||||||
'&:hover': {
|
backgroundColor: 'primary.main',
|
||||||
backgroundColor: 'primary.main',
|
|
||||||
},
|
|
||||||
'& .MuiListItemIcon-root': {
|
|
||||||
color: 'primary.contrastText',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
'& .MuiListItemIcon-root': {
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon
|
||||||
|
sx={{
|
||||||
|
color: isActive ? 'inherit' : 'text.secondary',
|
||||||
|
minWidth: sidebarCollapsed ? 0 : undefined,
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemIcon
|
{item.icon}
|
||||||
sx={{
|
</ListItemIcon>
|
||||||
color: isActive ? 'inherit' : 'text.secondary',
|
<ListItemText
|
||||||
minWidth: sidebarCollapsed ? 0 : undefined,
|
primary={item.text}
|
||||||
justifyContent: 'center',
|
sx={{
|
||||||
}}
|
display: sidebarCollapsed ? 'none' : 'block',
|
||||||
>
|
}}
|
||||||
{item.icon}
|
/>
|
||||||
</ListItemIcon>
|
</ListItemButton>
|
||||||
<ListItemText
|
</Tooltip>
|
||||||
primary={item.text}
|
</ListItem>
|
||||||
sx={{
|
|
||||||
display: sidebarCollapsed ? 'none' : 'block',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{hasSubItems && !sidebarCollapsed && (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleExpand(item.path);
|
|
||||||
}}
|
|
||||||
sx={{ color: isActive ? 'inherit' : 'text.secondary' }}
|
|
||||||
>
|
|
||||||
{expanded ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ListItemButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItem>
|
|
||||||
{hasSubItems && !sidebarCollapsed && (
|
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
|
||||||
<List disablePadding>
|
|
||||||
{item.subItems!.map((sub) => {
|
|
||||||
const isSubActive = location.pathname + location.search === sub.path;
|
|
||||||
return (
|
|
||||||
<ListItemButton
|
|
||||||
key={sub.path}
|
|
||||||
onClick={() => handleNavigation(sub.path)}
|
|
||||||
selected={isSubActive}
|
|
||||||
sx={{
|
|
||||||
pl: 4,
|
|
||||||
py: 0.5,
|
|
||||||
'&.Mui-selected': {
|
|
||||||
backgroundColor: 'primary.light',
|
|
||||||
color: 'primary.contrastText',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'primary.main',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={sub.text}
|
|
||||||
primaryTypographyProps={{ variant: 'body2' }}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
Reference in New Issue
Block a user