246 lines
8.2 KiB
JavaScript
246 lines
8.2 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
Box, Typography, Alert, IconButton, Tooltip,
|
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
|
TextField, Button, CircularProgress, Stack,
|
|
Snackbar, Select, MenuItem, FormControl, InputLabel,
|
|
} from '@mui/material';
|
|
import { DataGrid } from '@mui/x-data-grid';
|
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
|
import LockResetIcon from '@mui/icons-material/LockReset';
|
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
|
import axiosInstance from '../api/axiosInstance';
|
|
|
|
export default function UsersPage() {
|
|
const [rows, setRows] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
const [editUser, setEditUser] = useState(null);
|
|
const [editForm, setEditForm] = useState({});
|
|
const [saving, setSaving] = useState(false);
|
|
const [saveError, setSaveError] = useState('');
|
|
|
|
const [resetting, setResetting] = useState(null);
|
|
const [snackbar, setSnackbar] = useState('');
|
|
|
|
const [deleteUser, setDeleteUser] = useState(null);
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
axiosInstance.get('/users')
|
|
.then(({ data }) => setRows(data))
|
|
.catch(() => setError('Fehler beim Laden der Nutzer'))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const handleEditOpen = (user) => {
|
|
setEditUser(user);
|
|
setEditForm({
|
|
email: user.email ?? '',
|
|
nickname: user.nickname ?? '',
|
|
role: user.role?.replace('ROLE_', '') ?? '',
|
|
});
|
|
setSaveError('');
|
|
};
|
|
|
|
const handleEditClose = () => {
|
|
setEditUser(null);
|
|
setEditForm({});
|
|
};
|
|
|
|
const handleFormChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setEditForm((prev) => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
setSaveError('');
|
|
try {
|
|
const { data } = await axiosInstance.put(`/users/${editUser.id}`, editForm);
|
|
setRows((prev) => prev.map((r) => r.id === editUser.id ? { ...r, ...data } : r));
|
|
setSnackbar(`Änderungen an ${editUser.nickname} erfolgreich gespeichert.`);
|
|
handleEditClose();
|
|
} catch (err) {
|
|
setSaveError(err.response?.data?.message ?? 'Speichern fehlgeschlagen');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handlePasswordReset = async (user) => {
|
|
setResetting(user.id);
|
|
try {
|
|
await axiosInstance.post(`/users/${user.id}/reset-password`);
|
|
setSnackbar(`Passwort für ${user.nickname} wurde zurückgesetzt — E-Mail wurde versendet.`);
|
|
} catch (err) {
|
|
setSnackbar(err.response?.data?.message ?? 'Zurücksetzen fehlgeschlagen');
|
|
} finally {
|
|
setResetting(null);
|
|
}
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
setDeleting(true);
|
|
try {
|
|
await axiosInstance.delete(`/users/${deleteUser.id}`);
|
|
setRows((prev) => prev.filter((r) => r.id !== deleteUser.id));
|
|
setSnackbar(`Nutzer ${deleteUser.nickname} wurde gelöscht.`);
|
|
setDeleteUser(null);
|
|
} catch (err) {
|
|
setSnackbar(err.response?.data?.message ?? 'Löschen fehlgeschlagen');
|
|
} finally {
|
|
setDeleting(false);
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{ field: 'id', headerName: 'ID', width: 80 },
|
|
{ field: 'nickname', headerName: 'Anzeigename', flex: 1 },
|
|
{ field: 'email', headerName: 'E-Mail', flex: 1.5 },
|
|
{ field: 'role', headerName: 'Berechtigung', flex: 1.5 },
|
|
{
|
|
field: 'accountCreated',
|
|
headerName: 'Registriert',
|
|
flex: 1,
|
|
valueFormatter: (value) => value ? new Date(value).toLocaleDateString('de-DE') : '—',
|
|
},
|
|
{
|
|
field: 'actions',
|
|
headerName: '',
|
|
width: 130,
|
|
sortable: false,
|
|
renderCell: ({ row }) => (
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|
<Tooltip title="Bearbeiten">
|
|
<IconButton size="small" onClick={() => handleEditOpen(row)}>
|
|
<EditOutlinedIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip title="Passwort zurücksetzen">
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
color="warning"
|
|
onClick={() => handlePasswordReset(row)}
|
|
disabled={resetting === row.id}
|
|
>
|
|
{resetting === row.id
|
|
? <CircularProgress size={16} />
|
|
: <LockResetIcon fontSize="small" />
|
|
}
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip title="Nutzer löschen">
|
|
<IconButton size="small" color="error" onClick={() => setDeleteUser(row)}>
|
|
<DeleteOutlineIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Box>
|
|
<Typography variant="h5" fontWeight={600} mb={3}>Nutzer</Typography>
|
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
|
|
|
<Box sx={{ bgcolor: 'white', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
|
<DataGrid
|
|
rows={rows}
|
|
columns={columns}
|
|
loading={loading}
|
|
autoHeight
|
|
pageSizeOptions={[25, 50, 100]}
|
|
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
|
disableRowSelectionOnClick
|
|
sx={{ border: 'none' }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Edit Dialog */}
|
|
<Dialog open={!!editUser} onClose={handleEditClose} maxWidth="sm" fullWidth>
|
|
<DialogTitle sx={{ fontWeight: 600 }}>Nutzer bearbeiten</DialogTitle>
|
|
<DialogContent>
|
|
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
|
{saveError && <Alert severity="error">{saveError}</Alert>}
|
|
<TextField
|
|
label="E-Mail"
|
|
name="email"
|
|
type="email"
|
|
value={editForm.email ?? ''}
|
|
onChange={handleFormChange}
|
|
fullWidth
|
|
/>
|
|
<TextField
|
|
label="Anzeigename"
|
|
name="nickname"
|
|
value={editForm.nickname ?? ''}
|
|
onChange={handleFormChange}
|
|
fullWidth
|
|
/>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Berechtigung</InputLabel>
|
|
<Select
|
|
name="role"
|
|
value={editForm.role ?? ''}
|
|
label="Berechtigung"
|
|
onChange={handleFormChange}
|
|
>
|
|
<MenuItem value="ADMIN">ADMIN</MenuItem>
|
|
<MenuItem value="REPORTER">REPORTER</MenuItem>
|
|
<MenuItem value="USER">USER</MenuItem>
|
|
<MenuItem value="SITE_OWNER">SITE_OWNER</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
|
<Button onClick={handleEditClose} disabled={saving}>Abbrechen</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
|
|
>
|
|
Speichern
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Delete Bestätigungs-Dialog */}
|
|
<Dialog open={!!deleteUser} onClose={() => setDeleteUser(null)} maxWidth="xs" fullWidth>
|
|
<DialogTitle sx={{ fontWeight: 600 }}>Nutzer löschen</DialogTitle>
|
|
<DialogContent>
|
|
<Typography variant="body2">
|
|
Soll <strong>{deleteUser?.nickname}</strong> ({deleteUser?.email}) wirklich gelöscht werden? Diese Aktion kann nicht rückgängig gemacht werden.
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
|
<Button onClick={() => setDeleteUser(null)} disabled={deleting}>Abbrechen</Button>
|
|
<Button
|
|
variant="contained"
|
|
color="error"
|
|
onClick={handleDeleteConfirm}
|
|
disabled={deleting}
|
|
startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : null}
|
|
>
|
|
Löschen
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Snackbar */}
|
|
<Snackbar
|
|
open={!!snackbar}
|
|
autoHideDuration={5000}
|
|
onClose={() => setSnackbar('')}
|
|
message={snackbar}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
/>
|
|
</Box>
|
|
);
|
|
} |