From 8bbda836b31797758c05d6910b626484dfca36b6 Mon Sep 17 00:00:00 2001 From: olaf Date: Thu, 24 Jul 2025 09:35:57 +0000 Subject: [PATCH] prevent saving past events add function to show inactive events --- dashboard/src/apiEvents.ts | 6 +- dashboard/src/appointments.tsx | 74 +++++++++++++------ dashboard/src/components/CustomEventModal.tsx | 52 ++++++++++++- server/routes/events.py | 46 +++++++----- 4 files changed, 135 insertions(+), 43 deletions(-) diff --git a/dashboard/src/apiEvents.ts b/dashboard/src/apiEvents.ts index d3e79b7..e15d768 100644 --- a/dashboard/src/apiEvents.ts +++ b/dashboard/src/apiEvents.ts @@ -8,8 +8,10 @@ export interface Event { extendedProps: Record; } -export async function fetchEvents(groupId: string) { - const res = await fetch(`/api/events?group_id=${encodeURIComponent(groupId)}`); +export async function fetchEvents(groupId: string, showInactive = false) { + const res = await fetch( + `/api/events?group_id=${encodeURIComponent(groupId)}&show_inactive=${showInactive ? '1' : '0'}` + ); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden der Termine'); return data; diff --git a/dashboard/src/appointments.tsx b/dashboard/src/appointments.tsx index 6d91217..954b5d8 100644 --- a/dashboard/src/appointments.tsx +++ b/dashboard/src/appointments.tsx @@ -45,8 +45,6 @@ type RawEvent = { EndTime: string; IsAllDay: boolean; MediaId?: string | number; // Nur die MediaId wird gespeichert! - SlideshowInterval?: number; - WebsiteUrl?: string; }; import * as de from 'cldr-data/main/de/ca-gregorian.json'; @@ -106,6 +104,7 @@ const Appointments: React.FC = () => { const [modalInitialData, setModalInitialData] = useState({}); const [schedulerKey, setSchedulerKey] = useState(0); const [editMode, setEditMode] = useState(false); // NEU: Editiermodus + const [showInactive, setShowInactive] = React.useState(false); // Gruppen laden useEffect(() => { @@ -119,26 +118,36 @@ const Appointments: React.FC = () => { .catch(console.error); }, []); - // Termine für die ausgewählte Gruppe laden - useEffect(() => { + // fetchAndSetEvents als useCallback definieren, damit die Dependency korrekt ist: + const fetchAndSetEvents = React.useCallback(async () => { + if (!selectedGroupId) { + setEvents([]); + return; + } + try { + const data = await fetchEvents(selectedGroupId, showInactive); // selectedGroupId ist jetzt garantiert string + const mapped: Event[] = data.map((e: RawEvent) => ({ + Id: e.Id, + Subject: e.Subject, + StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'), + EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'), + IsAllDay: e.IsAllDay, + MediaId: e.MediaId, + })); + setEvents(mapped); + } catch (err) { + console.error(err); + } + }, [selectedGroupId, showInactive]); + + React.useEffect(() => { if (selectedGroupId) { - fetchEvents(selectedGroupId) - .then((data: RawEvent[]) => { - const mapped: Event[] = data.map(e => ({ - Id: e.Id, - Subject: e.Subject, - StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'), - EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'), - IsAllDay: e.IsAllDay, - MediaId: e.MediaId, - })); - setEvents(mapped); - }) - .catch(console.error); + // selectedGroupId kann null sein, fetchEvents erwartet aber string + fetchAndSetEvents(); } else { setEvents([]); } - }, [selectedGroupId]); + }, [selectedGroupId, showInactive, fetchAndSetEvents]); return (
@@ -155,9 +164,10 @@ const Appointments: React.FC = () => { fields={{ text: 'name', value: 'id' }} placeholder="Gruppe auswählen" value={selectedGroupId} - change={e => { + change={(e: { value: string }) => { + // <--- Typ für e ergänzt setEvents([]); // Events sofort leeren - setSelectedGroupId(e.value as string); + setSelectedGroupId(e.value); }} style={{ flex: 1 }} /> @@ -187,6 +197,17 @@ const Appointments: React.FC = () => { > Neuen Termin anlegen +
+ +
{ @@ -197,7 +218,7 @@ const Appointments: React.FC = () => { setModalOpen(false); setEditMode(false); if (selectedGroupId) { - const data = await fetchEvents(selectedGroupId); + const data = await fetchEvents(selectedGroupId, showInactive); const mapped: Event[] = data.map((e: RawEvent) => ({ Id: e.Id, Subject: e.Subject, @@ -291,8 +312,17 @@ const Appointments: React.FC = () => { eventRendered={(args: EventRenderedArgs) => { if (selectedGroupId && args.data && args.data.Id) { const groupColor = getGroupColor(selectedGroupId, groups); - if (groupColor) { + const now = new Date(); + if (args.data.EndTime && args.data.EndTime < now) { + // Vergangene Termine: Raumgruppenfarbe mit Transparenz und grauer Schrift + args.element.style.backgroundColor = groupColor + ? `${groupColor}80` // 80 = ~50% Transparenz für hex-Farben + : '#f3f3f3'; + args.element.style.color = ''; + } else if (groupColor) { + // Aktuelle/future Termine: normale Raumgruppenfarbe args.element.style.backgroundColor = groupColor; + args.element.style.color = ''; } } }} diff --git a/dashboard/src/components/CustomEventModal.tsx b/dashboard/src/components/CustomEventModal.tsx index 25f86b6..aa12c7e 100644 --- a/dashboard/src/components/CustomEventModal.tsx +++ b/dashboard/src/components/CustomEventModal.tsx @@ -123,6 +123,23 @@ const CustomEventModal: React.FC = ({ if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich'; if (!type) newErrors.type = 'Termintyp ist erforderlich'; + // Vergangenheitsprüfung + 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) { + newErrors.startDate = 'Termine in der Vergangenheit sind nicht erlaubt!'; + } + if (type === 'presentation') { if (!media) newErrors.media = 'Bitte eine Präsentation auswählen'; if (!slideshowInterval || slideshowInterval < 1) @@ -213,6 +230,19 @@ const CustomEventModal: React.FC = ({ console.log('handleSave called'); }; + // 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()); + return ( = ({ -
@@ -285,6 +319,22 @@ const CustomEventModal: React.FC = ({ {errors.startDate && (
{errors.startDate}
)} + {isPast && ( + + ⚠️ Termin liegt in der Vergangenheit! + + )}
diff --git a/server/routes/events.py b/server/routes/events.py index 8ca8848..d9425cd 100644 --- a/server/routes/events.py +++ b/server/routes/events.py @@ -14,28 +14,38 @@ def get_events(): session = Session() start = request.args.get("start") end = request.args.get("end") - # geändert: jetzt group_id statt client_uuid group_id = request.args.get("group_id") - query = session.query(Event).filter(Event.is_active == True) - if start and end: - query = query.filter(and_(Event.start < end, Event.end > start)) + show_inactive = request.args.get( + "show_inactive", "0") == "1" # Checkbox-Logik + + now = datetime.now(timezone.utc) + events_query = session.query(Event) if group_id: - # geändert: filter auf group_id - query = query.filter(Event.group_id == int(group_id)) - events = query.all() + events_query = events_query.filter(Event.group_id == int(group_id)) + events = events_query.all() + result = [] for e in events: - result.append({ - "Id": str(e.id), - "GroupId": e.group_id, - "Subject": e.title, - "StartTime": e.start.isoformat() if e.start else None, - "EndTime": e.end.isoformat() if e.end else None, - "IsAllDay": False, - "MediaId": e.event_media_id, # <--- Media-ID zurückgeben - "SlideshowInterval": e.slideshow_interval, - "WebsiteUrl": None, # Optional: für Website-Typ - }) + # Zeitzonen-Korrektur für e.end + if e.end and e.end.tzinfo is None: + end_dt = e.end.replace(tzinfo=timezone.utc) + else: + end_dt = e.end + + # Setze is_active auf False, wenn Termin vorbei ist + if end_dt and end_dt < now and e.is_active: + e.is_active = False + session.commit() + if show_inactive or e.is_active: + result.append({ + "Id": str(e.id), + "GroupId": e.group_id, + "Subject": e.title, + "StartTime": e.start.isoformat() if e.start else None, + "EndTime": e.end.isoformat() if e.end else None, + "IsAllDay": False, + "MediaId": e.event_media_id, + }) session.close() return jsonify(result)