new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 14:01:39 +01:00
parent d2dc64d54a
commit 3326156b15
35 changed files with 1341 additions and 257 deletions

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

View File

@@ -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[] = [