stabile version

This commit is contained in:
2026-04-15 15:17:11 +02:00
parent bd10bdc3ed
commit dd1d78e6ad
12 changed files with 248 additions and 167 deletions

View File

@@ -1,4 +1,4 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import {BrowserRouter, Routes, Route} from 'react-router-dom';
import { createTheme, ThemeProvider, CssBaseline } from '@mui/material'; import { createTheme, ThemeProvider, CssBaseline } from '@mui/material';
import { AuthProvider } from './context/AuthContext'; import { AuthProvider } from './context/AuthContext';
import PrivateRoute from './components/PrivateRoute'; import PrivateRoute from './components/PrivateRoute';
@@ -19,139 +19,139 @@ import EditCompanyPage from "./pages/Company/EditCompanyPage.jsx";
import CalenderPostPage from "./pages/Calender/CalenderPostPage.jsx"; import CalenderPostPage from "./pages/Calender/CalenderPostPage.jsx";
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
mode: 'light', mode: 'light',
primary: { main: '#1976d2' }, primary: { main: '#1976d2' },
background: { default: '#f5f5f5' }, background: { default: '#f5f5f5' },
},
shape: { borderRadius: 8 },
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
},
components: {
MuiButton: {
styleOverrides: {
root: { textTransform: 'none', fontWeight: 500 },
},
}, },
MuiCard: { shape: { borderRadius: 8 },
styleOverrides: { typography: {
root: { boxShadow: '0 1px 3px rgba(0,0,0,0.08)', border: '1px solid #e0e0e0' }, fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
}, },
components: {
MuiButton: {
styleOverrides: {
root: { textTransform: 'none', fontWeight: 500 },
},
},
MuiCard: {
styleOverrides: {
root: { boxShadow: '0 1px 3px rgba(0,0,0,0.08)', border: '1px solid #e0e0e0' },
},
},
}, },
},
}); });
export default function App() { export default function App() {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<AuthProvider> <AuthProvider>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/register" element={<RegisterPage />} />
<Route <Route
path="/" path="/"
element={ element={
<PrivateRoute> <PrivateRoute>
<Layout /> <Layout />
</PrivateRoute> </PrivateRoute>
} }
> >
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
<Route <Route
path="users" path="users"
element={ element={
<PrivateRoute allowedRoles={['ADMIN']}> <PrivateRoute allowedRoles={['ADMIN']}>
<UsersPage /> <UsersPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="attractions" path="attractions"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}> <PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<AttractionPage /> <AttractionPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="calenderPosts" path="calenderPosts"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}> <PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<CalenderPostPage /> <CalenderPostPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="posts" path="posts"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}> <PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<PostsPage /> <PostsPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="companies" path="companies"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}> <PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
<CompanyPage /> <CompanyPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="attractions/:id/edit" path="attractions/:id/edit"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}> <PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<EditAttractionPage/> <EditAttractionPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="companies/:id/edit" path="companies/:id/edit"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}> <PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
<EditCompanyPage/> <EditCompanyPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="organizations/:id/edit" path="organizations/:id/edit"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}> <PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
<EditOrganizationPage/> <EditOrganizationPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="organizations" path="organizations"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}> <PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
<OrganizationPage /> <OrganizationPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="posts/create" path="posts/create"
element={ element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}> <PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<CreateNewsPage /> <CreateNewsPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="push" path="push"
element={ element={
<PrivateRoute allowedRoles={['ADMIN']}> <PrivateRoute allowedRoles={['ADMIN']}>
<PushPage /> <PushPage />
</PrivateRoute> </PrivateRoute>
} }
/> />
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -0,0 +1,29 @@
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 32 }}>
<h2>Etwas ist schiefgelaufen.</h2>
<p>{this.state.error?.message || 'Unbekannter Fehler'}</p>
<button onClick={() => this.setState({ hasError: false })}>
Erneut versuchen
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -14,6 +14,7 @@ import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import HomeIcon from '@mui/icons-material/Home'; import HomeIcon from '@mui/icons-material/Home';
import StoreIcon from '@mui/icons-material/Store'; import StoreIcon from '@mui/icons-material/Store';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import ErrorBoundary from './ErrorBoundary';
const DRAWER_WIDTH = 240; const DRAWER_WIDTH = 240;
@@ -34,9 +35,9 @@ const navCategories = [
{ {
label: 'Reporter', label: 'Reporter',
items: [ items: [
{ label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] }, { label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
{ label: 'Kalendereinträge', path: '/calenderPosts', icon: <CalendarMonthIcon />, roles: ['ADMIN', 'REPORTER'] }, { label: 'Kalendereinträge', path: '/calenderPosts', icon: <CalendarMonthIcon />, roles: ['ADMIN', 'REPORTER'] },
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] }, { label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
], ],
}, },
{ {
@@ -173,8 +174,11 @@ export default function Layout() {
</Tooltip> </Tooltip>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Outlet /> <ErrorBoundary>
<Outlet />
</ErrorBoundary>
</Box> </Box>
</Box> </Box>
</Box> </Box>

View File

@@ -31,7 +31,7 @@ export default function AttractionPage() {
const { data } = await axiosInstance.get('/attraction'); const { data } = await axiosInstance.get('/attraction');
setRows(data); setRows(data);
} catch (err) { } catch (err) {
setError(err.response?.data?.message ?? 'Fehler beim laden der Attractions'); setError(err.response?.data?.error ?? 'Fehler beim laden der Attractions');
} finally { } finally {
setLoading(false); setLoading(false);
@@ -61,7 +61,7 @@ export default function AttractionPage() {
handleCreateClose(); handleCreateClose();
} catch (err) { } catch (err) {
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen'); setCreatingError(err.response?.data?.error ?? 'Erstellen fehlgeschlagen');
} finally { } finally {
setCreating(false); setCreating(false);
} }
@@ -80,7 +80,7 @@ export default function AttractionPage() {
await axiosInstance.delete(`/attraction/${id}`); await axiosInstance.delete(`/attraction/${id}`);
setRows((prev) => prev.filter((r) => r.id !== id)); setRows((prev) => prev.filter((r) => r.id !== id));
} catch(err){ } catch(err){
alert(err.response?.data?.message ?? 'Löschen fehlgeschlagen'); alert(err.response?.data?.error ?? 'Löschen fehlgeschlagen');
} }
}; };

