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
}
onClick={resetMenuOrder}
disabled={mutation.isPending || menuOrder.length === 0}
>
Zurücksetzen
{prefsLoading ? (
) : (
i.path)}
strategy={verticalListSortingStrategy}
>
{localNavItems.map((item) => (
))}
)}
{/* Nextcloud Talk */}
Nextcloud Talk
{ncLoading ? (
) : ncConnected ? (
{ncLoginName && (
als {ncLoginName}
)}
}
onClick={handleDisconnect}
size="small"
>
Verbindung trennen
) : (
{isConnecting ? (
Warte auf Bestätigung...
) : (
}
onClick={handleConnect}
size="small"
>
Mit Nextcloud verbinden
)}
)}
{/* Mitgliedsprofil */}
Mitgliedsprofil
Persönliche Daten, Standesbuchnummer und weitere Profileinstellungen.
}
onClick={() => navigate(`/mitglieder/${user?.id}`)}
disabled={!user?.id}
>
Zum Mitgliedsprofil
{/* 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;