OrganizationPage hinzugefügt, UsersPage fix

This commit is contained in:
2026-04-07 16:30:04 +02:00
parent c8e4f3b5ec
commit 8eb59cda2b
4 changed files with 277 additions and 20 deletions

View File

@@ -8,6 +8,7 @@ import RegisterPage from './pages/RegisterPage';
import UsersPage from './pages/UsersPage';
import PostsPage from './pages/PostsPage';
import CreateNewsPage from './pages/CreateNewsPage';
import OrganizationPage from './pages/OrganizationPage';
import PushPage from './pages/PushPage';
const theme = createTheme({
@@ -68,6 +69,14 @@ export default function App() {
</PrivateRoute>
}
/>
<Route
path="organizations"
element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<OrganizationPage />
</PrivateRoute>
}
/>
<Route
path="posts/create"
element={

View File

@@ -7,6 +7,7 @@ import {
import PeopleIcon from '@mui/icons-material/People';
import ArticleIcon from '@mui/icons-material/Article';
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
import CorporateFareIcon from '@mui/icons-material/CorporateFare';
import LogoutIcon from '@mui/icons-material/Logout';
import DashboardIcon from '@mui/icons-material/Dashboard';
import { useAuth } from '../context/AuthContext';
@@ -17,6 +18,7 @@ const navItems = [
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'REPORTER'] },
];
export default function Layout() {

View File

@@ -0,0 +1,201 @@
import { useEffect, useState } from 'react';
import {
Box, Typography, Alert, IconButton, Tooltip,
Dialog, DialogTitle, DialogContent, DialogActions,
Button, Stack, TextField, CircularProgress
} from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import AddIcon from '@mui/icons-material/Add';
import { Chip } from '@mui/material';
import axiosInstance from '../api/axiosInstance';
export default function OrganizationPage() {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [creatingError, setCreatingError] = useState('');
const [createForm, setCreateForm] = useState({
name: '',
email: ''
});
const loadOrganizations = async () => {
try {
const { data } = await axiosInstance.get('/api/organization');
setRows(data);
} catch {
setError('Fehler beim Laden der Organisationen');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadOrganizations();
}, []);
const handleFormChange = (e) => {
const { name, value } = e.target;
setCreateForm((prev) => ({ ...prev, [name]: value }));
};
const handleCreate = async () => {
setCreating(true);
setCreatingError('');
try {
const jsonPayload = {
name: createForm.name,
ownerEmail: createForm.email
};
await axiosInstance.post('/api/organization', jsonPayload);
await loadOrganizations(); // Liste neu laden
handleCreateClose();
} catch (err) {
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen');
} finally {
setCreating(false);
}
};
const handleCreateClose = () => {
setOpen(false);
setCreateForm({ name: '', email: '' });
setCreatingError('');
};
const handleDelete = async (id) => {
if (!window.confirm('Organisation wirklich löschen?')) return;
try {
await axiosInstance.delete(`/api/organization/${id}`);
setRows((prev) => prev.filter((r) => r.id !== id));
} catch {
alert('Löschen fehlgeschlagen');
}
};
const columns = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: 'Name', flex: 1.5 },
{
field: 'ownerEmail',
headerName: 'Verwalter',
flex: 2,
renderCell: ({ value }) => (
<Box sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'text.secondary', fontSize: 13 }}>
{value}
</Box>
),
},
{
field: 'active',
headerName: 'Status',
width: 150,
renderCell: ({ value }) => (
<Chip
label={value ? 'Aktiv' : 'Inaktiv'}
color={value ? 'success' : 'default'}
size="small"
variant={value ? 'filled' : 'outlined'}
/>
),
},
{
field: 'actions',
headerName: '',
width: 60,
sortable: false,
renderCell: ({ row }) => (
<Tooltip title="Löschen">
<IconButton size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
),
},
];
return (
<Box>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h5" fontWeight={600}>Organisationen</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setOpen(true)}
>
Organisation erstellen
</Button>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{/* Tabelle */}
<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>
{/* Create Dialog */}
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Organisation erstellen</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1 }}>
{creatingError && <Alert severity="error">{creatingError}</Alert>}
<TextField
label="Name"
name="name"
value={createForm.name}
onChange={handleFormChange}
fullWidth
/>
<TextField
label="Verwalter Email"
name="email"
type="email"
value={createForm.email}
onChange={handleFormChange}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={handleCreateClose} disabled={creating}>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleCreate}
disabled={creating}
startIcon={creating ? <CircularProgress size={16} color="inherit" /> : null}
>
Erstellen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}

View File

@@ -2,12 +2,13 @@ import { useEffect, useState } from 'react';
import {
Box, Typography, Alert, IconButton, Tooltip,
Dialog, DialogTitle, DialogContent, DialogActions,
TextField, Button, CircularProgress, Stack, Divider,
Snackbar,
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() {
@@ -20,8 +21,11 @@ export default function UsersPage() {
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
const [resetting, setResetting] = useState(null);
const [snackbar, setSnackbar] = useState('');
const [deleteUser, setDeleteUser] = useState(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
axiosInstance.get('/api/users')
@@ -35,7 +39,7 @@ export default function UsersPage() {
setEditForm({
email: user.email ?? '',
nickname: user.nickname ?? '',
role: user.role ?? '',
role: user.role?.replace('ROLE_', '') ?? '',
});
setSaveError('');
};
@@ -77,6 +81,20 @@ export default function UsersPage() {
}
};
const handleDeleteConfirm = async () => {
setDeleting(true);
try {
await axiosInstance.delete(`/api/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 },
@@ -91,7 +109,7 @@ export default function UsersPage() {
{
field: 'actions',
headerName: '',
width: 100,
width: 130,
sortable: false,
renderCell: ({ row }) => (
<Box sx={{ display: 'flex', gap: 0.5 }}>
@@ -115,6 +133,11 @@ export default function UsersPage() {
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nutzer löschen">
<IconButton size="small" color="error" onClick={() => setDeleteUser(row)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
),
},
@@ -159,19 +182,19 @@ export default function UsersPage() {
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>
<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>
</Select>
</FormControl>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
@@ -187,7 +210,29 @@ export default function UsersPage() {
</DialogActions>
</Dialog>
{/* Snackbar für Reset-Feedback */}
{/* 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}