refactor(mitglieder): split member profile into Stammdaten/Ausrüstung/Qualifikationen tabs with sub-tabs per qualification type
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Paper, Typography, TextField, Button, Alert,
|
||||
CircularProgress, Divider, Autocomplete,
|
||||
CircularProgress, Divider, Autocomplete, Chip,
|
||||
} from '@mui/material';
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
||||
import { api } from '../../services/api';
|
||||
import { adminApi } from '../../services/admin';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
@@ -60,26 +61,45 @@ export default function DataManagementTab() {
|
||||
queryKey: ['admin', 'users'],
|
||||
queryFn: adminApi.getUsers,
|
||||
});
|
||||
const [selectedUser, setSelectedUser] = useState<UserOverview | null>(null);
|
||||
const [selectedUsers, setSelectedUsers] = useState<UserOverview[]>([]);
|
||||
const [fdiskInput, setFdiskInput] = useState('');
|
||||
const [purging, setPurging] = useState(false);
|
||||
const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false);
|
||||
|
||||
const handleAddUser = useCallback((user: UserOverview) => {
|
||||
setSelectedUsers(prev => prev.some(u => u.id === user.id) ? prev : [...prev, user]);
|
||||
}, []);
|
||||
|
||||
const handleRemoveUser = useCallback((userId: string) => {
|
||||
setSelectedUsers(prev => prev.filter(u => u.id !== userId));
|
||||
}, []);
|
||||
|
||||
const handleAddAll = useCallback(() => {
|
||||
setSelectedUsers(users);
|
||||
}, [users]);
|
||||
|
||||
const handlePurge = useCallback(async () => {
|
||||
if (!selectedUser) return;
|
||||
if (selectedUsers.length === 0) return;
|
||||
setPurging(true);
|
||||
try {
|
||||
const result = await adminApi.purgeFdiskData(selectedUser.id);
|
||||
const total = result.profileFieldsCleared + result.ausbildungen + result.befoerderungen + result.untersuchungen + result.fahrgenehmigungen;
|
||||
showSuccess(`${total} FDISK-Eintraege fuer ${selectedUser.name || selectedUser.email} geloescht`);
|
||||
setSelectedUser(null);
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Fehler beim Loeschen';
|
||||
showError(msg);
|
||||
} finally {
|
||||
setPurging(false);
|
||||
setPurgeConfirmOpen(false);
|
||||
let totalDeleted = 0;
|
||||
let errorCount = 0;
|
||||
for (const user of selectedUsers) {
|
||||
try {
|
||||
const result = await adminApi.purgeFdiskData(user.id);
|
||||
totalDeleted += result.profileFieldsCleared + result.ausbildungen + result.befoerderungen + result.untersuchungen + result.fahrgenehmigungen;
|
||||
} catch {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}, [selectedUser, showSuccess, showError]);
|
||||
setPurging(false);
|
||||
setPurgeConfirmOpen(false);
|
||||
if (errorCount === 0) {
|
||||
showSuccess(`${totalDeleted} FDISK-Eintraege fuer ${selectedUsers.length} ${selectedUsers.length === 1 ? 'Benutzer' : 'Benutzer'} geloescht`);
|
||||
setSelectedUsers([]);
|
||||
} else {
|
||||
showError(`${errorCount} von ${selectedUsers.length} Benutzern konnten nicht verarbeitet werden`);
|
||||
}
|
||||
}, [selectedUsers, showSuccess, showError]);
|
||||
|
||||
const [states, setStates] = useState<Record<string, SectionState>>(() =>
|
||||
Object.fromEntries(SECTIONS.map(s => [s.key, { days: s.defaultDays, previewCount: null, loading: false }]))
|
||||
@@ -168,38 +188,73 @@ export default function DataManagementTab() {
|
||||
{/* FDISK data purge */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>
|
||||
FDISK-Daten eines Benutzers loeschen
|
||||
FDISK-Daten loeschen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Loescht alle FDISK-synchronisierten Daten eines Benutzers: Profilfelder, Ausbildungen,
|
||||
Loescht alle FDISK-synchronisierten Daten der ausgewaehlten Benutzer: Profilfelder, Ausbildungen,
|
||||
Befoerderungen, Untersuchungen und Fahrgenehmigungen. Beim naechsten FDISK-Sync werden
|
||||
die Daten erneut importiert.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Autocomplete
|
||||
options={users}
|
||||
options={users.filter(u => !selectedUsers.some(s => s.id === u.id))}
|
||||
loading={usersLoading}
|
||||
value={selectedUser}
|
||||
onChange={(_e, v) => setSelectedUser(v)}
|
||||
value={null}
|
||||
inputValue={fdiskInput}
|
||||
onInputChange={(_e, val, reason) => {
|
||||
if (reason === 'input') setFdiskInput(val);
|
||||
}}
|
||||
onChange={(_e, user) => {
|
||||
if (user) {
|
||||
handleAddUser(user);
|
||||
setFdiskInput('');
|
||||
}
|
||||
}}
|
||||
getOptionLabel={(u) => `${u.name || 'Kein Name'} (${u.email})`}
|
||||
isOptionEqualToValue={(a, b) => a.id === b.id}
|
||||
sx={{ minWidth: 320, flex: 1 }}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Benutzer waehlen" size="small" />
|
||||
<TextField {...params} label="Benutzer hinzufuegen" size="small" />
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={!selectedUser || purging}
|
||||
startIcon={purging ? <CircularProgress size={16} /> : <DeleteSweepIcon />}
|
||||
onClick={() => setPurgeConfirmOpen(true)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<GroupAddIcon />}
|
||||
onClick={handleAddAll}
|
||||
disabled={usersLoading || users.length === 0 || selectedUsers.length === users.length}
|
||||
>
|
||||
FDISK-Daten loeschen
|
||||
Alle hinzufuegen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{selectedUsers.length > 0 && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2 }}>
|
||||
{selectedUsers.map(u => (
|
||||
<Chip
|
||||
key={u.id}
|
||||
label={u.name || u.email}
|
||||
onDelete={() => handleRemoveUser(u.id)}
|
||||
size="small"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={purging}
|
||||
startIcon={purging ? <CircularProgress size={16} /> : <DeleteSweepIcon />}
|
||||
onClick={() => setPurgeConfirmOpen(true)}
|
||||
>
|
||||
FDISK-Daten loeschen ({selectedUsers.length})
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -207,9 +262,14 @@ export default function DataManagementTab() {
|
||||
onClose={() => !purging && setPurgeConfirmOpen(false)}
|
||||
onConfirm={handlePurge}
|
||||
title="FDISK-Daten loeschen?"
|
||||
message={selectedUser ? (
|
||||
message={selectedUsers.length > 0 ? (
|
||||
<>
|
||||
Alle FDISK-synchronisierten Daten fuer <strong>{selectedUser.name ?? selectedUser.email}</strong> werden geloescht:
|
||||
Alle FDISK-synchronisierten Daten fuer{' '}
|
||||
{selectedUsers.length === 1
|
||||
? <strong>{selectedUsers[0].name ?? selectedUsers[0].email}</strong>
|
||||
: <strong>{selectedUsers.length} Benutzer</strong>
|
||||
}{' '}
|
||||
werden geloescht:
|
||||
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
|
||||
<li>Profilfelder (Status, Dienstgrad, Geburtsdatum, Geburtsort, Geschlecht, Beruf, Wohnort, PLZ)</li>
|
||||
<li>Alle Ausbildungen, Befoerderungen, Untersuchungen und Fahrgenehmigungen</li>
|
||||
|
||||
Reference in New Issue
Block a user