View File

@@ -60,7 +60,8 @@ export default function EditAttractionPage() {
setTitleImageUrl(imgs[0] || null); setTitleImageUrl(imgs[0] || null);
setOtherImageUrls(imgs.slice(1)); setOtherImageUrls(imgs.slice(1));
} catch (err) { } catch (err) {
setError(err.response?.data?.message ?? 'Fehler beim Laden der Sehenswürdigkeit'); const msg = err.response?.data?.error;
setError(typeof msg === 'string' ? msg : 'Fehler beim Laden der Sehenswürdigkeit');
} }
}; };
fetchData(); fetchData();
@@ -135,7 +136,8 @@ export default function EditAttractionPage() {
}); });
navigate('/attractions'); navigate('/attractions');
} catch (err) { } catch (err) {
setError(err.response?.data?.message ?? 'Fehler beim Speichern'); const msg = err.response?.data?.error;
setError(typeof msg === 'string' ? msg : 'Fehler beim Speichern');
setSaving(false); setSaving(false);
} }
}; };
@@ -155,8 +157,11 @@ export default function EditAttractionPage() {
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSubmit}> <Box component="form" onSubmit={handleSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
{error && <Alert severity="error">{error}</Alert>} {error && (
<Alert severity="error">
{typeof error === 'string' ? error : JSON.stringify(error)}
</Alert>
)}
<TextField <TextField
label="Titel" label="Titel"
value={title} value={title}

View File

@@ -63,7 +63,7 @@ export default function UsersPage() {
setSnackbar(`Änderungen an ${editUser.nickname} erfolgreich gespeichert.`); setSnackbar(`Änderungen an ${editUser.nickname} erfolgreich gespeichert.`);
handleEditClose(); handleEditClose();
} catch (err) { } catch (err) {
setSaveError(err.response?.data?.message ?? 'Speichern fehlgeschlagen'); setSaveError(err.response?.data?.error ?? 'Speichern fehlgeschlagen');
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -75,7 +75,7 @@ export default function UsersPage() {
await axiosInstance.post(`/users/${user.id}/reset-password`); await axiosInstance.post(`/users/${user.id}/reset-password`);
setSnackbar(`Passwort für ${user.nickname} wurde zurückgesetzt — E-Mail wurde versendet.`); setSnackbar(`Passwort für ${user.nickname} wurde zurückgesetzt — E-Mail wurde versendet.`);
} catch (err) { } catch (err) {
setSnackbar(err.response?.data?.message ?? 'Zurücksetzen fehlgeschlagen'); setSnackbar(err.response?.data?.error ?? 'Zurücksetzen fehlgeschlagen');
} finally { } finally {
setResetting(null); setResetting(null);
} }
@@ -89,7 +89,7 @@ export default function UsersPage() {
setSnackbar(`Nutzer ${deleteUser.nickname} wurde gelöscht.`); setSnackbar(`Nutzer ${deleteUser.nickname} wurde gelöscht.`);
setDeleteUser(null); setDeleteUser(null);
} catch (err) { } catch (err) {
setSnackbar(err.response?.data?.message ?? 'Löschen fehlgeschlagen'); setSnackbar(err.response?.data?.error ?? 'Löschen fehlgeschlagen');
} finally { } finally {
setDeleting(false); setDeleting(false);
} }

