new features
This commit is contained in:
170
frontend/src/components/admin/DataManagementTab.tsx
Normal file
170
frontend/src/components/admin/DataManagementTab.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Paper, Typography, TextField, Button, Alert,
|
||||
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
|
||||
CircularProgress, Divider,
|
||||
} from '@mui/material';
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||
import { api } from '../../services/api';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
|
||||
interface CleanupSection {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
defaultDays: number;
|
||||
}
|
||||
|
||||
const SECTIONS: CleanupSection[] = [
|
||||
{ key: 'notifications', label: 'Benachrichtigungen', description: 'Alte Benachrichtigungen aller Benutzer entfernen.', defaultDays: 90 },
|
||||
{ key: 'audit-log', label: 'Audit-Log', description: 'Alte Audit-Log Eintraege entfernen.', defaultDays: 365 },
|
||||
{ key: 'events', label: 'Veranstaltungen', description: 'Vergangene Veranstaltungen entfernen (nach Enddatum).', defaultDays: 365 },
|
||||
{ key: 'bookings', label: 'Fahrzeugbuchungen', description: 'Abgeschlossene oder stornierte Buchungen entfernen.', defaultDays: 180 },
|
||||
{ key: 'orders', label: 'Bestellungen', description: 'Abgeschlossene Bestellungen entfernen.', defaultDays: 365 },
|
||||
{ key: 'vehicle-history', label: 'Fahrzeug-Wartungslog', description: 'Alte Fahrzeug-Wartungseintraege entfernen.', defaultDays: 730 },
|
||||
{ key: 'equipment-history', label: 'Ausruestungs-Wartungslog', description: 'Alte Ausruestungs-Wartungseintraege entfernen.', defaultDays: 730 },
|
||||
];
|
||||
|
||||
interface SectionState {
|
||||
days: number;
|
||||
previewCount: number | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default function DataManagementTab() {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const [states, setStates] = useState<Record<string, SectionState>>(() =>
|
||||
Object.fromEntries(SECTIONS.map(s => [s.key, { days: s.defaultDays, previewCount: null, loading: false }]))
|
||||
);
|
||||
|
||||
const [confirmDialog, setConfirmDialog] = useState<{ key: string; label: string; count: number } | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const updateState = useCallback((key: string, partial: Partial<SectionState>) => {
|
||||
setStates(prev => ({ ...prev, [key]: { ...prev[key], ...partial } }));
|
||||
}, []);
|
||||
|
||||
const handlePreview = useCallback(async (key: string) => {
|
||||
const s = states[key];
|
||||
if (!s || s.days < 1) return;
|
||||
updateState(key, { loading: true, previewCount: null });
|
||||
try {
|
||||
const res = await api.delete(`/api/admin/cleanup/${key}`, { data: { olderThanDays: s.days, confirm: false } });
|
||||
updateState(key, { previewCount: res.data.data.count, loading: false });
|
||||
} catch {
|
||||
showError('Vorschau konnte nicht geladen werden');
|
||||
updateState(key, { loading: false });
|
||||
}
|
||||
}, [states, updateState, showError]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!confirmDialog) return;
|
||||
const { key } = confirmDialog;
|
||||
const s = states[key];
|
||||
setDeleting(true);
|
||||
try {
|
||||
const res = await api.delete(`/api/admin/cleanup/${key}`, { data: { olderThanDays: s.days, confirm: true } });
|
||||
const deleted = res.data.data.count;
|
||||
showSuccess(`${deleted} Eintraege geloescht`);
|
||||
updateState(key, { previewCount: null });
|
||||
} catch {
|
||||
showError('Loeschen fehlgeschlagen');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
setConfirmDialog(null);
|
||||
}
|
||||
}, [confirmDialog, states, updateState, showSuccess, showError]);
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 800 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>Datenverwaltung</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Alte Daten bereinigen, um die Datenbank schlank zu halten. Geloeschte Daten koennen nicht wiederhergestellt werden.
|
||||
</Typography>
|
||||
|
||||
{SECTIONS.map((section, idx) => {
|
||||
const s = states[section.key];
|
||||
return (
|
||||
<Paper key={section.key} sx={{ p: 3, mb: 2 }}>
|
||||
<Typography variant="subtitle1" fontWeight={600}>{section.label}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>{section.description}</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Aelter als (Tage)"
|
||||
type="number"
|
||||
size="small"
|
||||
value={s.days}
|
||||
onChange={e => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (v > 0) updateState(section.key, { days: v, previewCount: null });
|
||||
}}
|
||||
sx={{ width: 160 }}
|
||||
inputProps={{ min: 1, max: 3650 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handlePreview(section.key)}
|
||||
disabled={s.loading}
|
||||
startIcon={s.loading ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
Vorschau
|
||||
</Button>
|
||||
|
||||
{s.previewCount !== null && (
|
||||
<>
|
||||
<Alert severity={s.previewCount > 0 ? 'warning' : 'info'} sx={{ py: 0 }}>
|
||||
{s.previewCount} {s.previewCount === 1 ? 'Eintrag' : 'Eintraege'} gefunden
|
||||
</Alert>
|
||||
|
||||
{s.previewCount > 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
startIcon={<DeleteSweepIcon />}
|
||||
onClick={() => setConfirmDialog({ key: section.key, label: section.label, count: s.previewCount! })}
|
||||
>
|
||||
Loeschen
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{idx < SECTIONS.length - 1 && <Divider sx={{ mt: 2, display: 'none' }} />}
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
|
||||
<Dialog open={!!confirmDialog} onClose={() => !deleting && setConfirmDialog(null)}>
|
||||
<DialogTitle>Daten loeschen?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{confirmDialog && (
|
||||
<>
|
||||
<strong>{confirmDialog.count}</strong> {confirmDialog.count === 1 ? 'Eintrag' : 'Eintraege'} aus <strong>{confirmDialog.label}</strong> werden
|
||||
unwiderruflich geloescht. Dieser Vorgang kann nicht rueckgaengig gemacht werden.
|
||||
</>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDialog(null)} disabled={deleting}>Abbrechen</Button>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deleting}
|
||||
startIcon={deleting ? <CircularProgress size={16} /> : <DeleteSweepIcon />}
|
||||
>
|
||||
{deleting ? 'Wird geloescht...' : 'Endgueltig loeschen'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -64,6 +64,7 @@ const adminSubItems: SubItem[] = [
|
||||
{ text: 'FDISK Sync', path: '/admin?tab=6' },
|
||||
{ text: 'Berechtigungen', path: '/admin?tab=7' },
|
||||
{ text: 'Bestellungen', path: '/admin?tab=8' },
|
||||
{ text: 'Datenverwaltung', path: '/admin?tab=9' },
|
||||
];
|
||||
|
||||
const baseNavigationItems: NavigationItem[] = [
|
||||
|
||||
Reference in New Issue
Block a user