Bug fixes: - Remove non-existent `role` column from admin users SQL query (A1) - Fix Nextcloud Talk chat API path v4 → v1 for messages/send/read (A2) - Fix ServiceModeTab sync: useState → useEffect to reflect DB state (A3) - Guard BookStack book_slug with book_id fallback to avoid broken URLs (A4) Layout & UI: - Chat panel: sticky full-height positioning, main content scrolls independently (B1) - Vehicle booking datetime inputs: explicit text color for dark mode (B2) - AnnouncementBanner moved into grid with full-width span (B3) Features: - Per-user widget visibility preferences stored in users.preferences JSONB (C1) - Link collections: grouped external links in admin UI and dashboard widget (C2) - Admin ping history: migration 026, checked_at timestamps, expandable history rows (C4) - Service mode end date picker with scheduled deactivation display (C5) - Vikunja startup config logging and configured:false warnings (C7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box, Card, CardContent, Typography, Switch, FormControlLabel,
|
|
TextField, Button, Alert, CircularProgress,
|
|
} from '@mui/material';
|
|
import BuildIcon from '@mui/icons-material/Build';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { settingsApi } from '../../services/settings';
|
|
import { useNotification } from '../../contexts/NotificationContext';
|
|
|
|
export default function ServiceModeTab() {
|
|
const queryClient = useQueryClient();
|
|
const { showSuccess, showError } = useNotification();
|
|
|
|
const { data: setting, isLoading } = useQuery({
|
|
queryKey: ['admin', 'settings', 'service_mode'],
|
|
queryFn: () => settingsApi.get('service_mode'),
|
|
});
|
|
|
|
const currentValue = setting?.value ?? { active: false, message: '' };
|
|
const [active, setActive] = useState<boolean>(currentValue.active ?? false);
|
|
const [message, setMessage] = useState<string>(currentValue.message ?? '');
|
|
const [endsAt, setEndsAt] = useState<string>('');
|
|
|
|
// Sync state when data loads
|
|
useEffect(() => {
|
|
if (setting?.value) {
|
|
setActive(setting.value.active ?? false);
|
|
setMessage(setting.value.message ?? '');
|
|
setEndsAt(setting.value.ends_at ?? '');
|
|
}
|
|
}, [setting]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (value: { active: boolean; message: string; ends_at?: string | null }) =>
|
|
settingsApi.update('service_mode', value),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['service-mode'] });
|
|
queryClient.invalidateQueries({ queryKey: ['admin', 'settings', 'service_mode'] });
|
|
showSuccess(active ? 'Wartungsmodus aktiviert' : 'Wartungsmodus deaktiviert');
|
|
},
|
|
onError: () => showError('Einstellung konnte nicht gespeichert werden'),
|
|
});
|
|
|
|
const handleSave = () => {
|
|
mutation.mutate({ active, message, ends_at: endsAt || null });
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ maxWidth: 600 }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
|
<BuildIcon color={active ? 'error' : 'action'} />
|
|
<Typography variant="h6">Wartungsmodus</Typography>
|
|
</Box>
|
|
|
|
{active && (
|
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
Wartungsmodus ist aktiv. Normale Benutzer sehen die Wartungsseite.
|
|
</Alert>
|
|
)}
|
|
|
|
{active && endsAt && (
|
|
<Alert severity="info" sx={{ mb: 2 }}>
|
|
Wartungsmodus endet automatisch am {new Date(endsAt).toLocaleString('de-DE')}.
|
|
</Alert>
|
|
)}
|
|
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={active}
|
|
onChange={(e) => setActive(e.target.checked)}
|
|
color="error"
|
|
/>
|
|
}
|
|
label={active ? 'Wartungsmodus aktiviert' : 'Wartungsmodus deaktiviert'}
|
|
sx={{ mb: 3, display: 'block' }}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={3}
|
|
label="Nachricht für Benutzer"
|
|
placeholder="Das Dashboard befindet sich aktuell im Wartungsmodus..."
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
sx={{ mb: 3 }}
|
|
helperText="Diese Nachricht sehen Benutzer auf der Wartungsseite."
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Automatisch deaktivieren am"
|
|
type="datetime-local"
|
|
value={endsAt}
|
|
onChange={(e) => setEndsAt(e.target.value)}
|
|
InputLabelProps={{ shrink: true }}
|
|
helperText="Optional: Wartungsmodus wird automatisch zu diesem Zeitpunkt deaktiviert."
|
|
sx={{ mb: 3, '& input': { color: 'text.primary' } }}
|
|
/>
|
|
|
|
<Button
|
|
variant="contained"
|
|
color={active ? 'error' : 'primary'}
|
|
onClick={handleSave}
|
|
disabled={mutation.isPending}
|
|
startIcon={mutation.isPending ? <CircularProgress size={16} /> : <BuildIcon />}
|
|
>
|
|
{mutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
}
|