resolve issues with new features
This commit is contained in:
@@ -64,6 +64,8 @@ import {
|
||||
Today as TodayIcon,
|
||||
Tune,
|
||||
ViewList as ListViewIcon,
|
||||
ViewDay as ViewDayIcon,
|
||||
ViewWeek as ViewWeekIcon,
|
||||
Warning,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -97,6 +99,10 @@ import {
|
||||
format as fnsFormat,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
addDays,
|
||||
subDays,
|
||||
addWeeks,
|
||||
subWeeks,
|
||||
eachDayOfInterval,
|
||||
@@ -1571,8 +1577,11 @@ export default function Kalender() {
|
||||
year: today.getFullYear(),
|
||||
month: today.getMonth(),
|
||||
});
|
||||
const [viewMode, setViewMode] = useState<'calendar' | 'list'>('calendar');
|
||||
const [viewMode, setViewMode] = useState<'calendar' | 'list' | 'day' | 'week'>('calendar');
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedKategorie, setSelectedKategorie] = useState<string | 'all'>('all');
|
||||
const [listFrom, setListFrom] = useState(() => fnsFormat(startOfMonth(today), 'yyyy-MM-dd'));
|
||||
const [listTo, setListTo] = useState(() => fnsFormat(endOfMonth(today), 'yyyy-MM-dd'));
|
||||
|
||||
const [trainingEvents, setTrainingEvents] = useState<UebungListItem[]>([]);
|
||||
const [veranstaltungen, setVeranstaltungen] = useState<VeranstaltungListItem[]>([]);
|
||||
@@ -1696,23 +1705,37 @@ export default function Kalender() {
|
||||
// ── Calendar tab helpers ─────────────────────────────────────────────────────
|
||||
|
||||
const handlePrev = () => {
|
||||
setViewMonth((prev) => {
|
||||
const m = prev.month === 0 ? 11 : prev.month - 1;
|
||||
const y = prev.month === 0 ? prev.year - 1 : prev.year;
|
||||
return { year: y, month: m };
|
||||
});
|
||||
if (viewMode === 'day') {
|
||||
setCurrentDate((d) => subDays(d, 1));
|
||||
} else if (viewMode === 'week') {
|
||||
setCurrentDate((d) => subWeeks(d, 1));
|
||||
} else {
|
||||
setViewMonth((prev) => {
|
||||
const m = prev.month === 0 ? 11 : prev.month - 1;
|
||||
const y = prev.month === 0 ? prev.year - 1 : prev.year;
|
||||
return { year: y, month: m };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setViewMonth((prev) => {
|
||||
const m = prev.month === 11 ? 0 : prev.month + 1;
|
||||
const y = prev.month === 11 ? prev.year + 1 : prev.year;
|
||||
return { year: y, month: m };
|
||||
});
|
||||
if (viewMode === 'day') {
|
||||
setCurrentDate((d) => addDays(d, 1));
|
||||
} else if (viewMode === 'week') {
|
||||
setCurrentDate((d) => addWeeks(d, 1));
|
||||
} else {
|
||||
setViewMonth((prev) => {
|
||||
const m = prev.month === 11 ? 0 : prev.month + 1;
|
||||
const y = prev.month === 11 ? prev.year + 1 : prev.year;
|
||||
return { year: y, month: m };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToday = () => {
|
||||
setViewMonth({ year: today.getFullYear(), month: today.getMonth() });
|
||||
const now = new Date();
|
||||
setViewMonth({ year: now.getFullYear(), month: now.getMonth() });
|
||||
setCurrentDate(now);
|
||||
};
|
||||
|
||||
const handleDayClick = useCallback(
|
||||
@@ -1742,23 +1765,59 @@ export default function Kalender() {
|
||||
});
|
||||
}, [veranstaltungen, popoverDay, selectedKategorie]);
|
||||
|
||||
// Filtered lists for list view (current month only)
|
||||
// Filtered lists for list view (filtered by date range)
|
||||
const trainingForMonth = useMemo(
|
||||
() =>
|
||||
trainingEvents.filter((t) => {
|
||||
() => {
|
||||
const from = parseISO(listFrom);
|
||||
const to = parseISO(listTo);
|
||||
return trainingEvents.filter((t) => {
|
||||
const d = new Date(t.datum_von);
|
||||
return d.getMonth() === viewMonth.month && d.getFullYear() === viewMonth.year;
|
||||
}),
|
||||
[trainingEvents, viewMonth]
|
||||
return d >= startOfDay(from) && d <= new Date(to.getFullYear(), to.getMonth(), to.getDate(), 23, 59, 59);
|
||||
});
|
||||
},
|
||||
[trainingEvents, listFrom, listTo]
|
||||
);
|
||||
|
||||
const eventsForMonth = useMemo(
|
||||
() =>
|
||||
veranstaltungen.filter((ev) => {
|
||||
() => {
|
||||
const from = parseISO(listFrom);
|
||||
const to = parseISO(listTo);
|
||||
return veranstaltungen.filter((ev) => {
|
||||
const d = new Date(ev.datum_von);
|
||||
return d.getMonth() === viewMonth.month && d.getFullYear() === viewMonth.year;
|
||||
}),
|
||||
[veranstaltungen, viewMonth]
|
||||
return d >= startOfDay(from) && d <= new Date(to.getFullYear(), to.getMonth(), to.getDate(), 23, 59, 59);
|
||||
});
|
||||
},
|
||||
[veranstaltungen, listFrom, listTo]
|
||||
);
|
||||
|
||||
// Events for the selected day (day view)
|
||||
const trainingForCurrentDay = useMemo(
|
||||
() => trainingEvents.filter((t) => sameDay(new Date(t.datum_von), currentDate)),
|
||||
[trainingEvents, currentDate]
|
||||
);
|
||||
|
||||
const eventsForCurrentDay = useMemo(
|
||||
() => veranstaltungen.filter((ev) => {
|
||||
const start = startOfDay(new Date(ev.datum_von));
|
||||
const end = startOfDay(new Date(ev.datum_bis));
|
||||
const cur = startOfDay(currentDate);
|
||||
return cur >= start && cur <= end;
|
||||
}),
|
||||
[veranstaltungen, currentDate]
|
||||
);
|
||||
|
||||
// Events for the selected week (week view)
|
||||
const currentWeekStartCal = useMemo(
|
||||
() => startOfWeek(currentDate, { weekStartsOn: 1 }),
|
||||
[currentDate]
|
||||
);
|
||||
const currentWeekEndCal = useMemo(
|
||||
() => endOfWeek(currentDate, { weekStartsOn: 1 }),
|
||||
[currentDate]
|
||||
);
|
||||
const weekDaysCal = useMemo(
|
||||
() => eachDayOfInterval({ start: currentWeekStartCal, end: currentWeekEndCal }),
|
||||
[currentWeekStartCal, currentWeekEndCal]
|
||||
);
|
||||
|
||||
// ── Veranstaltung cancel ─────────────────────────────────────────────────────
|
||||
@@ -1991,6 +2050,24 @@ export default function Kalender() {
|
||||
>
|
||||
{/* View toggle */}
|
||||
<ButtonGroup size="small" variant="outlined">
|
||||
<Tooltip title="Tagesansicht">
|
||||
<Button
|
||||
onClick={() => setViewMode('day')}
|
||||
variant={viewMode === 'day' ? 'contained' : 'outlined'}
|
||||
>
|
||||
<ViewDayIcon fontSize="small" />
|
||||
{!isMobile && <Box sx={{ ml: 0.5 }}>Tag</Box>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Wochenansicht">
|
||||
<Button
|
||||
onClick={() => setViewMode('week')}
|
||||
variant={viewMode === 'week' ? 'contained' : 'outlined'}
|
||||
>
|
||||
<ViewWeekIcon fontSize="small" />
|
||||
{!isMobile && <Box sx={{ ml: 0.5 }}>Woche</Box>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Monatsansicht">
|
||||
<Button
|
||||
onClick={() => setViewMode('calendar')}
|
||||
@@ -2051,22 +2128,20 @@ export default function Kalender() {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* PDF Export — only in list view */}
|
||||
{viewMode === 'list' && (
|
||||
<Tooltip title="Als PDF exportieren">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => generatePdf(
|
||||
viewMonth.year,
|
||||
viewMonth.month,
|
||||
trainingForMonth,
|
||||
eventsForMonth,
|
||||
)}
|
||||
>
|
||||
<PdfIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* PDF Export — available in all views */}
|
||||
<Tooltip title="Als PDF exportieren">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => generatePdf(
|
||||
viewMonth.year,
|
||||
viewMonth.month,
|
||||
trainingForMonth,
|
||||
eventsForMonth,
|
||||
)}
|
||||
>
|
||||
<PdfIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* CSV Import */}
|
||||
{canWriteEvents && (
|
||||
@@ -2091,7 +2166,7 @@ export default function Kalender() {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Month navigation */}
|
||||
{/* Navigation */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}>
|
||||
<IconButton onClick={handlePrev} size="small">
|
||||
<ChevronLeft />
|
||||
@@ -2100,7 +2175,11 @@ export default function Kalender() {
|
||||
variant="h6"
|
||||
sx={{ flexGrow: 1, textAlign: 'center', fontWeight: 600 }}
|
||||
>
|
||||
{MONTH_LABELS[viewMonth.month]} {viewMonth.year}
|
||||
{viewMode === 'day'
|
||||
? formatDateLong(currentDate)
|
||||
: viewMode === 'week'
|
||||
? `KW ${fnsFormat(currentWeekStartCal, 'w')} — ${fnsFormat(currentWeekStartCal, 'dd.MM.')} – ${fnsFormat(currentWeekEndCal, 'dd.MM.yyyy')}`
|
||||
: `${MONTH_LABELS[viewMonth.month]} ${viewMonth.year}`}
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -2121,13 +2200,171 @@ export default function Kalender() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Calendar / List body */}
|
||||
{/* Calendar / List / Day / Week body */}
|
||||
{calLoading ? (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={isMobile ? 320 : 480}
|
||||
sx={{ borderRadius: 2 }}
|
||||
/>
|
||||
) : viewMode === 'day' ? (
|
||||
/* ── Day View ── */
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
{trainingForCurrentDay.length === 0 && eventsForCurrentDay.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
|
||||
Keine Einträge an diesem Tag.
|
||||
</Typography>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{trainingForCurrentDay.length > 0 && (
|
||||
<Typography variant="overline" sx={{ px: 0.5, color: 'text.secondary' }}>
|
||||
Dienste
|
||||
</Typography>
|
||||
)}
|
||||
{trainingForCurrentDay.map((t) => (
|
||||
<ListItem
|
||||
key={`t-${t.id}`}
|
||||
sx={{
|
||||
borderLeft: `4px solid ${TYP_DOT_COLOR[t.typ]}`,
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
opacity: t.abgesagt ? 0.55 : 1,
|
||||
}}
|
||||
onClick={() => navigate(`/training/${t.id}`)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{t.pflichtveranstaltung && <StarIcon sx={{ fontSize: 14, color: 'warning.main' }} />}
|
||||
<Typography variant="body2" sx={{ fontWeight: t.pflichtveranstaltung ? 700 : 400, textDecoration: t.abgesagt ? 'line-through' : 'none' }}>
|
||||
{t.titel}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={`${formatTime(t.datum_von)} – ${formatTime(t.datum_bis)} Uhr · ${t.typ}`}
|
||||
/>
|
||||
<RsvpDot status={t.eigener_status} />
|
||||
</ListItem>
|
||||
))}
|
||||
{trainingForCurrentDay.length > 0 && eventsForCurrentDay.length > 0 && <Divider sx={{ my: 1 }} />}
|
||||
{eventsForCurrentDay.length > 0 && (
|
||||
<Typography variant="overline" sx={{ px: 0.5, color: 'text.secondary' }}>
|
||||
Veranstaltungen
|
||||
</Typography>
|
||||
)}
|
||||
{eventsForCurrentDay
|
||||
.filter((ev) => selectedKategorie === 'all' || ev.kategorie_id === selectedKategorie)
|
||||
.map((ev) => (
|
||||
<ListItem
|
||||
key={`e-${ev.id}`}
|
||||
sx={{
|
||||
borderLeft: `4px solid ${ev.kategorie_farbe ?? '#1976d2'}`,
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
opacity: ev.abgesagt ? 0.55 : 1,
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, textDecoration: ev.abgesagt ? 'line-through' : 'none' }}>
|
||||
{ev.titel}
|
||||
{ev.abgesagt && <Chip label="Abgesagt" size="small" color="error" variant="outlined" sx={{ ml: 0.5, fontSize: '0.6rem', height: 16 }} />}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
{ev.ganztaegig ? 'Ganztägig' : `${formatTime(ev.datum_von)} – ${formatTime(ev.datum_bis)} Uhr`}
|
||||
{ev.ort && ` · ${ev.ort}`}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Paper>
|
||||
) : viewMode === 'week' ? (
|
||||
/* ── Week View ── */
|
||||
<Paper elevation={1} sx={{ p: 1, overflowX: 'auto' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px', minWidth: 700 }}>
|
||||
{weekDaysCal.map((day) => {
|
||||
const isToday = sameDay(day, new Date());
|
||||
const dayTraining = trainingEvents.filter((t) => sameDay(new Date(t.datum_von), day));
|
||||
const dayEvents = veranstaltungen.filter((ev) => {
|
||||
if (selectedKategorie !== 'all' && ev.kategorie_id !== selectedKategorie) return false;
|
||||
const start = startOfDay(new Date(ev.datum_von));
|
||||
const end = startOfDay(new Date(ev.datum_bis));
|
||||
const cur = startOfDay(day);
|
||||
return cur >= start && cur <= end;
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={day.toISOString()}
|
||||
sx={{
|
||||
minHeight: 120,
|
||||
border: '1px solid',
|
||||
borderColor: isToday ? 'primary.main' : 'divider',
|
||||
borderRadius: 1,
|
||||
p: 0.5,
|
||||
bgcolor: isToday ? 'primary.main' : 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
fontWeight: 600,
|
||||
color: isToday ? 'primary.contrastText' : 'text.primary',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{fnsFormat(day, 'EEE dd.MM.', { locale: de })}
|
||||
</Typography>
|
||||
{dayTraining.map((t) => (
|
||||
<Chip
|
||||
key={`t-${t.id}`}
|
||||
label={t.titel}
|
||||
size="small"
|
||||
onClick={() => navigate(`/training/${t.id}`)}
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
height: 18,
|
||||
mb: '2px',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
bgcolor: t.abgesagt ? 'action.disabledBackground' : TYP_DOT_COLOR[t.typ],
|
||||
color: t.abgesagt ? 'text.disabled' : 'white',
|
||||
textDecoration: t.abgesagt ? 'line-through' : 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{dayEvents.map((ev) => (
|
||||
<Chip
|
||||
key={`e-${ev.id}`}
|
||||
label={ev.titel}
|
||||
size="small"
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
height: 18,
|
||||
mb: '2px',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
bgcolor: ev.abgesagt ? 'action.disabledBackground' : (ev.kategorie_farbe ?? '#1976d2'),
|
||||
color: ev.abgesagt ? 'text.disabled' : 'white',
|
||||
textDecoration: ev.abgesagt ? 'line-through' : 'none',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Paper>
|
||||
) : viewMode === 'calendar' ? (
|
||||
<Paper elevation={1} sx={{ p: 1 }}>
|
||||
<MonthCalendar
|
||||
@@ -2140,24 +2377,47 @@ export default function Kalender() {
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper elevation={1} sx={{ px: 1 }}>
|
||||
<CombinedListView
|
||||
trainingEvents={trainingForMonth}
|
||||
veranstaltungen={eventsForMonth}
|
||||
selectedKategorie={selectedKategorie}
|
||||
canWriteEvents={canWriteEvents}
|
||||
onTrainingClick={(id) => navigate(`/training/${id}`)}
|
||||
onEventEdit={(ev) => {
|
||||
setVeranstEditing(ev);
|
||||
setVeranstFormOpen(true);
|
||||
}}
|
||||
onEventCancel={(id) => {
|
||||
setCancelEventId(id);
|
||||
setCancelEventGrund('');
|
||||
}}
|
||||
onEventDelete={(id) => setDeleteEventId(id)}
|
||||
/>
|
||||
</Paper>
|
||||
<>
|
||||
{/* Date range inputs for list view */}
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
type="date"
|
||||
label="Von"
|
||||
size="small"
|
||||
value={listFrom}
|
||||
onChange={(e) => setListFrom(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ width: 170 }}
|
||||
/>
|
||||
<TextField
|
||||
type="date"
|
||||
label="Bis"
|
||||
size="small"
|
||||
value={listTo}
|
||||
onChange={(e) => setListTo(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ width: 170 }}
|
||||
/>
|
||||
</Box>
|
||||
<Paper elevation={1} sx={{ px: 1 }}>
|
||||
<CombinedListView
|
||||
trainingEvents={trainingForMonth}
|
||||
veranstaltungen={eventsForMonth}
|
||||
selectedKategorie={selectedKategorie}
|
||||
canWriteEvents={canWriteEvents}
|
||||
onTrainingClick={(id) => navigate(`/training/${id}`)}
|
||||
onEventEdit={(ev) => {
|
||||
setVeranstEditing(ev);
|
||||
setVeranstFormOpen(true);
|
||||
}}
|
||||
onEventCancel={(id) => {
|
||||
setCancelEventId(id);
|
||||
setCancelEventGrund('');
|
||||
}}
|
||||
onEventDelete={(id) => setDeleteEventId(id)}
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* FAB: Create Veranstaltung */}
|
||||
|
||||
Reference in New Issue
Block a user