From 6e6e5c383a4d9c76167591f4505ae86dc667188a Mon Sep 17 00:00:00 2001 From: olaf Date: Mon, 14 Jul 2025 16:58:00 +0000 Subject: [PATCH] Create and save custom events in database --- dashboard/src/appointments.tsx | 27 ++- dashboard/src/components/CustomEventModal.tsx | 196 +++++++++++++++--- .../CustomSelectUploadEventModal.tsx | 119 +++++++++++ server/routes/eventmedia.py | 20 ++ server/routes/events.py | 74 ++++++- 5 files changed, 397 insertions(+), 39 deletions(-) create mode 100644 dashboard/src/components/CustomSelectUploadEventModal.tsx diff --git a/dashboard/src/appointments.tsx b/dashboard/src/appointments.tsx index bf752f6..63d5a7f 100644 --- a/dashboard/src/appointments.tsx +++ b/dashboard/src/appointments.tsx @@ -97,6 +97,7 @@ const Appointments: React.FC = () => { const [events, setEvents] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [modalInitialData, setModalInitialData] = useState({}); + const [schedulerKey, setSchedulerKey] = useState(0); // Gruppen laden useEffect(() => { @@ -120,8 +121,8 @@ const Appointments: React.FC = () => { const mapped: Event[] = data.map(e => ({ Id: e.Id, Subject: e.Subject, - StartTime: new Date(e.StartTime), - EndTime: new Date(e.EndTime), + 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, })); setEvents(mapped); @@ -166,16 +167,28 @@ const Appointments: React.FC = () => { setModalOpen(false)} - onSave={eventData => { - console.log('Event-Daten zum Speichern:', eventData); - // Event speichern (API-Aufruf oder State-Update) + onSave={async eventData => { setModalOpen(false); + console.log('onSave wird aufgerufen'); + if (selectedGroupId) { + const data = await fetchEvents(selectedGroupId); + console.log('Events nach Save:', data); + 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, + })); + setEvents(mapped); + setSchedulerKey(prev => prev + 1); // <-- Key erhöhen + } }} initialData={modalInitialData} - groupName={groups.find(g => g.id === selectedGroupId)?.name ?? ''} + groupName={groups.find(g => g.id === selectedGroupId) ?? { id: selectedGroupId, name: '' }} /> void; - onSave: (event: CustomEventData) => void; - initialData?: Partial; - groupName?: string; // <--- NEU + onSave: (eventData: any) => void; + initialData?: any; + groupName: string | { id: string | null; name: string }; // <- angepasst }; const weekdayOptions = [ @@ -42,7 +43,6 @@ const typeOptions = [ { value: 'video', label: 'Video' }, { value: 'message', label: 'Nachricht' }, { value: 'webuntis', label: 'WebUntis' }, - { value: 'other', label: 'Sonstiges' }, ]; const CustomEventModal: React.FC = ({ @@ -65,30 +65,57 @@ const CustomEventModal: React.FC = ({ const [repeatUntil, setRepeatUntil] = React.useState(initialData.repeatUntil || null); const [skipHolidays, setSkipHolidays] = React.useState(initialData.skipHolidays || false); const [errors, setErrors] = React.useState<{ [key: string]: string }>({}); + const [media, setMedia] = React.useState<{ id: string; path: string; name: string } | null>(null); + const [pendingMedia, setPendingMedia] = React.useState<{ + id: string; + path: string; + name: string; + } | null>(null); + const [slideshowInterval, setSlideshowInterval] = React.useState(10); + const [websiteUrl, setWebsiteUrl] = React.useState(''); + const [mediaModalOpen, setMediaModalOpen] = React.useState(false); React.useEffect(() => { - if (open && initialData) { + if (open) { 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 || ''); + setType(initialData.type ?? 'presentation'); // Immer 'presentation' als Default setDescription(initialData.description || ''); setRepeat(initialData.repeat || false); setWeekdays(initialData.weekdays || []); setRepeatUntil(initialData.repeatUntil || null); setSkipHolidays(initialData.skipHolidays || false); + setMedia(null); + setSlideshowInterval(10); + setWebsiteUrl(''); } }, [open, initialData]); - const handleSave = () => { + React.useEffect(() => { + if (!mediaModalOpen && pendingMedia) { + setMedia(pendingMedia); + setPendingMedia(null); + } + }, [mediaModalOpen, pendingMedia]); + + const handleSave = async () => { 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'; - // ggf. weitere Felder prüfen + + 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 (Object.keys(newErrors).length > 0) { setErrors(newErrors); @@ -96,22 +123,71 @@ const CustomEventModal: React.FC = ({ } setErrors({}); - onSave({ + + // group_id ist jetzt wirklich die ID (z.B. als prop übergeben) + const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName; + + // Daten für das Backend zusammenstellen + const payload: any = { + group_id, title, - startDate, - startTime, - endTime, - type, description, - repeat, - weekdays, - repeatUntil, - skipHolidays, - }); + 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, + event_type: type, + is_active: 1, + created_by: 1, + }; + + if (type === 'presentation') { + payload.event_media_id = media?.id; + payload.slideshow_interval = slideshowInterval; + } + + if (type === 'website') { + payload.website_url = websiteUrl; + } + + try { + const res = await fetch('/api/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (res.ok) { + onSave(payload); // <--- HIER ergänzen! + onClose(); + } else { + const err = await res.json(); + setErrors({ api: err.error || 'Fehler beim Speichern' }); + } + } catch { + setErrors({ api: 'Netzwerkfehler beim Speichern' }); + } + console.log('handleSave called'); }; return ( ( @@ -119,7 +195,7 @@ const CustomEventModal: React.FC = ({ Neuen Termin anlegen {groupName && ( - für Raumgruppe: {groupName} + für Raumgruppe: {typeof groupName === 'object' ? groupName.name : groupName} )} @@ -141,6 +217,7 @@ const CustomEventModal: React.FC = ({
+ {/* ...Titel, Beschreibung, Datum, Zeit... */}
= ({ )}
-
- setType(e.value as string)} - /> - {errors.type &&
{errors.type}
} -
+ {/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */}
= ({
+ + {/* 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 + )} +
+ setSlideshowInterval(Number(e.value))} + /> +
+ )} + {type === 'website' && ( +
+ setWebsiteUrl(e.value)} + /> +
+ )} +
+
+
+ {mediaModalOpen && ( + setMediaModalOpen(false)} + onSelect={({ id, path, name }) => { + setPendingMedia({ id, path, name }); + setMediaModalOpen(false); + }} + selectedFileId={null} + /> + )}
); }; diff --git a/dashboard/src/components/CustomSelectUploadEventModal.tsx b/dashboard/src/components/CustomSelectUploadEventModal.tsx new file mode 100644 index 0000000..7a3f122 --- /dev/null +++ b/dashboard/src/components/CustomSelectUploadEventModal.tsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { DialogComponent } from '@syncfusion/ej2-react-popups'; +import { + FileManagerComponent, + Inject, + NavigationPane, + DetailsView, + Toolbar, +} from '@syncfusion/ej2-react-filemanager'; + +const hostUrl = '/api/eventmedia/filemanager/'; + +type CustomSelectUploadEventModalProps = { + open: boolean; + onClose: () => void; + onSelect: (file: { id: string; path: string; name: string }) => void; // name ergänzt + selectedFileId?: string | null; +}; + +const CustomSelectUploadEventModal: React.FC = props => { + const { open, onClose, onSelect } = props; + + const [selectedFile, setSelectedFile] = useState<{ + id: string; + path: string; + name: string; + } | null>(null); + + // Callback für Dateiauswahl + interface FileSelectEventArgs { + fileDetails: { + name: string; + isFile: boolean; + size: number; + // weitere Felder falls benötigt + }; + } + + const handleFileSelect = async (args: FileSelectEventArgs) => { + if (args.fileDetails.isFile && args.fileDetails.size > 0) { + const filename = args.fileDetails.name; + + try { + const response = await fetch( + `/api/eventmedia/find_by_filename?filename=${encodeURIComponent(filename)}` + ); + if (response.ok) { + const data = await response.json(); + setSelectedFile({ id: data.id, path: data.file_path, name: filename }); + } else { + setSelectedFile({ id: filename, path: filename, name: filename }); + } + } catch (e) { + console.error('Error fetching file details:', e); + } + } + }; + + // Button-Handler + const handleSelectClick = () => { + if (selectedFile) { + onSelect(selectedFile); + } + }; + + return ( + ( +
+ + +
+ )} + > + + + +
+ ); +}; + +export default CustomSelectUploadEventModal; diff --git a/server/routes/eventmedia.py b/server/routes/eventmedia.py index 52e2fea..6044d25 100644 --- a/server/routes/eventmedia.py +++ b/server/routes/eventmedia.py @@ -143,3 +143,23 @@ def update_media(media_id): # Event-Zuordnung ggf. ergänzen session.commit() return jsonify(media.to_dict()) + + +@eventmedia_bp.route('/find_by_filename', methods=['GET']) +def find_by_filename(): + filename = request.args.get('filename') + if not filename: + return jsonify({'error': 'Missing filename'}), 400 + session = Session() + # Suche nach exaktem Dateinamen in url oder file_path + media = session.query(EventMedia).filter( + (EventMedia.url == filename) | ( + EventMedia.file_path.like(f"%{filename}")) + ).first() + if not media: + return jsonify({'error': 'Not found'}), 404 + return jsonify({ + 'id': media.id, + 'file_path': media.file_path, + 'url': media.url + }) diff --git a/server/routes/events.py b/server/routes/events.py index 93b57c5..233cbc7 100644 --- a/server/routes/events.py +++ b/server/routes/events.py @@ -1,7 +1,8 @@ -from database import Session -from sqlalchemy import and_ -from models import Event from flask import Blueprint, request, jsonify +from database import Session +from models import Event, EventMedia, MediaType +from datetime import datetime, timezone +from sqlalchemy import and_ import sys sys.path.append('/workspace') @@ -47,3 +48,70 @@ def delete_event(event_id): session.commit() session.close() return jsonify({"success": True}) + + +@events_bp.route("", methods=["POST"]) +def create_event(): + data = request.json + session = Session() + + # Pflichtfelder prüfen + required = ["group_id", "title", "description", + "start", "end", "event_type", "created_by"] + for field in required: + if field not in data: + return jsonify({"error": f"Missing field: {field}"}), 400 + + event_type = data["event_type"] + event_media_id = None + slideshow_interval = None + + # Präsentation: event_media_id und slideshow_interval übernehmen + if event_type == "presentation": + event_media_id = data.get("event_media_id") + slideshow_interval = data.get("slideshow_interval") + if not event_media_id: + return jsonify({"error": "event_media_id required for presentation"}), 400 + + # Website: Webseite als EventMedia anlegen und ID übernehmen + if event_type == "website": + website_url = data.get("website_url") + if not website_url: + return jsonify({"error": "website_url required for website"}), 400 + # EventMedia für Webseite anlegen + media = EventMedia( + media_type=MediaType.website, + url=website_url, + file_path=website_url + ) + session.add(media) + session.commit() + event_media_id = media.id + + # created_by aus den Daten holen, Default: None + created_by = data.get("created_by") + + # Start- und Endzeit in UTC umwandeln, falls kein Zulu-Zeitstempel + start = datetime.fromisoformat(data["start"]) + end = datetime.fromisoformat(data["end"]) + if start.tzinfo is None: + start = start.astimezone(timezone.utc) + if end.tzinfo is None: + end = end.astimezone(timezone.utc) + + # Event anlegen + event = Event( + group_id=data["group_id"], + title=data["title"], + description=data["description"], + start=start, + end=end, + event_type=event_type, + is_active=True, + event_media_id=event_media_id, + slideshow_interval=slideshow_interval, + created_by=created_by # <--- HIER hinzugefügt + ) + session.add(event) + session.commit() + return jsonify({"success": True, "event_id": event.id})