This commit is contained in:
Matthias Hochmeister
2026-03-16 15:01:09 +01:00
parent 3c72fe627f
commit f3ad989a9e
28 changed files with 794 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Box,
Typography,
@@ -60,6 +60,7 @@ import type {
VeranstaltungKategorie,
GroupInfo,
CreateVeranstaltungInput,
ConflictEvent,
} from '../types/events.types';
// ---------------------------------------------------------------------------
@@ -609,6 +610,46 @@ function EventFormDialog({
}
}, [open, editingEvent]);
// -----------------------------------------------------------------------
// Conflict detection — debounced check when dates change
// -----------------------------------------------------------------------
const [conflicts, setConflicts] = useState<ConflictEvent[]>([]);
const conflictTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
// Clear conflicts when dialog closes
if (!open) {
setConflicts([]);
return;
}
const vonDate = new Date(form.datum_von);
const bisDate = new Date(form.datum_bis);
if (isNaN(vonDate.getTime()) || isNaN(bisDate.getTime()) || bisDate <= vonDate) {
setConflicts([]);
return;
}
if (conflictTimerRef.current) clearTimeout(conflictTimerRef.current);
conflictTimerRef.current = setTimeout(async () => {
try {
const result = await eventsApi.checkConflicts(
vonDate.toISOString(),
bisDate.toISOString(),
editingEvent?.id
);
setConflicts(result);
} catch {
// Silently ignore — conflict check is advisory only
setConflicts([]);
}
}, 500);
return () => {
if (conflictTimerRef.current) clearTimeout(conflictTimerRef.current);
};
}, [open, form.datum_von, form.datum_bis, editingEvent]);
const handleChange = (field: keyof CreateVeranstaltungInput, value: unknown) => {
if (field === 'kategorie_id' && !editingEvent) {
// Auto-fill zielgruppen / alle_gruppen from the selected category (only for new events)
@@ -771,6 +812,31 @@ function EventFormDialog({
fullWidth
/>
{/* Conflict warning */}
{conflicts.length > 0 && (
<Alert severity="warning" sx={{ mt: 0 }}>
<Typography variant="body2" fontWeight={600} sx={{ mb: 0.5 }}>
Überschneidung mit bestehenden Veranstaltungen:
</Typography>
{conflicts.map((c) => {
const von = new Date(c.datum_von);
const bis = new Date(c.datum_bis);
const fmtDate = (d: Date) =>
`${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${d.getFullYear()}`;
const fmtTime = (d: Date) =>
`${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
const range = sameDay(von, bis)
? `${fmtDate(von)} ${fmtTime(von)} - ${fmtTime(bis)}`
: `${fmtDate(von)} ${fmtTime(von)} - ${fmtDate(bis)} ${fmtTime(bis)}`;
return (
<Typography key={c.id} variant="body2">
{'\u2022'} &quot;{c.titel}&quot; ({range})
</Typography>
);
})}
</Alert>
)}
{/* Ort */}
<TextField
label="Ort"