legal sachen, profil löschen etc

This commit is contained in:
2026-04-23 17:54:02 +02:00
parent 0b65dc5550
commit 5f5fcd2eef
11 changed files with 718 additions and 24 deletions

View File

@@ -18,6 +18,11 @@ import CompanyPage from "./pages/Company/CompanyPage.jsx";
import EditCompanyPage from "./pages/Company/EditCompanyPage.jsx";
import CalenderPostPage from "./pages/Calender/CalenderPostPage.jsx";
import ProfilePage from "./pages/Auth/ProfilePage.jsx";
import UnauthorizedPage from "./pages/Auth/UnauthorizedPage.jsx";
import DatenschutzPage from "./pages/Legal/DatenschutzPage.jsx";
import ImpressumPage from "./pages/Legal/ImpressumPage.jsx";
const theme = createTheme({
palette: {
@@ -51,7 +56,10 @@ export default function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/impressum" element={<ImpressumPage />} />
<Route path="/datenschutz" element={<DatenschutzPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} /> {/* ← neu */}
<Route
path="/"
element={
@@ -60,7 +68,14 @@ export default function App() {
</PrivateRoute>
}
>
<Route index element={<HomePage />} />
<Route
index
element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER', 'SITE_OWNER']}>
<HomePage />
</PrivateRoute>
}
/>
<Route
path="users"
element={

View File

@@ -13,7 +13,7 @@ axiosInstance.interceptors.response.use(
const isLogin = error.config?.url?.includes('/auth/login');
if (error.response?.status === 401 && !isAuthCheck && !isLogin) {
window.location.href = '/login';
window.dispatchEvent(new CustomEvent('auth:expired'));
}
return Promise.reject(error);
}

View File

@@ -1,7 +1,7 @@
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
import { useNavigate, useLocation, Outlet, Link as RouterLink } from 'react-router-dom';
import {
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip, IconButton
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip, IconButton, Link
} from '@mui/material';
import PeopleIcon from '@mui/icons-material/People';
import ArticleIcon from '@mui/icons-material/Article';
@@ -156,6 +156,14 @@ export default function Layout() {
</ListItemButton>
</ListItem>
</List>
<Box sx={{ px: 2, py: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
<Link component={RouterLink} to="/impressum" variant="caption" color="text.disabled" sx={{ mr: 2 }}>
Impressum
</Link>
<Link component={RouterLink} to="/datenschutz" variant="caption" color="text.disabled">
Datenschutz
</Link>
</Box>
</Drawer>
{/* Main content */}

View File

@@ -21,6 +21,15 @@ export function AuthProvider({ children }) {
.finally(() => setLoading(false));
}, []);
useEffect(() => {
const handleExpired = () => {
setIsAuthenticated(false);
setRole(null);
};
window.addEventListener('auth:expired', handleExpired);
return () => window.removeEventListener('auth:expired', handleExpired);
}, []);
const login = async (email, password) => {
const { data } = await axiosInstance.post('/auth/login', { email, password });
setRole(data.role.replace('ROLE_', ''));

View File

@@ -1,11 +1,16 @@
import { useState } from 'react';
import { useAuth } from '../../context/AuthContext';
import { useNavigate } from 'react-router-dom';
import {
Box, Card, CardContent, TextField, Button, Typography,
Alert, CircularProgress, Divider,
Alert, CircularProgress, Divider, DialogTitle, DialogContent, Stack, DialogActions, Dialog,
} from '@mui/material';
import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import axiosInstance from '../../api/axiosInstance';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from "@mui/icons-material/Add";
import {Info} from "@mui/icons-material";
export default function ProfilePage() {
@@ -21,6 +26,47 @@ export default function ProfilePage() {
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState('');
const [open, setOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deletingError, setDeletingError] = useState('');
const { logout } = useAuth();
const navigate = useNavigate();
const [deleteForm, setDeleteForm] = useState({
password: ''
});
const handleFormChange = (e) => {
const { name, value } = e.target;
setDeleteForm((prev) => ({ ...prev, [name]: value }));
};
const handleDelete = async () => {
setDeleting(true);
setDeletingError('');
try {
const jsonPayload = {
password: deleteForm.password};
await axiosInstance.post('/users/delete/me', jsonPayload);
handleDeleteClose();
await logout();
navigate('/login');
} catch (err) {
setDeletingError(err.response?.data?.error ?? 'Löschen fehlgeschlagen');
} finally {
setDeleting(false);
}
};
const handleDeleteClose = () => {
setOpen(false);
setDeleteForm({ password: ''});
setDeletingError('');
};
const handleNicknameSave = async (e) => {
e.preventDefault();
setNicknameError('');
@@ -151,6 +197,72 @@ export default function ProfilePage() {
</CardContent>
</Card>
{/* Konto Löschen Card */}
<Card sx={{ border: '1px solid', borderColor: 'error.light' }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<DeleteIcon color="error" />
<Typography variant="h6" fontWeight={600} color="error">
Konto löschen
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Dein Konto und alle zugehörigen Daten werden dauerhaft gelöscht.
Diese Aktion kann nicht rückgängig gemacht werden.
</Typography>
<Button
variant="contained"
color="error"
fullWidth
startIcon={<DeleteIcon />}
onClick={() => setOpen(true)}
>
Konto löschen
</Button>
</CardContent>
</Card>
{/* Konto Löschen Dialog */}
<Dialog open={open} onClose={handleDeleteClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Konto löschen</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1 }}>
{deletingError && <Alert severity="error">{deletingError}</Alert>}
<Alert severity="warning">
Das Löschen deines Kontos kann nicht rückgängig gemacht werden. Sobald du dein Konto löscht werden alle News etc. die zu dir gehören automatisch mit gelöscht. Unternehmen/Vereine, bei welchen du als Verwalter eingetragen bist bleiben bestehen, falls diese auch gelöscht werden sollen kannst du dies manuel vor der Löschung des Kontos machen oder bei einem Admin beauftragen.
</Alert>
<TextField
label="Password"
name="password"
type="password"
value={deleteForm.password}
onChange={handleFormChange}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={handleDeleteClose} disabled={deleting}>
Abbrechen
</Button>
<Button
variant="contained"
color="error"
onClick={handleDelete}
disabled={deleting}
startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : <DeleteIcon />}
>
Konto löschen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate, Link as RouterLink } from 'react-router-dom';
import {
Box, Card, CardContent, TextField, Button, Typography,
Alert, CircularProgress, Link, Divider,
Alert, CircularProgress, Link, Divider, FormControlLabel, Checkbox
} from '@mui/material';
import PersonAddOutlinedIcon from '@mui/icons-material/PersonAddOutlined';
import axiosInstance from '../../api/axiosInstance.js';
@@ -12,6 +12,7 @@ export default function RegisterPage() {
const [form, setForm] = useState({ email: '', nickname: '', password: '', confirmPassword: '' });
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [accepted, setAccepted] = useState(false);
const handleChange = (e) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
@@ -114,13 +115,32 @@ export default function RegisterPage() {
}
/>
<FormControlLabel
control={
<Checkbox
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
size="small"
/>
}
label={
<Typography variant="body2">
Ich habe die{' '}
<Link component={RouterLink} to="/datenschutz" underline="hover" target="_blank">
Datenschutzerklärung
</Link>
{' '}gelesen und stimme zu.
</Typography>
}
/>
<Button
type="submit"
variant="contained"
fullWidth
size="large"
disabled={loading}
sx={{ mt: 1 }}
type="submit"
variant="contained"
fullWidth
size="large"
disabled={loading || !accepted}
sx={{ mt: 1 }}
>
{loading ? <CircularProgress size={24} color="inherit" /> : 'Registrieren'}
</Button>

View File

@@ -0,0 +1,17 @@
import { Box, Typography, Button } from '@mui/material';
import { useNavigate } from 'react-router-dom';
export default function UnauthorizedPage() {
const navigate = useNavigate();
return (
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" minHeight="100vh">
<Typography variant="h4" gutterBottom>Kein Zugriff</Typography>
<Typography variant="body1" color="text.secondary" mb={3}>
Du hast keine Berechtigung, diese Seite zu sehen.
</Typography>
<Button variant="contained" onClick={() => navigate('/login')}>
Zurück zum Login
</Button>
</Box>
);
}

View File

@@ -11,12 +11,18 @@ import { Chip } from '@mui/material';
import axiosInstance from '../../api/axiosInstance.js';
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import {useNavigate} from "react-router-dom";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
export default function CompanyPage() {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [openChange, setOpenChange] = useState(false);
const [changing, setChanging] = useState(false);
const [changingId, setChangingId] = useState('');
const [changingError, setChangingError] = useState('');
const [userOptions, setUserOptions] = useState([]);
const [usersLoading, setUsersLoading] = useState(false);
@@ -30,6 +36,11 @@ export default function CompanyPage() {
email: ''
});
const [changeForm, setChangeForm] = useState({
email: ''
})
const loadCompanies = async () => {
try {
const { data } = await axiosInstance.get('/company');
@@ -45,6 +56,37 @@ export default function CompanyPage() {
loadCompanies();
}, []);
const handleOpenChange = (id) => {
setOpenChange(true);
setChangingId(id);
loadUsers();
}
const handleChangeClose = () => {
setOpenChange(false);
setChangeForm({ email: ''});
setChangingError('');
setChangingId('');
}
const handleChange = async () => {
setChanging(true);
setChangingError('');
try {
await axiosInstance.put(`/company/owner/${changingId}`, {
newOwner: changeForm.email
});
handleChangeClose();
await loadCompanies();
alert('Verwalter geändert');
} catch (err) {
setChangingError(err.response?.data?.error)
} finally {
setChanging(false);
}
}
const handleFormChange = (e) => {
const { name, value } = e.target;
setCreateForm((prev) => ({ ...prev, [name]: value }));
@@ -143,12 +185,17 @@ export default function CompanyPage() {
sortable: false,
renderCell: ({ row }) => (
<Tooltip title="">
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<Box>
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton title="Verwalter ändern" size="small" color="grey" onClick={() => handleOpenChange(row.id)}>
<ManageAccountsIcon fontSize="small" />
</IconButton>
</Box>
</Tooltip>
),
},
@@ -184,6 +231,66 @@ export default function CompanyPage() {
/>
</Box>
{/* Change Owner Dialog */}
<Dialog open={openChange} onClose={handleChangeClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Unternehmensverwalter ändern</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1}}>
{changingError && <Alert severity="error">{changingError}</Alert>}
<Autocomplete
options={userOptions}
loading={usersLoading}
value={changeForm.email}
onChange={(_, newValue) => {
setChangeForm(prev => ({ ...prev, email: newValue ?? '' }));
}}
onInputChange={(_, newInputValue) => {
setChangeForm(prev => ({ ...prev, email: newInputValue }));
}}
freeSolo
filterOptions={(options, { inputValue }) =>
options.filter(o => o.toLowerCase().includes(inputValue.toLowerCase()))
}
renderInput={(params) => (
<TextField
{...params}
label="Neue Verwalter-Email"
name="email"
type="email"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{usersLoading ? <CircularProgress size={16} /> : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={handleChangeClose} disabled={changing}>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleChange}
disabled={changing}
startIcon={changing ? <CircularProgress size={16} color="inherit" /> : null}
>
Verwalter ändern
</Button>
</DialogActions>
</Dialog>
{/* Create Dialog */}
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Unternehmen erstellen</DialogTitle>

View File

@@ -0,0 +1,184 @@
import { useNavigate } from 'react-router-dom';
import {
Box, Card, CardContent, Typography, Divider, Button,
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
function Section({ title, children }) {
return (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
{title}
</Typography>
{children}
</Box>
);
}
function Body({ children }) {
return (
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{children}
</Typography>
);
}
export default function DatenschutzPage() {
const navigate = useNavigate();
return (
<Box
sx={{
minHeight: '100vh',
bgcolor: 'grey.50',
display: 'flex',
justifyContent: 'center',
py: 6,
px: 2,
}}
>
<Card sx={{ width: '100%', maxWidth: 680, p: 1, alignSelf: 'flex-start' }}>
<CardContent>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 1 }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={() => navigate(-1)}
size="small"
sx={{ mr: 1 }}
>
Zurück
</Button>
<Typography variant="h5" fontWeight={600}>
Datenschutzerklärung
</Typography>
</Box>
<Typography variant="caption" color="text.disabled" display="block" sx={{ mb: 3 }}>
Gemäß DSGVO (EU) 2016/679 Stand: [Monat Jahr]
</Typography>
<Divider sx={{ mb: 3 }} />
{/* 1. Verantwortlicher */}
<Section title="1. Verantwortlicher">
<Body>
Verantwortlicher im Sinne der DSGVO ist:
</Body>
<Typography variant="body2" color="text.secondary">
[Name / Firma]<br />
[Straße, Hausnummer]<br />
[PLZ] [Ort]<br />
E-Mail: [kontakt@example.com]
</Typography>
</Section>
{/* 2. Erhobene Daten */}
<Section title="2. Welche Daten wir erheben">
<Body>
Bei der Registrierung und Nutzung des Admin-Panels erheben wir folgende personenbezogene Daten:
</Body>
<Typography variant="body2" color="text.secondary">
E-Mail-Adresse<br />
Nickname (Benutzername)<br />
Passwort (verschlüsselt gespeichert, nicht im Klartext)<br />
Zeitpunkt der Kontoerstellung<br />
Zugewiesene Benutzerrolle (z. B. ADMIN, REPORTER, SITE_OWNER)
</Typography>
</Section>
{/* 3. Zweck der Verarbeitung */}
<Section title="3. Zweck der Datenverarbeitung">
<Body>
Die erhobenen Daten werden ausschließlich für folgende Zwecke verarbeitet:
</Body>
<Typography variant="body2" color="text.secondary">
Authentifizierung und Zugangskontrolle zum Admin-Panel<br />
Verwaltung von Inhalten der DeineDorfApp (Neuigkeiten, Veranstaltungen, Sehenswürdigkeiten etc.)<br />
Versand von Push-Benachrichtigungen an App-Nutzer (nur für berechtigte Rollen)<br />
Nachvollziehbarkeit von Änderungen im System
</Typography>
</Section>
{/* 4. Rechtsgrundlage */}
<Section title="4. Rechtsgrundlage">
<Body>
Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO
(Vertragserfüllung bzw. vorvertragliche Maßnahmen) sowie Art. 6 Abs. 1 lit. a DSGVO
(Einwilligung), die bei der Registrierung erteilt wird.
</Body>
</Section>
{/* 5. Speicherdauer */}
<Section title="5. Speicherdauer">
<Body>
Personenbezogene Daten werden so lange gespeichert, wie der Account aktiv ist oder
dies für die Erfüllung der genannten Zwecke erforderlich ist. Nach Löschung des
Accounts werden die Daten gelöscht, sofern keine gesetzlichen Aufbewahrungspflichten
entgegenstehen.
</Body>
</Section>
{/* 6. Weitergabe an Dritte */}
<Section title="6. Weitergabe an Dritte">
<Body>
Eine Weitergabe der Daten an Dritte erfolgt nicht, außer es ist zur Erbringung
des Dienstes notwendig. Folgende Drittanbieter werden eingesetzt:
</Body>
<Typography variant="body2" color="text.secondary">
<strong>Firebase (Google)</strong> Push-Benachrichtigungen (FCM). Datenschutzerklärung:
policies.google.com/privacy<br />
<strong>[ggf. weiterer Dienst]</strong> [Zweck]
</Typography>
</Section>
{/* 7. Hosting */}
<Section title="7. Hosting">
<Body>
Die Anwendung wird auf eigenen Servern betrieben, gehostet mittels Docker.
Der Serverstandort befindet sich in [Land / Rechenzentrum].
Es findet keine Übermittlung in Drittstaaten außerhalb der EU statt,
sofern nicht oben anders angegeben.
</Body>
</Section>
{/* 8. Betroffenenrechte */}
<Section title="8. Ihre Rechte">
<Body>
Sie haben gegenüber uns folgende Rechte hinsichtlich Ihrer personenbezogenen Daten:
</Body>
<Typography variant="body2" color="text.secondary">
<strong>Auskunft</strong> (Art. 15 DSGVO)<br />
<strong>Berichtigung</strong> (Art. 16 DSGVO)<br />
<strong>Löschung</strong> (Art. 17 DSGVO)<br />
<strong>Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)<br />
<strong>Widerspruch</strong> (Art. 21 DSGVO)<br />
<strong>Datenübertragbarkeit</strong> (Art. 20 DSGVO)
</Typography>
<Body>
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: [kontakt@example.com]
</Body>
</Section>
{/* 9. Beschwerderecht */}
<Section title="9. Beschwerderecht">
<Body>
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehörde über die
Verarbeitung Ihrer personenbezogenen Daten zu beschweren. Die zuständige
Behörde richtet sich nach Ihrem Wohnsitz oder dem Sitz unseres Unternehmens.
</Body>
</Section>
<Divider sx={{ mb: 2 }} />
<Typography variant="caption" color="text.disabled">
Alle Platzhalter in eckigen Klammern müssen vor Inbetriebnahme durch korrekte Angaben ersetzt werden.
Im Zweifel einen Rechtsanwalt oder Datenschutzbeauftragten hinzuziehen.
</Typography>
</CardContent>
</Card>
</Box>
);
}

View File

@@ -0,0 +1,118 @@
import { useNavigate } from 'react-router-dom';
import {
Box, Card, CardContent, Typography, Divider, Button,
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
function Section({ title, children }) {
return (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
{title}
</Typography>
{children}
</Box>
);
}
function Line({ label, value }) {
return (
<Typography variant="body2" color="text.secondary">
{label && <strong>{label}: </strong>}
{value}
</Typography>
);
}
export default function ImpressumPage() {
const navigate = useNavigate();
return (
<Box
sx={{
minHeight: '100vh',
bgcolor: 'grey.50',
display: 'flex',
justifyContent: 'center',
py: 6,
px: 2,
}}
>
<Card sx={{ width: '100%', maxWidth: 680, p: 1, alignSelf: 'flex-start' }}>
<CardContent>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 1 }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={() => navigate(-1)}
size="small"
sx={{ mr: 1 }}
>
Zurück
</Button>
<Typography variant="h5" fontWeight={600}>
Impressum
</Typography>
</Box>
<Typography variant="caption" color="text.disabled" display="block" sx={{ mb: 3 }}>
Angaben gemäß § 5 TMG
</Typography>
<Divider sx={{ mb: 3 }} />
{/* Anbieter */}
<Section title="Anbieter">
<Line value="[Vollständiger Name oder Firmenname]" />
<Line value="[Straße und Hausnummer]" />
<Line value="[PLZ] [Ort]" />
<Line value="[Land]" />
</Section>
{/* Kontakt */}
<Section title="Kontakt">
<Line label="Telefon" value="[+49 000 000000]" />
<Line label="E-Mail" value="[kontakt@example.com]" />
</Section>
{/* Vertreten durch */}
<Section title="Vertreten durch">
<Line value="[Vor- und Nachname des Vertreters / Geschäftsführers]" />
</Section>
{/* Registereintrag nur ausfüllen wenn vorhanden */}
<Section title="Registereintrag (falls vorhanden)">
<Line label="Registergericht" value="[Amtsgericht ...]" />
<Line label="Registernummer" value="[HRB / HRA ...]" />
</Section>
{/* Umsatzsteuer-ID nur wenn vorhanden */}
<Section title="Umsatzsteuer-ID (falls vorhanden)">
<Line
value="Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG: [DE000000000]"
/>
</Section>
<Divider sx={{ mb: 3 }} />
{/* Haftungsausschluss */}
<Section title="Haftungsausschluss">
<Typography variant="body2" color="text.secondary">
Die Inhalte dieser Anwendung wurden mit größtmöglicher Sorgfalt erstellt.
Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte übernehmen
wir jedoch keine Gewähr. Als Diensteanbieter sind wir gemäß § 7 Abs. 1 TMG
für eigene Inhalte nach den allgemeinen Gesetzen verantwortlich.
</Typography>
</Section>
{/* Stand */}
<Typography variant="caption" color="text.disabled">
Stand: [Monat Jahr] Bitte alle Platzhalter in eckigen Klammern ersetzen.
</Typography>
</CardContent>
</Card>
</Box>
);
}

View File

@@ -9,6 +9,7 @@ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import AddIcon from '@mui/icons-material/Add';
import { Chip } from '@mui/material';
import axiosInstance from '../../api/axiosInstance.js';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import {useNavigate} from "react-router-dom";
@@ -20,6 +21,11 @@ export default function OrganizationPage() {
const [userOptions, setUserOptions] = useState([]);
const [usersLoading, setUsersLoading] = useState(false);
const [openChange, setOpenChange] = useState(false);
const [changing, setChanging] = useState(false);
const [changingId, setChangingId] = useState('');
const [changingError, setChangingError] = useState('');
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [creatingError, setCreatingError] = useState('');
@@ -30,6 +36,10 @@ export default function OrganizationPage() {
email: ''
});
const [changeForm, setChangeForm] = useState({
email: ''
})
const loadOrganizations = async () => {
try {
const { data } = await axiosInstance.get('/organization');
@@ -46,6 +56,12 @@ export default function OrganizationPage() {
loadUsers();
}
const handleOpenChange = (id) => {
setOpenChange(true);
setChangingId(id);
loadUsers();
}
useEffect(() => {
loadOrganizations();
}, []);
@@ -96,6 +112,30 @@ export default function OrganizationPage() {
setCreatingError('');
};
const handleChangeClose = () => {
setOpenChange(false);
setChangeForm({ email: ''});
setChangingError('');
setChangingId('');
}
const handleChange = async () => {
setChanging(true);
setChangingError('');
try {
await axiosInstance.put(`/organization/owner/${changingId}`, {
newOwner: changeForm.email
});
handleChangeClose();
await loadOrganizations();
alert('Verwalter geändert');
} catch (err) {
setChangingError(err.response?.data?.error)
} finally {
setChanging(false);
}
}
const handleDelete = async (id) => {
if (!window.confirm('Organisation wirklich löschen?')) return;
@@ -144,12 +184,17 @@ export default function OrganizationPage() {
sortable: false,
renderCell: ({ row }) => (
<Tooltip title="">
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<Box>
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton title="Verwalter ändern" size="small" color="grey" onClick={() => handleOpenChange(row.id)}>
<ManageAccountsIcon fontSize="small" />
</IconButton>
</Box>
</Tooltip>
),
},
@@ -185,6 +230,65 @@ export default function OrganizationPage() {
/>
</Box>
{/* Change Owner Dialog */}
<Dialog open={openChange} onClose={handleChangeClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Organisationsverwalter ändern</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1}}>
{changingError && <Alert severity="error">{changingError}</Alert>}
<Autocomplete
options={userOptions}
loading={usersLoading}
value={changeForm.email}
onChange={(_, newValue) => {
setChangeForm(prev => ({ ...prev, email: newValue ?? '' }));
}}
onInputChange={(_, newInputValue) => {
setChangeForm(prev => ({ ...prev, email: newInputValue }));
}}
freeSolo
filterOptions={(options, { inputValue }) =>
options.filter(o => o.toLowerCase().includes(inputValue.toLowerCase()))
}
renderInput={(params) => (
<TextField
{...params}
label="Neue Verwalter-Email"
name="email"
type="email"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{usersLoading ? <CircularProgress size={16} /> : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={handleChangeClose} disabled={changing}>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleChange}
disabled={changing}
startIcon={changing ? <CircularProgress size={16} color="inherit" /> : null}
>
Verwalter ändern
</Button>
</DialogActions>
</Dialog>
{/* Create Dialog */}
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600}}>Organisation erstellen</DialogTitle>