OrganizationPage hinzugefügt, UsersPage fix
This commit is contained in:
@@ -8,6 +8,7 @@ import RegisterPage from './pages/RegisterPage';
|
|||||||
import UsersPage from './pages/UsersPage';
|
import UsersPage from './pages/UsersPage';
|
||||||
import PostsPage from './pages/PostsPage';
|
import PostsPage from './pages/PostsPage';
|
||||||
import CreateNewsPage from './pages/CreateNewsPage';
|
import CreateNewsPage from './pages/CreateNewsPage';
|
||||||
|
import OrganizationPage from './pages/OrganizationPage';
|
||||||
import PushPage from './pages/PushPage';
|
import PushPage from './pages/PushPage';
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -68,6 +69,14 @@ export default function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="organizations"
|
||||||
|
element={
|
||||||
|
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
|
||||||
|
<OrganizationPage />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="posts/create"
|
path="posts/create"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import PeopleIcon from '@mui/icons-material/People';
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
import ArticleIcon from '@mui/icons-material/Article';
|
import ArticleIcon from '@mui/icons-material/Article';
|
||||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||||
|
import CorporateFareIcon from '@mui/icons-material/CorporateFare';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
@@ -17,6 +18,7 @@ const navItems = [
|
|||||||
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
||||||
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
||||||
|
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
|
|||||||
201
src/pages/OrganizationPage.jsx
Normal file
201
src/pages/OrganizationPage.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ import { useEffect, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Box, Typography, Alert, IconButton, Tooltip,
|
Box, Typography, Alert, IconButton, Tooltip,
|
||||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||||
TextField, Button, CircularProgress, Stack, Divider,
|
TextField, Button, CircularProgress, Stack,
|
||||||
Snackbar,
|
Snackbar, Select, MenuItem, FormControl, InputLabel,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import LockResetIcon from '@mui/icons-material/LockReset';
|
import LockResetIcon from '@mui/icons-material/LockReset';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
import axiosInstance from '../api/axiosInstance';
|
import axiosInstance from '../api/axiosInstance';
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
@@ -20,8 +21,11 @@ export default function UsersPage() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
|
|
||||||
const [resetting, setResetting] = useState(null); // user id der gerade resettet wird
|
const [resetting, setResetting] = useState(null);
|
||||||
const [snackbar, setSnackbar] = useState(''); // Erfolgsmeldung
|
const [snackbar, setSnackbar] = useState('');
|
||||||
|
|
||||||
|
const [deleteUser, setDeleteUser] = useState(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axiosInstance.get('/api/users')
|
axiosInstance.get('/api/users')
|
||||||
@@ -35,7 +39,7 @@ export default function UsersPage() {
|
|||||||
setEditForm({
|
setEditForm({
|
||||||
email: user.email ?? '',
|
email: user.email ?? '',
|
||||||
nickname: user.nickname ?? '',
|
nickname: user.nickname ?? '',
|
||||||
role: user.role ?? '',
|
role: user.role?.replace('ROLE_', '') ?? '',
|
||||||
});
|
});
|
||||||
setSaveError('');
|
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 = [
|
const columns = [
|
||||||
{ field: 'id', headerName: 'ID', width: 80 },
|
{ field: 'id', headerName: 'ID', width: 80 },
|
||||||
{ field: 'nickname', headerName: 'Anzeigename', flex: 1 },
|
{ field: 'nickname', headerName: 'Anzeigename', flex: 1 },
|
||||||
@@ -91,7 +109,7 @@ export default function UsersPage() {
|
|||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
headerName: '',
|
headerName: '',
|
||||||
width: 100,
|
width: 130,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: ({ row }) => (
|
renderCell: ({ row }) => (
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
@@ -115,6 +133,11 @@ export default function UsersPage() {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Nutzer löschen">
|
||||||
|
<IconButton size="small" color="error" onClick={() => setDeleteUser(row)}>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -159,19 +182,19 @@ export default function UsersPage() {
|
|||||||
onChange={handleFormChange}
|
onChange={handleFormChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextField
|
<FormControl fullWidth>
|
||||||
label="Berechtigung"
|
<InputLabel>Berechtigung</InputLabel>
|
||||||
name="role"
|
<Select
|
||||||
select
|
name="role"
|
||||||
value={editForm.role ?? ''}
|
value={editForm.role ?? ''}
|
||||||
onChange={handleFormChange}
|
label="Berechtigung"
|
||||||
fullWidth
|
onChange={handleFormChange}
|
||||||
SelectProps={{ native: true }}
|
>
|
||||||
>
|
<MenuItem value="ADMIN">ADMIN</MenuItem>
|
||||||
<option value="ADMIN">ADMIN</option>
|
<MenuItem value="REPORTER">REPORTER</MenuItem>
|
||||||
<option value="REPORTER">REPORTER</option>
|
<MenuItem value="USER">USER</MenuItem>
|
||||||
<option value="USER">USER</option>
|
</Select>
|
||||||
</TextField>
|
</FormControl>
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||||
@@ -187,7 +210,29 @@ export default function UsersPage() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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
|
<Snackbar
|
||||||
open={!!snackbar}
|
open={!!snackbar}
|
||||||
autoHideDuration={5000}
|
autoHideDuration={5000}
|
||||||
|
|||||||
Reference in New Issue
Block a user