stabile version
This commit is contained in:
262
src/App.jsx
262
src/App.jsx
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
29
src/components/ErrorBoundary.jsx
Normal file
29
src/components/ErrorBoundary.jsx
Normal 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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 (22–8 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
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user