import React from 'react'; import { DialogComponent } from '@syncfusion/ej2-react-popups'; import { TextBoxComponent } from '@syncfusion/ej2-react-inputs'; import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-calendars'; import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns'; import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons'; import CustomSelectUploadEventModal from './CustomSelectUploadEventModal'; import { updateEvent, detachEventOccurrence } from '../apiEvents'; // Holiday exceptions are now created in the backend type CustomEventData = { title: string; startDate: Date | null; startTime: Date | null; endTime: Date | null; type: string; description: string; repeat: boolean; weekdays: number[]; repeatUntil: Date | null; skipHolidays: boolean; media?: { id: string; path: string; name: string } | null; slideshowInterval?: number; pageProgress?: boolean; autoProgress?: boolean; websiteUrl?: string; // Video-specific fields autoplay?: boolean; loop?: boolean; volume?: number; muted?: boolean; }; // Typ für initialData erweitern, damit Id unterstützt wird type CustomEventModalProps = { open: boolean; onClose: () => void; onSave: (eventData: CustomEventData) => void; initialData?: Partial & { Id?: string; OccurrenceOfId?: string; isSingleOccurrence?: boolean; occurrenceDate?: Date; }; groupName: string | { id: string | null; name: string }; groupColor?: string; editMode?: boolean; // Removed unused blockHolidays and isHolidayRange }; const weekdayOptions = [ { value: 0, label: 'Montag' }, { value: 1, label: 'Dienstag' }, { value: 2, label: 'Mittwoch' }, { value: 3, label: 'Donnerstag' }, { value: 4, label: 'Freitag' }, { value: 5, label: 'Samstag' }, { value: 6, label: 'Sonntag' }, ]; const typeOptions = [ { value: 'presentation', label: 'Präsentation' }, { value: 'website', label: 'Website' }, { value: 'video', label: 'Video' }, { value: 'message', label: 'Nachricht' }, { value: 'webuntis', label: 'WebUntis' }, ]; const CustomEventModal: React.FC = ({ open, onClose, onSave, initialData = {}, groupName, groupColor, editMode, }) => { const [title, setTitle] = React.useState(initialData.title || ''); const [startDate, setStartDate] = React.useState(initialData.startDate || null); const [startTime, setStartTime] = React.useState( initialData.startTime || new Date(0, 0, 0, 9, 0) ); const [endTime, setEndTime] = React.useState(initialData.endTime || new Date(0, 0, 0, 9, 30)); const [type, setType] = React.useState(initialData.type ?? 'presentation'); const [description, setDescription] = React.useState(initialData.description || ''); // Initialize recurrence state - force to false/empty for single occurrence editing const isSingleOccurrence = initialData.isSingleOccurrence || false; const [repeat, setRepeat] = React.useState(isSingleOccurrence ? false : (initialData.repeat || false)); const [weekdays, setWeekdays] = React.useState(isSingleOccurrence ? [] : (initialData.weekdays || [])); const [repeatUntil, setRepeatUntil] = React.useState(isSingleOccurrence ? null : (initialData.repeatUntil || null)); // Default to true so recurrences skip holidays by default, but false for single occurrences const [skipHolidays, setSkipHolidays] = React.useState( isSingleOccurrence ? false : (initialData.skipHolidays !== undefined ? initialData.skipHolidays : true) ); const [errors, setErrors] = React.useState<{ [key: string]: string }>({}); // --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen --- const [media, setMedia] = React.useState<{ id: string; path: string; name: string } | null>( initialData.media ?? null ); // General settings state for presentation // Removed unused generalLoaded and setGeneralLoaded // Removed unused generalLoaded/generalSlideshowInterval/generalPageProgress/generalAutoProgress // Per-event state const [slideshowInterval, setSlideshowInterval] = React.useState( initialData.slideshowInterval ?? 10 ); const [pageProgress, setPageProgress] = React.useState( initialData.pageProgress ?? true ); const [autoProgress, setAutoProgress] = React.useState( initialData.autoProgress ?? true ); const [websiteUrl, setWebsiteUrl] = React.useState(initialData.websiteUrl ?? ''); // Video-specific state with system defaults loading const [autoplay, setAutoplay] = React.useState(initialData.autoplay ?? true); const [loop, setLoop] = React.useState(initialData.loop ?? true); const [volume, setVolume] = React.useState(initialData.volume ?? 0.8); const [muted, setMuted] = React.useState(initialData.muted ?? false); const [videoDefaultsLoaded, setVideoDefaultsLoaded] = React.useState(false); const [isSaving, setIsSaving] = React.useState(false); const [mediaModalOpen, setMediaModalOpen] = React.useState(false); // Load system video defaults once when opening for a new video event React.useEffect(() => { if (open && !editMode && !videoDefaultsLoaded) { (async () => { try { const api = await import('../apiSystemSettings'); const keys = ['video_autoplay', 'video_loop', 'video_volume', 'video_muted'] as const; const [autoplayRes, loopRes, volumeRes, mutedRes] = await Promise.all( keys.map(k => api.getSetting(k).catch(() => ({ value: null } as { value: string | null }))) ); // Only apply defaults if not already set from initialData if (initialData.autoplay === undefined) { setAutoplay(autoplayRes.value == null ? true : autoplayRes.value === 'true'); } if (initialData.loop === undefined) { setLoop(loopRes.value == null ? true : loopRes.value === 'true'); } if (initialData.volume === undefined) { const volParsed = volumeRes.value == null ? 0.8 : parseFloat(String(volumeRes.value)); setVolume(Number.isFinite(volParsed) ? volParsed : 0.8); } if (initialData.muted === undefined) { setMuted(mutedRes.value == null ? false : mutedRes.value === 'true'); } setVideoDefaultsLoaded(true); } catch { // Silently fall back to hard-coded defaults setVideoDefaultsLoaded(true); } })(); } }, [open, editMode, videoDefaultsLoaded, initialData]); React.useEffect(() => { if (open) { const isSingleOccurrence = initialData.isSingleOccurrence || false; setTitle(initialData.title || ''); setStartDate(initialData.startDate || null); setStartTime(initialData.startTime || new Date(0, 0, 0, 9, 0)); setEndTime(initialData.endTime || new Date(0, 0, 0, 9, 30)); setType(initialData.type ?? 'presentation'); setDescription(initialData.description || ''); // For single occurrence editing, force recurrence settings to be disabled if (isSingleOccurrence) { setRepeat(false); setWeekdays([]); setRepeatUntil(null); setSkipHolidays(false); } else { setRepeat(initialData.repeat || false); setWeekdays(initialData.weekdays || []); setRepeatUntil(initialData.repeatUntil || null); setSkipHolidays(initialData.skipHolidays !== undefined ? initialData.skipHolidays : true); } // --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen --- setMedia(initialData.media ?? null); setSlideshowInterval(initialData.slideshowInterval ?? 10); setPageProgress(initialData.pageProgress ?? true); setAutoProgress(initialData.autoProgress ?? true); setWebsiteUrl(initialData.websiteUrl ?? ''); // Video fields - use initialData values when editing if (editMode) { setAutoplay(initialData.autoplay ?? true); setLoop(initialData.loop ?? true); setVolume(initialData.volume ?? 0.8); setMuted(initialData.muted ?? false); } } }, [open, initialData, editMode]); const handleSave = async () => { if (isSaving) { return; } const newErrors: { [key: string]: string } = {}; if (!title.trim()) newErrors.title = 'Titel ist erforderlich'; if (!startDate) newErrors.startDate = 'Startdatum ist erforderlich'; if (!startTime) newErrors.startTime = 'Startzeit ist erforderlich'; if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich'; if (!type) newErrors.type = 'Termintyp ist erforderlich'; // Vergangenheitsprüfung - für Einzeltermine strikt, für Serien flexibler const startDateTime = startDate && startTime ? new Date( startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), startTime.getHours(), startTime.getMinutes() ) : null; const isPast = startDateTime && startDateTime < new Date(); if (isPast) { if (isSingleOccurrence || !repeat) { // Einzeltermine oder nicht-wiederkehrende Events dürfen nicht in der Vergangenheit liegen newErrors.startDate = 'Termine in der Vergangenheit sind nicht erlaubt!'; } else if (repeat && repeatUntil && repeatUntil < new Date()) { // Wiederkehrende Events sind erlaubt, wenn das End-Datum in der Zukunft liegt newErrors.repeatUntil = 'Terminserien mit End-Datum in der Vergangenheit sind nicht erlaubt!'; } // Andernfalls: Wiederkehrende Serie ohne End-Datum oder mit End-Datum in der Zukunft ist erlaubt } if (type === 'presentation') { if (!media) newErrors.media = 'Bitte eine Präsentation auswählen'; if (!slideshowInterval || slideshowInterval < 1) newErrors.slideshowInterval = 'Intervall angeben'; } if (type === 'website') { if (!websiteUrl.trim()) newErrors.websiteUrl = 'Webseiten-URL ist erforderlich'; } if (type === 'video') { if (!media) newErrors.media = 'Bitte ein Video auswählen'; } const parsedMediaId = media?.id ? Number(media.id) : null; if ( (type === 'presentation' || type === 'video') && (!Number.isFinite(parsedMediaId) || (parsedMediaId as number) <= 0) ) { newErrors.media = 'Ausgewähltes Medium ist ungültig. Bitte Datei erneut auswählen.'; } // Holiday blocking logic removed (blockHolidays, isHolidayRange no longer used) if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } setIsSaving(true); const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName; // Build recurrence rule if repeat is enabled let recurrenceRule = null; let recurrenceEnd = null; if (repeat && weekdays.length > 0) { // Convert weekdays to RRULE format (0=Monday -> MO) const rruleDays = weekdays.map(day => { const dayNames = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']; return dayNames[day]; }).join(','); recurrenceRule = `FREQ=WEEKLY;BYDAY=${rruleDays}`; if (repeatUntil) { const untilDate = new Date(repeatUntil); untilDate.setHours(23, 59, 59); recurrenceEnd = untilDate.toISOString(); // Note: RRULE UNTIL should be in UTC format for consistency const untilUTC = untilDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; recurrenceRule += `;UNTIL=${untilUTC}`; } } const payload: CustomEventData & { [key: string]: unknown } = { group_id, title, description, start: startDate && startTime ? new Date( startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), startTime.getHours(), startTime.getMinutes() ).toISOString() : null, end: startDate && endTime ? new Date( startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), endTime.getHours(), endTime.getMinutes() ).toISOString() : null, type, startDate, startTime, endTime, repeat: isSingleOccurrence ? false : repeat, weekdays: isSingleOccurrence ? [] : weekdays, repeatUntil: isSingleOccurrence ? null : repeatUntil, skipHolidays: isSingleOccurrence ? false : skipHolidays, event_type: type, is_active: 1, created_by: 1, recurrence_rule: isSingleOccurrence ? null : recurrenceRule, recurrence_end: isSingleOccurrence ? null : recurrenceEnd, }; if (type === 'presentation') { payload.event_media_id = parsedMediaId as number; payload.slideshow_interval = slideshowInterval; payload.page_progress = pageProgress; payload.auto_progress = autoProgress; } if (type === 'website') { payload.website_url = websiteUrl; } if (type === 'video') { payload.event_media_id = parsedMediaId as number; payload.autoplay = autoplay; payload.loop = loop; payload.volume = volume; payload.muted = muted; } try { let res; if (editMode && initialData && typeof initialData.Id === 'string') { // Check if this is a recurring event occurrence that should be detached const shouldDetach = isSingleOccurrence && initialData.OccurrenceOfId && !isNaN(Number(initialData.OccurrenceOfId)); if (shouldDetach) { // DETACH single occurrence from recurring series // Use occurrenceDate from initialData, or fall back to startDate const sourceDate = initialData.occurrenceDate || startDate; if (!sourceDate) { setErrors({ api: 'Fehler: Kein Datum für Einzeltermin verfügbar' }); return; } const occurrenceDate = sourceDate instanceof Date ? sourceDate.toISOString().split('T')[0] : new Date(sourceDate).toISOString().split('T')[0]; try { // Use the master event ID (OccurrenceOfId) for detaching const masterId = Number(initialData.OccurrenceOfId); res = await detachEventOccurrence(masterId, occurrenceDate, payload); } catch (error) { console.error('Detach operation failed:', error); setErrors({ api: `API Fehler: ${error instanceof Error ? error.message : String(error)}` }); return; } } else { // UPDATE entire event/series OR standalone event res = await updateEvent(initialData.Id, payload); } } else { // CREATE const createResponse = await fetch('/api/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); let createData: { success?: boolean; error?: string } = {}; try { createData = await createResponse.json(); } catch { createData = { error: `HTTP ${createResponse.status}` }; } if (!createResponse.ok) { setErrors({ api: createData.error || `Fehler beim Speichern (HTTP ${createResponse.status})`, }); return; } res = createData; } if (res.success) { onSave(payload); onClose(); // <--- Box nach erfolgreichem Speichern schließen } else { setErrors({ api: res.error || 'Fehler beim Speichern' }); } } catch { setErrors({ api: 'Netzwerkfehler beim Speichern' }); } finally { setIsSaving(false); } }; // Vergangenheitsprüfung (außerhalb von handleSave, damit im Render verfügbar) const startDateTime = startDate && startTime ? new Date( startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), startTime.getHours(), startTime.getMinutes() ) : null; const isPast = !!(startDateTime && startDateTime < new Date()); // Button sollte nur für Einzeltermine in der Vergangenheit deaktiviert werden // Wiederkehrende Serien können bearbeitet werden, auch wenn der Starttermin vergangen ist const shouldDisableButton = isPast && (isSingleOccurrence || !repeat); return ( (
{editMode ? (isSingleOccurrence ? 'Einzeltermin bearbeiten' : 'Terminserie bearbeiten') : 'Neuen Termin anlegen'} {groupName && ( für Raumgruppe: {typeof groupName === 'object' ? groupName.name : groupName} )}
)} showCloseIcon={true} close={onClose} isModal={true} footerTemplate={() => (
)} >
{errors.api && (
{errors.api}
)}
{/* ...Titel, Beschreibung, Datum, Zeit... */}
setTitle(e.value)} /> {errors.title &&
{errors.title}
}
setDescription(e.value)} />
setStartDate(e.value)} /> {errors.startDate && (
{errors.startDate}
)} {isPast && (isSingleOccurrence || !repeat) && ( ⚠️ Termin liegt in der Vergangenheit! )} {isPast && repeat && !isSingleOccurrence && ( ℹ️ Bearbeitung einer Terminserie (Startdatum kann in Vergangenheit liegen) )}
setStartTime(e.value)} /> {errors.startTime && (
{errors.startTime}
)}
setEndTime(e.value)} /> {errors.endTime && (
{errors.endTime}
)}
{/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */} {isSingleOccurrence && (
ℹ️ Bearbeitung eines Einzeltermins - Wiederholungsoptionen nicht verfügbar
)}
setRepeat(e.checked)} disabled={isSingleOccurrence} />
setWeekdays(e.value as number[])} disabled={!repeat || isSingleOccurrence} showDropDownIcon={true} closePopupOnSelect={false} />
setRepeatUntil(e.value)} disabled={!repeat || isSingleOccurrence} />
setSkipHolidays(e.checked)} disabled={!repeat || isSingleOccurrence} />
{/* NEUER ZWEISPALTIGER BEREICH für Termintyp und Zusatzfelder */}
setType(e.value as string)} style={{ width: '100%' }} /> {errors.type &&
{errors.type}
}
{type === 'presentation' && (
Ausgewähltes Medium:{' '} {media ? ( media.path ) : ( Kein Medium ausgewählt )}
{errors.media &&
{errors.media}
} {errors.slideshowInterval && (
{errors.slideshowInterval}
)} setSlideshowInterval(Number(e.value))} />
setPageProgress(e.checked || false)} />
setAutoProgress(e.checked || false)} />
)} {type === 'website' && (
setWebsiteUrl(e.value)} />
)} {type === 'video' && (
Ausgewähltes Video:{' '} {media ? ( media.path ) : ( Kein Video ausgewählt )}
{errors.media &&
{errors.media}
}
setAutoplay(e.checked || false)} />
setLoop(e.checked || false)} />
setVolume(Math.max(0, Math.min(1, Number(e.value))))} style={{ flex: 1 }} /> setMuted(e.checked || false)} />
)}
{mediaModalOpen && ( setMediaModalOpen(false)} onSelect={({ id, path, name }) => { setMedia({ id, path, name }); setErrors(prev => { if (!prev.media) return prev; const next = { ...prev }; delete next.media; return next; }); setMediaModalOpen(false); }} selectedFileId={null} /> )}
); }; export default CustomEventModal;