update
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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'] },
|
||||
],
|
||||
},
|
||||
|
||||
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