update
This commit is contained in:
@@ -16,6 +16,7 @@ import HomePage from './pages/HomePage';
|
|||||||
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
||||||
import CompanyPage from "./pages/CompanyPage.jsx";
|
import CompanyPage from "./pages/CompanyPage.jsx";
|
||||||
import EditCompanyPage from "./pages/EditCompanyPage.jsx";
|
import EditCompanyPage from "./pages/EditCompanyPage.jsx";
|
||||||
|
import CalenderPostPage from "./pages/CalenderPostPage.jsx";
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -75,6 +76,14 @@ export default function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="calenderPosts"
|
||||||
|
element={
|
||||||
|
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
|
||||||
|
<CalenderPostPage />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="posts"
|
path="posts"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 LocationOnIcon from '@mui/icons-material/LocationOn';
|
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||||
|
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';
|
||||||
@@ -34,6 +35,7 @@ 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: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
|
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
322
src/pages/CalenderPostPage.jsx
Normal file
322
src/pages/CalenderPostPage.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user