View File

@@ -63,7 +63,7 @@ export default function CompanyPage() {
handleCreateClose(); handleCreateClose();
} catch (err) { } catch (err) {
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen'); setCreatingError(err.response?.data?.error ?? 'Erstellen fehlgeschlagen');
} finally { } finally {
setCreating(false); setCreating(false);
} }
@@ -82,7 +82,7 @@ export default function CompanyPage() {
await axiosInstance.delete(`/company/${id}`); await axiosInstance.delete(`/company/${id}`);
setRows((prev) => prev.filter((r) => r.id !== id)); setRows((prev) => prev.filter((r) => r.id !== id));
} catch (err){ } catch (err){
alert(err.response?.data?.message ?? 'Löschen fehlgeschlagen'); alert(err.response?.data?.error ?? 'Löschen fehlgeschlagen');
} }
}; };

View File

@@ -70,7 +70,9 @@ export default function EditCompanyPage() {
setTitleImageUrl(imgs[0] || null); setTitleImageUrl(imgs[0] || null);
setOtherImageUrls(imgs.slice(1)); setOtherImageUrls(imgs.slice(1));
} catch (err) { } catch (err) {
setError(err.response?.data || 'Fehler beim Laden'); // ✅ err.response.data kann ein Objekt sein → .error extrahieren
const serverMessage = err.response?.data?.error || err.response?.data;
setError(typeof serverMessage === 'string' ? serverMessage : 'Fehler beim Laden');
} }
}; };
fetchData(); fetchData();
@@ -143,9 +145,15 @@ export default function EditCompanyPage() {
await axiosInstance.put(`/company/${id}`, formData, { await axiosInstance.put(`/company/${id}`, formData, {
headers: { 'Content-Type': undefined } headers: { 'Content-Type': undefined }
}); });
navigate('/companies'); navigate('/companies');
} catch (err) { } catch (err) {
setError(err.response?.data || 'Fehler beim Speichern'); // ✅ Fehlermeldung korrekt aus Axios-Response extrahieren
const serverMessage = err.response?.data?.error || err.response?.data;
const message = typeof serverMessage === 'string'
? serverMessage
: err.message || 'Fehler beim Speichern';
setError(message);
setSaving(false); setSaving(false);
} }
}; };
@@ -165,8 +173,11 @@ export default function EditCompanyPage() {
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSubmit}> <Box component="form" onSubmit={handleSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
{error && <Alert severity="error">{error}</Alert>} {error && (
<Alert severity="error">
{typeof error === 'string' ? error : JSON.stringify(error)}
</Alert>
)}
<TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." /> <TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." />
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} /> <TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} />
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth /> <TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
@@ -224,7 +235,6 @@ export default function EditCompanyPage() {
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{titleImageFile ? titleImageFile.name : titleImageUrl} {titleImageFile ? titleImageFile.name : titleImageUrl}
</Typography> </Typography>
{/* Titelbild tauschen ohne zu löschen */}
<Button <Button
component="label" component="label"
size="small" size="small"
@@ -261,7 +271,6 @@ export default function EditCompanyPage() {
</Typography> </Typography>
<Stack spacing={1.5}> <Stack spacing={1.5}>
{/* Bestehende weitere Bilder */}
{otherImageUrls.map((src, i) => ( {otherImageUrls.map((src, i) => (
<Box <Box
key={`existing-${i}`} key={`existing-${i}`}
@@ -287,7 +296,6 @@ export default function EditCompanyPage() {
</Box> </Box>
))} ))}
{/* Neu hinzugefügte weitere Bilder */}
{otherPreviews.map((src, i) => ( {otherPreviews.map((src, i) => (
<Box <Box
key={`new-${i}`} key={`new-${i}`}

View File

@@ -69,7 +69,7 @@ export default function CreateNewsPage() {
}); });
navigate('/posts'); navigate('/posts');
} catch (err) { } catch (err) {
setError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen'); setError(err.response?.data?.error ?? 'Erstellen fehlgeschlagen');
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -90,8 +90,11 @@ export default function CreateNewsPage() {
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSubmit}> <Box component="form" onSubmit={handleSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
{error && <Alert severity="error">{error}</Alert>} {error && (
<Alert severity="error">
{typeof error === 'string' ? error : JSON.stringify(error)}
</Alert>
)}
<TextField label="Titel" name="title" value={form.title} onChange={handleChange} required fullWidth autoFocus /> <TextField label="Titel" name="title" value={form.title} onChange={handleChange} required fullWidth autoFocus />
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth multiline rows={6} /> <TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth multiline rows={6} />
<TextField label="Kategorie" name="category" value={form.category} onChange={handleChange} required fullWidth /> <TextField label="Kategorie" name="category" value={form.category} onChange={handleChange} required fullWidth />
@@ -166,6 +169,16 @@ export default function CreateNewsPage() {
{/* Push Notification */} {/* Push Notification */}
<Box> <Box>
<Typography variant="subtitle2" fontWeight={500} mb={1.5}>Push Notification</Typography> <Typography variant="subtitle2" fontWeight={500} mb={1.5}>Push Notification</Typography>
<Alert
severity="info"
variant="outlined"
sx={{ py: 0.5, mb: 2, fontSize: 13 }}
>
Wird gebündelt nach ~30 min gesendet. Kein Versand bei Ruhezeiten (228 Uhr),
wenn in den letzten 90 min bereits gesendet wurde oder das Tageslimit (3) erreicht ist.
</Alert>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<ToggleButtonGroup <ToggleButtonGroup
exclusive exclusive

View File

@@ -23,7 +23,7 @@ export default function PushPage() {
setTitle(''); setTitle('');
setBody(''); setBody('');
} catch (err) { } catch (err) {
setResult({ type: 'error', msg: err.response?.data?.message ?? 'Senden fehlgeschlagen' }); setResult({ type: 'error', msg: err.response?.data?.error ?? 'Senden fehlgeschlagen' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -31,7 +31,19 @@ export default function PushPage() {
return ( return (
<Box> <Box>
<Typography variant="h5" fontWeight={600} mb={3}>Push Notifications</Typography> <Typography variant="h5" fontWeight={600} mb={3}>Push Benachrichtigung</Typography>
<Alert severity="info" sx={{ maxWidth: 560, mb: 3 }}>
Nachrichten werden über <strong>Firebase Cloud Messaging (FCM)</strong> versendet
und landen zunächst in einer Queue. Der Worker verarbeitet sie anschließend
asynchron Zustellung kann einige Sekunden dauern. iOS- und Android-Geräte
müssen Benachrichtigungen in der App-Einstellung erlaubt haben.
Benachrichtigung werden nicht verschickt, wenn eines der folgenden Punkte zutrifft:
- Benachrichtigung wird innerhalb der Ruhezeiten verschickt (22:00-8:00)
- In den letzten 90 Minuten wurde bereits an diese Platform eine Benachrichtigung geschickt
- Heute wurden bereits 3 Benachrichtigung an diese Platform geschickt
</Alert>
<Card sx={{ maxWidth: 560 }}> <Card sx={{ maxWidth: 560 }}>
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSend} sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> <Box component="form" onSubmit={handleSend} sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>

View File

@@ -70,7 +70,7 @@ export default function EditOrganizationPage() {
setTitleImageUrl(imgs[0] || null); setTitleImageUrl(imgs[0] || null);
setOtherImageUrls(imgs.slice(1)); setOtherImageUrls(imgs.slice(1));
} catch (err) { } catch (err) {
setError(err.response?.data || 'Fehler beim Laden'); setError(err.response?.data?.error || err.response?.data || 'Fehler beim Laden');
} }
}; };
fetchData(); fetchData();
@@ -140,12 +140,22 @@ export default function EditOrganizationPage() {
if (titleImageFile) formData.append('images', titleImageFile); if (titleImageFile) formData.append('images', titleImageFile);
otherFiles.forEach((f) => formData.append('images', f)); otherFiles.forEach((f) => formData.append('images', f));
// ✅ await hinzugefügt — ohne das wird die Response nie abgewartet
await axiosInstance.put(`/organization/${id}`, formData, { await axiosInstance.put(`/organization/${id}`, formData, {
headers: { 'Content-Type': undefined } headers: { 'Content-Type': undefined }
}); });
// Axios wirft bei 4xx/5xx automatisch eine Exception,
// hier kommen wir nur bei Erfolg an
navigate('/organizations'); navigate('/organizations');
} catch (err) { } catch (err) {
setError(err.response?.data || 'Fehler beim Speichern'); // Axios-Fehlerstruktur: err.response.data enthält den Body
const serverMessage = err.response?.data?.error || err.response?.data;
const message =
typeof serverMessage === 'string'
? serverMessage
: err.message || 'Fehler beim Speichern';
setError(message);
setSaving(false); setSaving(false);
} }
}; };
@@ -165,8 +175,11 @@ export default function EditOrganizationPage() {
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSubmit}> <Box component="form" onSubmit={handleSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
{error && <Alert severity="error">{error}</Alert>} {error && (
<Alert severity="error">
{typeof error === 'string' ? error : JSON.stringify(error)}
</Alert>
)}
<TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." /> <TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." />
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} /> <TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} />
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth /> <TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
@@ -224,7 +237,6 @@ export default function EditOrganizationPage() {
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{titleImageFile ? titleImageFile.name : titleImageUrl} {titleImageFile ? titleImageFile.name : titleImageUrl}
</Typography> </Typography>
{/* Titelbild tauschen ohne zu löschen */}
<Button <Button
component="label" component="label"
size="small" size="small"
@@ -261,7 +273,6 @@ export default function EditOrganizationPage() {
</Typography> </Typography>
<Stack spacing={1.5}> <Stack spacing={1.5}>
{/* Bestehende weitere Bilder */}
{otherImageUrls.map((src, i) => ( {otherImageUrls.map((src, i) => (
<Box <Box
key={`existing-${i}`} key={`existing-${i}`}
@@ -287,7 +298,6 @@ export default function EditOrganizationPage() {
</Box> </Box>
))} ))}
{/* Neu hinzugefügte weitere Bilder */}
{otherPreviews.map((src, i) => ( {otherPreviews.map((src, i) => (
<Box <Box
key={`new-${i}`} key={`new-${i}`}

View File

@@ -63,7 +63,7 @@ export default function OrganizationPage() {
handleCreateClose(); handleCreateClose();
} catch (err) { } catch (err) {
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen'); setCreatingError(err.response?.data?.error ?? 'Erstellen fehlgeschlagen');
} finally { } finally {
setCreating(false); setCreating(false);
} }