This commit is contained in:
2026-04-13 14:43:06 +02:00
parent a8e155f5d5
commit 57dd2f5955
3 changed files with 333 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ import HomePage from './pages/HomePage';
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
import CompanyPage from "./pages/CompanyPage.jsx";
import EditCompanyPage from "./pages/EditCompanyPage.jsx";
import CalenderPostPage from "./pages/CalenderPostPage.jsx";
const theme = createTheme({
palette: {
@@ -75,6 +76,14 @@ export default function App() {
</PrivateRoute>
}
/>
<Route
path="calenderPosts"
element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
<CalenderPostPage />
</PrivateRoute>
}
/>
<Route
path="posts"
element={

View File

@@ -10,6 +10,7 @@ import CorporateFareIcon from '@mui/icons-material/CorporateFare';
import LogoutIcon from '@mui/icons-material/Logout';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import HomeIcon from '@mui/icons-material/Home';
import StoreIcon from '@mui/icons-material/Store';
import { useAuth } from '../context/AuthContext';
@@ -34,6 +35,7 @@ const navCategories = [
label: 'Reporter',
items: [
{ label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
{ label: 'Kalendereinträge', path: '/calenderPosts', icon: <CalendarMonthIcon />, roles: ['ADMIN', 'REPORTER'] },
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
],
},

View File

@@ -0,0 +1,322 @@
import { useEffect, useState } from 'react';
import {
Box, Typography, Alert, IconButton, Tooltip,
Dialog, DialogTitle, DialogContent, DialogActions,
Button, Stack, TextField, FormControlLabel, Checkbox, Chip,
} from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import EditIcon from '@mui/icons-material/Edit';
import AddIcon from '@mui/icons-material/Add';
import axiosInstance from '../api/axiosInstance';
const emptyForm = {
title: '',
description: '',
location: '',
organizer: '',
fullDay: false,
startTime: '',
endTime: '',
};
export default function CalenderPostPage() {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [detailEntry, setDetailEntry] = useState(null);
const [editEntry, setEditEntry] = useState(null);
const [formData, setFormData] = useState(emptyForm);
const [saving, setSaving] = useState(false);
useEffect(() => {
axiosInstance.get('/calenderPost')
.then(({ data }) => setRows(data))
.catch(() => setError('Fehler beim Laden der Kalendereinträge'))
.finally(() => setLoading(false));
}, []);
const handleDelete = async (id) => {
if (!window.confirm('Kalendereintrag wirklich löschen?')) return;
try {
await axiosInstance.delete(`/calenderPost/${id}`);
setRows((prev) => prev.filter((r) => r.id !== id));
} catch {
alert('Löschen fehlgeschlagen');
}
};
const openCreate = () => {
setFormData(emptyForm);
setEditEntry({});
};
const openEdit = (row) => {
setFormData({
title: row.title || '',
description: row.description || '',
location: row.location || '',
organizer: row.organizer || '',
fullDay: row.fullDay || false,
startTime: row.fullDay
? (row.startTime ? row.startTime.substring(0, 10) : '')
: (row.startTime ? row.startTime.substring(0, 16) : ''),
endTime: row.endTime ? row.endTime.substring(0, 16) : '',
});
setDetailEntry(null);
setEditEntry(row);
};
const handleSave = async () => {
setSaving(true);
try {
const payload = {
...formData,
startTime: formData.fullDay
? (formData.startTime ? `${formData.startTime}T00:00:00` : null)
: (formData.startTime || null),
endTime: formData.fullDay ? null : (formData.endTime || null),
};
if (editEntry?.id) {
const { data } = await axiosInstance.put(`/calenderPost/${editEntry.id}`, payload);
setRows((prev) => prev.map((r) => r.id === data.id ? data : r));
} else {
await axiosInstance.post('/calenderPost', payload);
const { data: updated } = await axiosInstance.get('/calenderPost');
setRows(updated);
}
setEditEntry(null);
} catch {
alert('Speichern fehlgeschlagen');
} finally {
setSaving(false);
}
};
const formatDateTime = (value) => {
if (!value) return '—';
return new Date(value).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' });
};
const columns = [
{ field: 'id', headerName: 'ID', width: 70 },
{
field: 'title', headerName: 'Titel', flex: 1.5,
renderCell: ({ row, value }) => (
<Tooltip title="Details anzeigen">
<Box
onClick={() => setDetailEntry(row)}
sx={{ cursor: 'pointer', color: 'primary.main', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{value}
</Box>
</Tooltip>
),
},
{ field: 'location', headerName: 'Ort', width: 130 },
{ field: 'organizer', headerName: 'Organisator', width: 140 },
{
field: 'fullDay', headerName: 'Ganztag', width: 95,
renderCell: ({ value }) => value
? <Chip label="Ja" size="small" color="primary" />
: <Chip label="Nein" size="small" variant="outlined" />,
},
{
field: 'startTime', headerName: 'Beginn', width: 150,
valueFormatter: (value) => formatDateTime(value),
},
{
field: 'endTime', headerName: 'Ende', width: 150,
valueFormatter: (value) => formatDateTime(value),
},
{
field: 'author', headerName: 'Autor', width: 130,
valueGetter: (value) => value || '—', // war: value?.username || value?.name || '—'
},
{
field: 'actions', headerName: '', width: 100, sortable: false,
renderCell: ({ row }) => (
<Box>
<Tooltip title="Bearbeiten">
<IconButton size="small" color="primary" onClick={() => openEdit(row)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Löschen">
<IconButton size="small" color="error" onClick={() => handleDelete(row.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
),
},
];
const isEditing = editEntry !== null;
const isNew = isEditing && !editEntry?.id;
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h5" fontWeight={600}>Kalendereinträge</Typography>
<Button variant="contained" startIcon={<AddIcon />} onClick={openCreate}>
Eintrag erstellen
</Button>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<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>
{/* Detail Dialog */}
<Dialog open={!!detailEntry} onClose={() => setDetailEntry(null)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600 }}>{detailEntry?.title}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box>
<Typography variant="caption" color="text.secondary">Beschreibung</Typography>
<Typography variant="body2" sx={{ mt: 0.5, whiteSpace: 'pre-wrap' }}>{detailEntry?.description}</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
<Box>
<Typography variant="caption" color="text.secondary">Ort</Typography>
<Typography variant="body2">{detailEntry?.location || '—'}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Organisator</Typography>
<Typography variant="body2">{detailEntry?.organizer || '—'}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Ganztag</Typography>
<Typography variant="body2">{detailEntry?.fullDay ? 'Ja' : 'Nein'}</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
<Box>
<Typography variant="caption" color="text.secondary">Beginn</Typography>
<Typography variant="body2">{formatDateTime(detailEntry?.startTime)}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Ende</Typography>
<Typography variant="body2">{formatDateTime(detailEntry?.endTime)}</Typography>
</Box>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Autor</Typography>
<Typography variant="body2">{detailEntry?.author || '—'}</Typography>
</Box>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={() => openEdit(detailEntry)}>Bearbeiten</Button>
<Button onClick={() => setDetailEntry(null)}>Schließen</Button>
</DialogActions>
</Dialog>
{/* Erstellen / Bearbeiten Dialog */}
<Dialog open={isEditing} onClose={() => setEditEntry(null)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600 }}>
{isNew ? 'Neuer Kalendereintrag' : 'Kalendereintrag bearbeiten'}
</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1 }}>
<TextField
label="Titel"
value={formData.title}
onChange={(e) => setFormData((p) => ({ ...p, title: e.target.value }))}
fullWidth
required
/>
<TextField
label="Beschreibung"
value={formData.description}
onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
fullWidth
multiline
rows={3}
required
/>
<TextField
label="Ort"
value={formData.location}
onChange={(e) => setFormData((p) => ({ ...p, location: e.target.value }))}
fullWidth
/>
<TextField
label="Organisator"
value={formData.organizer}
onChange={(e) => setFormData((p) => ({ ...p, organizer: e.target.value }))}
fullWidth
/>
<FormControlLabel
control={
<Checkbox
checked={formData.fullDay}
onChange={(e) => setFormData((p) => ({
...p,
fullDay: e.target.checked,
startTime: '',
endTime: '',
}))}
/>
}
label="Ganztägig"
/>
{formData.fullDay ? (
<TextField
label="Datum"
type="date"
value={formData.startTime}
onChange={(e) => setFormData((p) => ({ ...p, startTime: e.target.value }))}
fullWidth
required
InputLabelProps={{ shrink: true }}
/>
) : (
<>
<TextField
label="Beginn"
type="datetime-local"
value={formData.startTime}
onChange={(e) => setFormData((p) => ({ ...p, startTime: e.target.value }))}
fullWidth
InputLabelProps={{ shrink: true }}
/>
<TextField
label="Ende"
type="datetime-local"
value={formData.endTime}
onChange={(e) => setFormData((p) => ({ ...p, endTime: e.target.value }))}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</>
)}
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2.5 }}>
<Button onClick={() => setEditEntry(null)} disabled={saving}>Abbrechen</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={saving || !formData.title || !formData.description || !formData.startTime}
>
{saving ? 'Speichern…' : 'Speichern'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}