Files
dashboard/frontend/src/components/admin/ServiceModeTab.tsx
Matthias Hochmeister a5cd78f01f feat: bug fixes, layout improvements, and new features
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>
2026-03-12 14:57:54 +01:00

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>
);
}