import React, { useEffect, useMemo, useState } from 'react'; import { ScheduleComponent, Day, Week, WorkWeek, Month, Agenda, Inject, ViewsDirective, ViewDirective, } from '@syncfusion/ej2-react-schedule'; import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns'; import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base'; import type { EventRenderedArgs, ActionEventArgs, RenderCellEventArgs, } from '@syncfusion/ej2-react-schedule'; import { fetchEvents } from './apiEvents'; import { fetchGroups } from './apiGroups'; import { getGroupColor } from './groupColors'; import { deleteEvent } from './apiEvents'; import CustomEventModal from './components/CustomEventModal'; import { fetchMediaById } from './apiClients'; import { listHolidays, type Holiday } from './apiHolidays'; import { getAcademicPeriodForDate, listAcademicPeriods, setActiveAcademicPeriod, } from './apiAcademicPeriods'; import { Presentation, Globe, Video, MessageSquare, School, CheckCircle, AlertCircle, TentTree, } from 'lucide-react'; import caGregorian from './cldr/ca-gregorian.json'; import numbers from './cldr/numbers.json'; import timeZoneNames from './cldr/timeZoneNames.json'; import numberingSystems from './cldr/numberingSystems.json'; // Typ für Gruppe ergänzen type Group = { id: string; name: string; }; // Typ für Event ergänzen type Event = { Id: string; Subject: string; StartTime: Date; EndTime: Date; IsAllDay: boolean; IsBlock?: boolean; // Syncfusion block appointment isHoliday?: boolean; // marker for styling/logic MediaId?: string | number; SlideshowInterval?: number; WebsiteUrl?: string; Icon?: string; // <--- Icon ergänzen! Type?: string; // <--- Typ ergänzen, falls benötigt OccurrenceOfId?: string; // Serieninstanz Recurrence?: boolean; // Marker für Serientermin RecurrenceRule?: string | null; RecurrenceEnd?: string | null; SkipHolidays?: boolean; RecurrenceException?: string; }; type RawEvent = { Id: string; Subject: string; StartTime: string; EndTime: string; IsAllDay: boolean; MediaId?: string | number; Icon?: string; // <--- Icon ergänzen! Type?: string; OccurrenceOfId?: string; RecurrenceRule?: string | null; RecurrenceEnd?: string | null; SkipHolidays?: boolean; RecurrenceException?: string; }; // CLDR-Daten laden (direkt die JSON-Objekte übergeben) loadCldr( caGregorian as object, numbers as object, timeZoneNames as object, numberingSystems as object ); // Deutsche Lokalisierung für den Scheduler L10n.load({ de: { schedule: { day: 'Tag', week: 'Woche', workWeek: 'Arbeitswoche', month: 'Monat', agenda: 'Agenda', today: 'Heute', noEvents: 'Keine Termine', allDay: 'Ganztägig', start: 'Start', end: 'Ende', event: 'Termin', save: 'Speichern', cancel: 'Abbrechen', delete: 'Löschen', edit: 'Bearbeiten', newEvent: 'Neuer Termin', title: 'Titel', description: 'Beschreibung', location: 'Ort', recurrence: 'Wiederholung', repeat: 'Wiederholen', deleteEvent: 'Termin löschen', deleteContent: 'Möchten Sie diesen Termin wirklich löschen?', moreDetails: 'Mehr Details', addTitle: 'Termintitel', }, }, }); // Kultur setzen setCulture('de'); // Mapping für Lucide-Icons const iconMap: Record = { Presentation, Globe, Video, MessageSquare, School, }; const eventTemplate = (event: Event) => { const IconComponent = iconMap[event.Icon ?? ''] || null; // Zeitangabe formatieren const start = event.StartTime instanceof Date ? event.StartTime : new Date(event.StartTime); const end = event.EndTime instanceof Date ? event.EndTime : new Date(event.EndTime); const timeString = `${start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; // Render TentTree icon if SkipHolidays is true const showTentTree = !!event.SkipHolidays; return (
{IconComponent && ( )} {showTentTree && ( )} {event.Subject}
{timeString}
); } // Robust ISO date parsing: append 'Z' only if no timezone offset present function parseEventDate(iso: string): Date { if (!iso) return new Date(NaN); // If string already contains 'Z' or a timezone offset like +00:00/-02:00, don't modify const hasTZ = /Z$/i.test(iso) || /[+-]\d{2}:\d{2}$/.test(iso); const s = hasTZ ? iso : `${iso}Z`; return new Date(s); } const Appointments: React.FC = () => { // Debug logging removed const [groups, setGroups] = useState([]); const [selectedGroupId, setSelectedGroupId] = useState(null); const [events, setEvents] = useState([]); const [holidays, setHolidays] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [modalInitialData, setModalInitialData] = useState({}); const [schedulerKey, setSchedulerKey] = useState(0); const [editMode, setEditMode] = useState(false); // NEU: Editiermodus const [showInactive, setShowInactive] = React.useState(true); const [allowScheduleOnHolidays, setAllowScheduleOnHolidays] = React.useState(false); const [showHolidayList, setShowHolidayList] = React.useState(true); // Always let Syncfusion handle recurrence; do not expand on backend // Remove expandRecurrences toggle and state const scheduleRef = React.useRef(null); const [holidaysInView, setHolidaysInView] = React.useState(0); const [schoolYearLabel, setSchoolYearLabel] = React.useState(''); const [hasSchoolYearPlan, setHasSchoolYearPlan] = React.useState(false); const [periods, setPeriods] = React.useState<{ id: number; label: string }[]>([]); const [activePeriodId, setActivePeriodId] = React.useState(null); // Gruppen laden useEffect(() => { fetchGroups() .then(data => { // Nur Gruppen mit id != 1 berücksichtigen (nicht zugeordnet ignorieren) const filtered = Array.isArray(data) ? data.filter(g => g.id && g.name && g.id !== 1) : []; setGroups(filtered); if (filtered.length > 0) setSelectedGroupId(filtered[0].id); }) .catch(console.error); }, []); // Holidays laden useEffect(() => { listHolidays() .then(res => setHolidays(res.holidays || [])) .catch(err => console.error('Ferien laden fehlgeschlagen:', err)); }, []); // Perioden laden (Dropdown) useEffect(() => { listAcademicPeriods() .then(all => { setPeriods(all.map(p => ({ id: p.id, label: p.display_name || p.name }))); const active = all.find(p => p.is_active); setActivePeriodId(active ? active.id : null); }) .catch(err => console.error('Akademische Perioden laden fehlgeschlagen:', err)); }, []); // fetchAndSetEvents als useCallback definieren, damit die Dependency korrekt ist: const fetchAndSetEvents = React.useCallback(async () => { if (!selectedGroupId) { setEvents([]); return; } try { // Sichtbaren Bereich bestimmen (optional, for future filtering) const inst = scheduleRef.current; let startRange: Date | undefined; let endRange: Date | undefined; // Fallback to current week if scheduler is not initialized yet const today = new Date(); const fallbackStart = new Date(today); const day = today.getDay(); const diffToMonday = (day + 6) % 7; // Monday=0 fallbackStart.setDate(today.getDate() - diffToMonday); fallbackStart.setHours(0, 0, 0, 0); const fallbackEnd = new Date(fallbackStart); fallbackEnd.setDate(fallbackStart.getDate() + 6); fallbackEnd.setHours(23, 59, 59, 999); if (inst) { const view = inst.currentView as 'Day' | 'Week' | 'WorkWeek' | 'Month' | 'Agenda'; const baseDate = inst.selectedDate as Date; if (baseDate) { if (view === 'Day' || view === 'Agenda') { startRange = new Date(baseDate); startRange.setHours(0, 0, 0, 0); endRange = new Date(baseDate); endRange.setHours(23, 59, 59, 999); } else if (view === 'Week' || view === 'WorkWeek') { const day = baseDate.getDay(); const diffToMonday = (day + 6) % 7; // Monday=0 startRange = new Date(baseDate); startRange.setDate(baseDate.getDate() - diffToMonday); startRange.setHours(0, 0, 0, 0); endRange = new Date(startRange); endRange.setDate(startRange.getDate() + 6); endRange.setHours(23, 59, 59, 999); } else if (view === 'Month') { startRange = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1, 0, 0, 0, 0); endRange = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0, 23, 59, 59, 999); } } } if (!startRange || !endRange) { startRange = fallbackStart; endRange = fallbackEnd; } // Always request non-expanded events (expand=0) const data = await fetchEvents(selectedGroupId, showInactive, { start: startRange!, end: endRange!, expand: false, }); const mapped: Event[] = data.map((e: RawEvent) => ({ Id: e.Id, Subject: e.Subject, StartTime: parseEventDate(e.StartTime), EndTime: parseEventDate(e.EndTime), IsAllDay: e.IsAllDay, MediaId: e.MediaId, Icon: e.Icon, Type: e.Type, OccurrenceOfId: e.OccurrenceOfId, Recurrence: !!e.RecurrenceRule, RecurrenceRule: e.RecurrenceRule ?? null, RecurrenceEnd: e.RecurrenceEnd ?? null, SkipHolidays: e.SkipHolidays ?? false, RecurrenceException: e.RecurrenceException ?? undefined, recurrenceException: e.RecurrenceException ?? undefined, // for Syncfusion })); setEvents(mapped); } catch (err) { console.error('Fehler beim Laden der Termine:', err); } }, [selectedGroupId, showInactive]); React.useEffect(() => { if (selectedGroupId) { // selectedGroupId kann null sein, fetchEvents erwartet aber string fetchAndSetEvents(); } else { setEvents([]); } }, [selectedGroupId, showInactive, fetchAndSetEvents]); // Helper: prüfe, ob Zeitraum einen Feiertag/Ferienbereich schneidet const isWithinHolidayRange = React.useCallback( (start: Date, end: Date) => { // normalisiere Endzeit minimal (Syncfusion nutzt exklusive Enden in einigen Fällen) const adjEnd = new Date(end); // keine Änderung nötig – unsere eigenen Events sind präzise for (const h of holidays) { // Holiday dates are strings YYYY-MM-DD (local date) const hs = new Date(h.start_date + 'T00:00:00'); const he = new Date(h.end_date + 'T23:59:59'); if ( (start >= hs && start <= he) || (adjEnd >= hs && adjEnd <= he) || (start <= hs && adjEnd >= he) ) { return true; } } return false; }, [holidays] ); // Baue Holiday-Anzeige-Events und Block-Events const holidayDisplayEvents: Event[] = useMemo(() => { if (!showHolidayList) return []; const out: Event[] = []; for (const h of holidays) { const start = new Date(h.start_date + 'T00:00:00'); const end = new Date(h.end_date + 'T23:59:59'); out.push({ Id: `holiday-${h.id}-display`, Subject: h.name, StartTime: start, EndTime: end, IsAllDay: true, isHoliday: true, }); } return out; }, [holidays, showHolidayList]); const holidayBlockEvents: Event[] = useMemo(() => { if (allowScheduleOnHolidays) return []; const out: Event[] = []; for (const h of holidays) { const start = new Date(h.start_date + 'T00:00:00'); const end = new Date(h.end_date + 'T23:59:59'); out.push({ Id: `holiday-${h.id}-block`, Subject: h.name, StartTime: start, EndTime: end, IsAllDay: true, IsBlock: true, isHoliday: true, }); } return out; }, [holidays, allowScheduleOnHolidays]); const dataSource = useMemo(() => { // Filter: Events with SkipHolidays=true are never shown on holidays, regardless of toggle const filteredEvents = events.filter(ev => { if (ev.SkipHolidays) { // If event falls within a holiday, hide it const s = ev.StartTime instanceof Date ? ev.StartTime : new Date(ev.StartTime); const e = ev.EndTime instanceof Date ? ev.EndTime : new Date(ev.EndTime); for (const h of holidays) { const hs = new Date(h.start_date + 'T00:00:00'); const he = new Date(h.end_date + 'T23:59:59'); if ( (s >= hs && s <= he) || (e >= hs && e <= he) || (s <= hs && e >= he) ) { return false; } } } return true; }); return [...filteredEvents, ...holidayDisplayEvents, ...holidayBlockEvents]; }, [events, holidayDisplayEvents, holidayBlockEvents, holidays]); // Removed dataSource logging // Aktive akademische Periode für Datum aus dem Backend ermitteln const refreshAcademicPeriodFor = React.useCallback( async (baseDate: Date) => { try { const p = await getAcademicPeriodForDate(baseDate); if (!p) { setSchoolYearLabel(''); setHasSchoolYearPlan(false); return; } // Anzeige: bevorzugt display_name, sonst name const label = p.display_name ? p.display_name : p.name; setSchoolYearLabel(label); // Existiert ein Ferienplan innerhalb der Periode? const start = new Date(p.start_date + 'T00:00:00'); const end = new Date(p.end_date + 'T23:59:59'); let exists = false; for (const h of holidays) { const hs = new Date(h.start_date + 'T00:00:00'); const he = new Date(h.end_date + 'T23:59:59'); if (hs <= end && he >= start) { exists = true; break; } } setHasSchoolYearPlan(exists); } catch (e) { console.error('Akademische Periode laden fehlgeschlagen:', e); setSchoolYearLabel(''); setHasSchoolYearPlan(false); } }, [holidays] ); // Anzahl an Ferienzeiträumen in aktueller Ansicht ermitteln + Perioden-Indikator setzen const updateHolidaysInView = React.useCallback(() => { const inst = scheduleRef.current; if (!inst) { setHolidaysInView(0); return; } const view = inst.currentView as 'Day' | 'Week' | 'WorkWeek' | 'Month' | 'Agenda'; const baseDate = inst.selectedDate as Date; if (!baseDate) { setHolidaysInView(0); return; } let rangeStart = new Date(baseDate); let rangeEnd = new Date(baseDate); if (view === 'Day' || view === 'Agenda') { rangeStart.setHours(0, 0, 0, 0); rangeEnd.setHours(23, 59, 59, 999); } else if (view === 'Week' || view === 'WorkWeek') { const day = baseDate.getDay(); const diffToMonday = (day + 6) % 7; // Monday=0 rangeStart = new Date(baseDate); rangeStart.setDate(baseDate.getDate() - diffToMonday); rangeStart.setHours(0, 0, 0, 0); rangeEnd = new Date(rangeStart); rangeEnd.setDate(rangeStart.getDate() + 6); rangeEnd.setHours(23, 59, 59, 999); } else if (view === 'Month') { rangeStart = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1, 0, 0, 0, 0); rangeEnd = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0, 23, 59, 59, 999); } let count = 0; for (const h of holidays) { const hs = new Date(h.start_date + 'T00:00:00'); const he = new Date(h.end_date + 'T23:59:59'); const overlaps = (hs >= rangeStart && hs <= rangeEnd) || (he >= rangeStart && he <= rangeEnd) || (hs <= rangeStart && he >= rangeEnd); if (overlaps) count += 1; } setHolidaysInView(count); // Perioden-Indikator über Backend prüfen refreshAcademicPeriodFor(baseDate); }, [holidays, refreshAcademicPeriodFor]); // Aktualisiere Indikator wenn Ferien oder Ansicht (Key) wechseln React.useEffect(() => { updateHolidaysInView(); }, [holidays, updateHolidaysInView, schedulerKey]); return (

