Files
DeineDorfApp-Admin-Panel/src/pages/UsersPage.jsx
2026-04-09 13:56:39 +02:00

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