initial
This commit is contained in:
200
src/pages/UsersPage.jsx
Normal file
200
src/pages/UsersPage.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box, Typography, Alert, IconButton, Tooltip,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
TextField, Button, CircularProgress, Stack, Divider,
|
||||
Snackbar,
|
||||
} 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 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); // user id der gerade resettet wird
|
||||
const [snackbar, setSnackbar] = useState(''); // Erfolgsmeldung
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance.get('/api/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 ?? '',
|
||||
});
|
||||
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(`/api/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(`/api/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 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: 100,
|
||||
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>
|
||||
</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
|
||||
/>
|
||||
<TextField
|
||||
label="Berechtigung"
|
||||
name="role"
|
||||
select
|
||||
value={editForm.role ?? ''}
|
||||
onChange={handleFormChange}
|
||||
fullWidth
|
||||
SelectProps={{ native: true }}
|
||||
>
|
||||
<option value="ADMIN">ADMIN</option>
|
||||
<option value="REPORTER">REPORTER</option>
|
||||
<option value="USER">USER</option>
|
||||
</TextField>
|
||||
</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>
|
||||
|
||||
{/* Snackbar für Reset-Feedback */}
|
||||
<Snackbar
|
||||
open={!!snackbar}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => setSnackbar('')}
|
||||
message={snackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user