Terminmanagement

{ // <--- Typ für e ergänzt setEvents([]); // Events sofort leeren setSelectedGroupId(e.value); }} style={{}} /> {/* Akademische Periode Selector + Plan-Badge */} Periode: { const id = Number(e.value); if (!id) return; try { const updated = await setActiveAcademicPeriod(id); setActivePeriodId(updated.id); // Zum gleichen Tag/Monat (heute) innerhalb der gewählten Periode springen const today = new Date(); const targetYear = new Date(updated.start_date).getFullYear(); const target = new Date(targetYear, today.getMonth(), today.getDate(), 12, 0, 0); if (scheduleRef.current) { scheduleRef.current.selectedDate = target; scheduleRef.current.dataBind?.(); } updateHolidaysInView(); } catch (err) { console.error('Aktive Periode setzen fehlgeschlagen:', err); } }} style={{}} /> {/* School-year/period plan badge (adjacent) */} {hasSchoolYearPlan ? ( ) : ( )} {schoolYearLabel || 'Periode'}
{/* Removed expandRecurrences toggle: Syncfusion now always handles recurrence */} {/* Right-aligned indicators */}
{/* Holidays-in-view badge */} 0 ? '#ffe8cc' : '#f3f4f6', border: holidaysInView > 0 ? '1px solid #ffcf99' : '1px solid #e5e7eb', color: '#000', padding: '4px 10px', borderRadius: 16, fontSize: 12, }} > {holidaysInView > 0 ? `Ferien im Blick: ${holidaysInView}` : 'Keine Ferien in Ansicht'}
{/* Removed expandRecurrences indicator: Syncfusion now always handles recurrence */}
{ setModalOpen(false); setEditMode(false); // Editiermodus zurücksetzen }} onSave={async () => { setModalOpen(false); setEditMode(false); // Reload events using the same logic as fetchAndSetEvents await fetchAndSetEvents(); setSchedulerKey(prev => prev + 1); // <-- Key erhöhen }} initialData={modalInitialData} groupName={groups.find(g => g.id === selectedGroupId) ?? { id: selectedGroupId, name: '' }} groupColor={selectedGroupId ? getGroupColor(selectedGroupId, groups) : undefined} editMode={editMode} // NEU: Prop für Editiermodus blockHolidays={!allowScheduleOnHolidays} isHolidayRange={(s, e) => isWithinHolidayRange(s, e)} /> { updateHolidaysInView(); // Bei Navigation oder Viewwechsel Events erneut laden (für Range-basierte Expansion) if (args && (args.requestType === 'dateNavigate' || args.requestType === 'viewNavigate')) { fetchAndSetEvents(); } }} cellClick={args => { if (!allowScheduleOnHolidays && isWithinHolidayRange(args.startTime, args.endTime)) { args.cancel = true; return; // block creation on holidays } // args.startTime und args.endTime sind Date-Objekte args.cancel = true; // Verhindert die Standardaktion setModalInitialData({ startDate: args.startTime, startTime: args.startTime, endTime: args.endTime, }); setEditMode(false); // NEU: kein Editiermodus setModalOpen(true); }} popupOpen={async args => { if (args.type === 'Editor') { args.cancel = true; const event = args.data; // Removed event logging let media = null; if (event.MediaId) { try { const mediaData = await fetchMediaById(event.MediaId); media = { id: mediaData.id, path: mediaData.file_path, name: mediaData.name || mediaData.url, }; } catch (err) { console.error('Fehler beim Laden der Mediainfos:', err); } } // Parse recurrence info if present (supports FREQ=WEEKLY;BYDAY=...;[UNTIL=...]) let repeat = false; let weekdays: number[] = []; let repeatUntil: Date | null = null; const rr = (event.RecurrenceRule as string) || ''; if (rr && rr.includes('FREQ=WEEKLY')) { repeat = true; const m = rr.match(/BYDAY=([^;]+)/); if (m && m[1]) { const map: Record = { MO: 0, TU: 1, WE: 2, TH: 3, FR: 4, SA: 5, SU: 6 }; weekdays = m[1].split(',').map(s => map[s]).filter(v => v !== undefined); } const mu = rr.match(/UNTIL=([0-9TZ]+)/); if (mu && mu[1]) { // UNTIL is UTC in yyyymmddThhmmssZ form; we take date part const untilIso = mu[1] .replace(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/, '$1-$2-$3T$4:$5:$6Z'); repeatUntil = new Date(untilIso); } else if (event.RecurrenceEnd) { repeatUntil = new Date(event.RecurrenceEnd); } } setModalInitialData({ Id: event.Id, title: event.Subject, startDate: event.StartTime, startTime: event.StartTime, endTime: event.EndTime, description: event.Description ?? '', type: event.Type ?? 'presentation', repeat, weekdays, repeatUntil, skipHolidays: event.SkipHolidays ?? false, media, // Metadaten werden nur bei Bedarf geladen! slideshowInterval: event.SlideshowInterval ?? 10, websiteUrl: event.WebsiteUrl ?? '', }); // Removed modal initial data logging setEditMode(true); setModalOpen(true); } }} eventRendered={(args: EventRenderedArgs) => { // Always hide events that skip holidays when they fall on holidays, regardless of toggle if (args.data) { const ev = args.data as unknown as Partial; if (ev.SkipHolidays && !args.data.isHoliday) { const s = args.data.StartTime instanceof Date ? args.data.StartTime : new Date(args.data.StartTime); const e = args.data.EndTime instanceof Date ? args.data.EndTime : new Date(args.data.EndTime); if (isWithinHolidayRange(s, e)) { args.cancel = true; return; } } } // Blende Nicht-Ferien-Events aus, falls sie in Ferien fallen und Terminieren nicht erlaubt ist // Hide events on holidays if not allowed if (!allowScheduleOnHolidays && args.data && !args.data.isHoliday) { const s = args.data.StartTime instanceof Date ? args.data.StartTime : new Date(args.data.StartTime); const e = args.data.EndTime instanceof Date ? args.data.EndTime : new Date(args.data.EndTime); if (isWithinHolidayRange(s, e)) { args.cancel = true; return; } } if (selectedGroupId && args.data && args.data.Id) { const groupColor = getGroupColor(selectedGroupId, groups); const now = new Date(); // Vergangene Termine: Raumgruppenfarbe if (args.data.EndTime && args.data.EndTime < now) { args.element.style.backgroundColor = groupColor ? `${groupColor}` : '#f3f3f3'; args.element.style.color = '#000'; } else if (groupColor) { args.element.style.backgroundColor = groupColor; args.element.style.color = '#000'; } // Spezielle Darstellung für Ferienanzeige-Events if (args.data.isHoliday && !args.data.IsBlock) { args.element.style.backgroundColor = '#ffe8cc'; // sanftes Orange args.element.style.border = '1px solid #ffcf99'; args.element.style.color = '#000'; } // Gleiche Darstellung für Ferien-Block-Events if (args.data.isHoliday && args.data.IsBlock) { args.element.style.backgroundColor = '#ffe8cc'; args.element.style.border = '1px solid #ffcf99'; args.element.style.color = '#000'; } } }} actionBegin={async (args: ActionEventArgs) => { if (args.requestType === 'eventRemove') { // args.data ist ein Array von zu löschenden Events const toDelete = Array.isArray(args.data) ? args.data : [args.data]; for (const ev of toDelete) { try { await deleteEvent(ev.Id); // Deine API-Funktion } catch (err) { // Optional: Fehlerbehandlung console.error('Fehler beim Löschen:', err); } } // Events nach Löschen neu laden if (selectedGroupId) { fetchEvents(selectedGroupId, showInactive) .then((data: RawEvent[]) => { const mapped: Event[] = data.map((e: RawEvent) => ({ Id: e.Id, Subject: e.Subject, StartTime: parseEventDate(e.StartTime), EndTime: parseEventDate(e.EndTime), IsAllDay: e.IsAllDay, MediaId: e.MediaId, SkipHolidays: e.SkipHolidays ?? false, })); setEvents(mapped); }) .catch(console.error); } // Syncfusion soll das Event nicht selbst löschen args.cancel = true; } else if ( (args.requestType === 'eventCreate' || args.requestType === 'eventChange') && !allowScheduleOnHolidays ) { // Verhindere Erstellen/Ändern in Ferienbereichen (Failsafe, falls eingebauter Editor genutzt wird) type PartialEventLike = { StartTime?: Date | string; EndTime?: Date | string }; const raw = (args as ActionEventArgs).data as | PartialEventLike | PartialEventLike[] | undefined; const data = Array.isArray(raw) ? raw[0] : raw; if (data && data.StartTime && data.EndTime) { const s = data.StartTime instanceof Date ? data.StartTime : new Date(data.StartTime); const e = data.EndTime instanceof Date ? data.EndTime : new Date(data.EndTime); if (isWithinHolidayRange(s, e)) { args.cancel = true; return; } } } }} firstDayOfWeek={1} renderCell={(args: RenderCellEventArgs) => { // Nur für Arbeitszellen (Stunden-/Tageszellen) if (args.elementType === 'workCells') { const now = new Date(); // args.element ist vom Typ Element, daher als HTMLElement casten: const cell = args.element as HTMLElement; if (args.date && args.date < now) { cell.style.backgroundColor = '#fff9e3'; // Hellgelb für Vergangenheit cell.style.opacity = '0.7'; } } }} >
); }; export default Appointments;