feat: add full-page chat route and sidebar menu ordering
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user