feat: add full-page chat route and sidebar menu ordering

This commit is contained in:
Matthias Hochmeister
2026-03-27 18:00:58 +01:00
parent 1a66a66aab
commit c1b4a92a12
7 changed files with 284 additions and 24 deletions

View File

@@ -15,8 +15,12 @@ import {
CircularProgress,
Button,
Chip,
IconButton,
List,
ListItem,
ListItemText,
} from '@mui/material';
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode, Widgets, Cloud, LinkOff, Forum, Person, OpenInNew } from '@mui/icons-material';
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode, Widgets, Cloud, LinkOff, Forum, Person, OpenInNew, ArrowUpward, ArrowDownward, Sort, Restore } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
@@ -26,16 +30,34 @@ import { WIDGETS, WidgetKey } from '../constants/widgets';
import { nextcloudApi } from '../services/nextcloud';
import { useNotification } from '../contexts/NotificationContext';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
const POLL_INTERVAL = 2000;
const POLL_TIMEOUT = 5 * 60 * 1000;
// Ordered list of nav items eligible for reordering (mirrors baseNavigationItems, excluding admin/settings)
const ORDERABLE_NAV_ITEMS = [
{ text: 'Dashboard', path: '/dashboard', permission: undefined as string | undefined },
{ text: 'Chat', path: '/chat', permission: undefined as string | undefined },
{ text: 'Kalender', path: '/kalender', permission: 'kalender:view' },
{ text: 'Fahrzeugbuchungen', path: '/fahrzeugbuchungen', permission: 'fahrzeugbuchungen:view' },
{ text: 'Fahrzeuge', path: '/fahrzeuge', permission: 'fahrzeuge:view' },
{ text: 'Ausrüstung', path: '/ausruestung', permission: 'ausruestung:view' },
{ text: 'Mitglieder', path: '/mitglieder', permission: 'mitglieder:view_own' },
{ text: 'Atemschutz', path: '/atemschutz', permission: 'atemschutz:view' },
{ text: 'Wissen', path: '/wissen', permission: 'wissen:view' },
{ text: 'Bestellungen', path: '/bestellungen', permission: 'bestellungen:view' },
{ text: 'Interne Bestellungen', path: '/ausruestungsanfrage', permission: 'ausruestungsanfrage:view' },
{ text: 'Issues', path: '/issues', permission: 'issues:view_own' },
];
function Settings() {
const { themeMode, setThemeMode } = useThemeMode();
const queryClient = useQueryClient();
const { showInfo } = useNotification();
const { user } = useAuth();
const navigate = useNavigate();
const { hasPermission } = usePermissionContext();
const { data: preferences, isLoading: prefsLoading } = useQuery({
queryKey: ['user-preferences'],
@@ -60,6 +82,36 @@ function Settings() {
mutation.mutate({ ...current, widgets });
};
// Menu ordering
const menuOrder: string[] = (preferences?.menuOrder as string[] | undefined) ?? [];
const visibleNavItems = ORDERABLE_NAV_ITEMS.filter(
(item) => !item.permission || hasPermission(item.permission),
);
const orderedNavItems = [...visibleNavItems].sort((a, b) => {
const aIdx = menuOrder.indexOf(a.path);
const bIdx = menuOrder.indexOf(b.path);
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
if (aIdx !== -1) return -1;
if (bIdx !== -1) return 1;
return 0;
});
const moveNavItem = (fromIdx: number, toIdx: number) => {
if (toIdx < 0 || toIdx >= orderedNavItems.length) return;
const newOrder = [...orderedNavItems];
const [moved] = newOrder.splice(fromIdx, 1);
newOrder.splice(toIdx, 0, moved);
const current = preferences ?? {};
mutation.mutate({ ...current, menuOrder: newOrder.map((i) => i.path) });
};
const resetMenuOrder = () => {
const current = preferences ?? {};
const updated = { ...current };
delete updated.menuOrder;
mutation.mutate(updated);
};
// Nextcloud Talk connection
const { data: ncData, isLoading: ncLoading } = useQuery({
queryKey: ['nextcloud-talk-rooms'],
@@ -194,6 +246,70 @@ function Settings() {
</Card>
</Grid>
{/* Menu Ordering */}
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Sort color="primary" sx={{ mr: 2 }} />
<Typography variant="h6">Menü-Reihenfolge</Typography>
</Box>
<Button
size="small"
startIcon={<Restore />}
onClick={resetMenuOrder}
disabled={mutation.isPending || menuOrder.length === 0}
>
Zurücksetzen
</Button>
</Box>
<Divider sx={{ mb: 1 }} />
{prefsLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
<CircularProgress size={24} />
</Box>
) : (
<List disablePadding>
{orderedNavItems.map((item, idx) => (
<ListItem
key={item.path}
disablePadding
sx={{ py: 0.25 }}
secondaryAction={
<Box>
<IconButton
size="small"
onClick={() => moveNavItem(idx, idx - 1)}
disabled={idx === 0 || mutation.isPending}
aria-label="Nach oben"
>
<ArrowUpward fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={() => moveNavItem(idx, idx + 1)}
disabled={idx === orderedNavItems.length - 1 || mutation.isPending}
aria-label="Nach unten"
>
<ArrowDownward fontSize="small" />
</IconButton>
</Box>
}
>
<ListItemText
primary={item.text}
primaryTypographyProps={{ variant: 'body2' }}
sx={{ pr: 8 }}
/>
</ListItem>
))}
</List>
)}
</CardContent>
</Card>
</Grid>
{/* Nextcloud Talk */}
<Grid item xs={12} md={6}>
<Card>