import { useState, useRef, useEffect, useCallback } from 'react'; import { Container, Typography, Card, CardContent, Grid, FormGroup, FormControlLabel, Switch, Divider, Box, ToggleButtonGroup, ToggleButton, 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, Sort, Restore, DragIndicator } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { useThemeMode } from '../contexts/ThemeContext'; import { preferencesApi } from '../services/settings'; 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: 'Checklisten', path: '/checklisten', permission: 'checklisten:view' }, { text: 'Issues', path: '/issues', permission: 'issues:view_own' }, ]; function SortableNavItem({ id, text }: { id: string; text: string }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); return ( ); } 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'], queryFn: preferencesApi.get, }); const mutation = useMutation({ mutationFn: preferencesApi.update, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); }, }); const isWidgetVisible = (key: WidgetKey) => { return preferences?.widgets?.[key] !== false; }; const toggleWidget = (key: WidgetKey) => { const current = preferences ?? {}; const widgets = { ...(current.widgets ?? {}) }; widgets[key] = !isWidgetVisible(key); 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 [localNavItems, setLocalNavItems] = useState(orderedNavItems); useEffect(() => { setLocalNavItems(orderedNavItems); // eslint-disable-next-line react-hooks/exhaustive-deps }, [visibleNavItems.map((i) => i.path).join(','), menuOrder.join(',')]); const navSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), ); const handleNavDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIdx = localNavItems.findIndex((i) => i.path === active.id); const newIdx = localNavItems.findIndex((i) => i.path === over.id); if (oldIdx === -1 || newIdx === -1) return; const newItems = arrayMove(localNavItems, oldIdx, newIdx); setLocalNavItems(newItems); const current = preferences ?? {}; mutation.mutate({ ...current, menuOrder: newItems.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'], queryFn: () => nextcloudApi.getRooms(), retry: 1, }); const ncConnected = ncData?.connected ?? false; const ncLoginName = ncData?.loginName; const [isConnecting, setIsConnecting] = useState(false); const pollIntervalRef = useRef | null>(null); const popupRef = useRef(null); // Show one-time info snackbar when not connected useEffect(() => { if (!ncLoading && !ncConnected && !sessionStorage.getItem('nextcloud-talk-notified')) { sessionStorage.setItem('nextcloud-talk-notified', '1'); showInfo('Nextcloud Talk ist nicht verbunden. Verbinde dich in den Einstellungen.'); } }, [ncLoading, ncConnected, showInfo]); const stopPolling = useCallback(() => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } if (popupRef.current && !popupRef.current.closed) { popupRef.current.close(); } popupRef.current = null; setIsConnecting(false); }, []); useEffect(() => { return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } if (popupRef.current && !popupRef.current.closed) { popupRef.current.close(); } }; }, []); const handleConnect = async () => { try { setIsConnecting(true); const { loginUrl, pollToken, pollEndpoint } = await nextcloudApi.connect(); const popup = window.open(loginUrl, '_blank', 'width=600,height=700'); popupRef.current = popup; const startTime = Date.now(); pollIntervalRef.current = setInterval(async () => { if (Date.now() - startTime > POLL_TIMEOUT) { stopPolling(); return; } if (popup && popup.closed) { stopPolling(); return; } try { const result = await nextcloudApi.poll(pollToken, pollEndpoint); if (result.completed) { stopPolling(); queryClient.invalidateQueries({ queryKey: ['nextcloud', 'connection'] }); queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] }); } } catch { // Polling error — keep trying until timeout } }, POLL_INTERVAL); } catch { setIsConnecting(false); } }; const handleDisconnect = async () => { try { await nextcloudApi.disconnect(); queryClient.invalidateQueries({ queryKey: ['nextcloud-talk'] }); queryClient.invalidateQueries({ queryKey: ['nextcloud-talk-rooms'] }); } catch { // Disconnect failed silently } }; return ( Einstellungen {/* Widget Visibility */} Dashboard-Widgets {prefsLoading ? ( ) : ( {WIDGETS.map((w) => ( toggleWidget(w.key)} disabled={mutation.isPending} /> } label={w.label} /> ))} )} {/* Menu Ordering */} Menü-Reihenfolge {prefsLoading ? ( ) : ( i.path)} strategy={verticalListSortingStrategy} > {localNavItems.map((item) => ( ))} )} {/* Nextcloud Talk */} Nextcloud Talk {ncLoading ? ( ) : ncConnected ? ( {ncLoginName && ( als {ncLoginName} )} ) : ( {isConnecting ? ( Warte auf Bestätigung... ) : ( )} )} {/* Mitgliedsprofil */} Mitgliedsprofil Persönliche Daten, Standesbuchnummer und weitere Profileinstellungen. {/* Notification Settings */} Benachrichtigungen } label="E-Mail-Benachrichtigungen" /> } label="Einsatz-Alarme" /> } label="Wartungserinnerungen" /> } label="System-Benachrichtigungen" /> (Bald verfügbar) {/* Display Settings */} Anzeigeoptionen } label="Kompakte Ansicht" /> } label="Animationen" /> (Bald verfügbar) Farbschema { if (value) setThemeMode(value); }} size="small" > System Hell Dunkel {/* Language Settings */} Sprache Aktuelle Sprache: Deutsch Kommende Features: Sprachauswahl, Datumsformat, Zeitzone {/* General Settings */} Allgemein Kommende Features: Dashboard-Layout, Standardansichten, Exporteinstellungen ); } export default Settings;