408 lines
14 KiB
TypeScript
408 lines
14 KiB
TypeScript
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
import {
|
|
Container,
|
|
Typography,
|
|
Card,
|
|
CardContent,
|
|
Grid,
|
|
FormGroup,
|
|
FormControlLabel,
|
|
Switch,
|
|
Divider,
|
|
Box,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
CircularProgress,
|
|
Button,
|
|
Chip,
|
|
} from '@mui/material';
|
|
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode, Widgets, Cloud, LinkOff, Forum, Person, OpenInNew } from '@mui/icons-material';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router-dom';
|
|
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';
|
|
|
|
const POLL_INTERVAL = 2000;
|
|
const POLL_TIMEOUT = 5 * 60 * 1000;
|
|
|
|
function Settings() {
|
|
const { themeMode, setThemeMode } = useThemeMode();
|
|
const queryClient = useQueryClient();
|
|
const { showInfo } = useNotification();
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
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 });
|
|
};
|
|
|
|
// 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<ReturnType<typeof setInterval> | null>(null);
|
|
const popupRef = useRef<Window | null>(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 (
|
|
<DashboardLayout>
|
|
<Container maxWidth="lg">
|
|
<Typography variant="h4" gutterBottom sx={{ mb: 4 }}>
|
|
Einstellungen
|
|
</Typography>
|
|
|
|
<Grid container spacing={3}>
|
|
{/* Widget Visibility */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Widgets color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Dashboard-Widgets</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
{prefsLoading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
|
|
<CircularProgress size={24} />
|
|
</Box>
|
|
) : (
|
|
<FormGroup>
|
|
{WIDGETS.map((w) => (
|
|
<FormControlLabel
|
|
key={w.key}
|
|
control={
|
|
<Switch
|
|
checked={isWidgetVisible(w.key)}
|
|
onChange={() => toggleWidget(w.key)}
|
|
disabled={mutation.isPending}
|
|
/>
|
|
}
|
|
label={w.label}
|
|
/>
|
|
))}
|
|
</FormGroup>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Nextcloud Talk */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Forum color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Nextcloud Talk</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
{ncLoading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
|
|
<CircularProgress size={24} />
|
|
</Box>
|
|
) : ncConnected ? (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Chip label="Verbunden" size="small" color="success" />
|
|
{ncLoginName && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
als {ncLoginName}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
<Button
|
|
variant="outlined"
|
|
color="error"
|
|
startIcon={<LinkOff />}
|
|
onClick={handleDisconnect}
|
|
size="small"
|
|
>
|
|
Verbindung trennen
|
|
</Button>
|
|
</Box>
|
|
) : (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Chip label="Nicht verbunden" size="small" color="default" />
|
|
</Box>
|
|
{isConnecting ? (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<CircularProgress size={20} />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Warte auf Bestätigung...
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<Cloud />}
|
|
onClick={handleConnect}
|
|
size="small"
|
|
>
|
|
Mit Nextcloud verbinden
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Mitgliedsprofil */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Person color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Mitgliedsprofil</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
Persönliche Daten, Standesbuchnummer und weitere Profileinstellungen.
|
|
</Typography>
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
startIcon={<OpenInNew />}
|
|
onClick={() => navigate(`/mitglieder/${user?.id}`)}
|
|
disabled={!user?.id}
|
|
>
|
|
Zum Mitgliedsprofil
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Notification Settings */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Notifications color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Benachrichtigungen</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
<FormGroup>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="E-Mail-Benachrichtigungen"
|
|
/>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="Einsatz-Alarme"
|
|
/>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="Wartungserinnerungen"
|
|
/>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="System-Benachrichtigungen"
|
|
/>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
|
(Bald verfügbar)
|
|
</Typography>
|
|
</FormGroup>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Display Settings */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Palette color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Anzeigeoptionen</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
<FormGroup>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="Kompakte Ansicht"
|
|
/>
|
|
<FormControlLabel
|
|
control={<Switch checked={false} disabled />}
|
|
label="Animationen"
|
|
/>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
|
(Bald verfügbar)
|
|
</Typography>
|
|
<Box sx={{ mt: 1 }}>
|
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
Farbschema
|
|
</Typography>
|
|
<ToggleButtonGroup
|
|
value={themeMode}
|
|
exclusive
|
|
onChange={(_e, value) => { if (value) setThemeMode(value); }}
|
|
size="small"
|
|
>
|
|
<ToggleButton value="system">
|
|
<SettingsBrightness fontSize="small" sx={{ mr: 0.5 }} />
|
|
System
|
|
</ToggleButton>
|
|
<ToggleButton value="light">
|
|
<LightMode fontSize="small" sx={{ mr: 0.5 }} />
|
|
Hell
|
|
</ToggleButton>
|
|
<ToggleButton value="dark">
|
|
<DarkMode fontSize="small" sx={{ mr: 0.5 }} />
|
|
Dunkel
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Box>
|
|
</FormGroup>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Language Settings */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Language color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Sprache</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aktuelle Sprache: Deutsch
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
|
Kommende Features: Sprachauswahl, Datumsformat, Zeitzone
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* General Settings */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<SettingsIcon color="primary" sx={{ mr: 2 }} />
|
|
<Typography variant="h6">Allgemein</Typography>
|
|
</Box>
|
|
<Divider sx={{ mb: 2 }} />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Kommende Features: Dashboard-Layout, Standardansichten, Exporteinstellungen
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
export default Settings;
|