From 4e6451ce8002f884b02bf53239b3e6acb33ef284 Mon Sep 17 00:00:00 2001 From: olaf Date: Thu, 24 Jul 2025 07:43:32 +0000 Subject: [PATCH] group color shown in CustomEventModal add functionality for edit of events --- dashboard/src/apiClients.ts | 6 ++ dashboard/src/apiEvents.ts | 15 +++ dashboard/src/appointments.tsx | 96 ++++++++++++++++++- dashboard/src/components/CustomEventModal.tsx | 79 ++++++++++----- dashboard/src/termine.tsx | 0 server/routes/eventmedia.py | 18 ++++ server/routes/events.py | 31 +++++- 7 files changed, 215 insertions(+), 30 deletions(-) delete mode 100644 dashboard/src/termine.tsx diff --git a/dashboard/src/apiClients.ts b/dashboard/src/apiClients.ts index e7cbf93..e9c7455 100644 --- a/dashboard/src/apiClients.ts +++ b/dashboard/src/apiClients.ts @@ -97,3 +97,9 @@ export async function deleteClient(uuid: string) { if (!res.ok) throw new Error('Fehler beim Entfernen des Clients'); return await res.json(); } + +export async function fetchMediaById(mediaId: number | string) { + const response = await fetch(`/api/eventmedia/${mediaId}`); + if (!response.ok) throw new Error('Fehler beim Laden der Mediainformationen'); + return await response.json(); +} diff --git a/dashboard/src/apiEvents.ts b/dashboard/src/apiEvents.ts index a0764d1..d3e79b7 100644 --- a/dashboard/src/apiEvents.ts +++ b/dashboard/src/apiEvents.ts @@ -23,3 +23,18 @@ export async function deleteEvent(eventId: string) { if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Löschen des Termins'); return data; } + +export interface UpdateEventPayload { + [key: string]: unknown; +} + +export async function updateEvent(eventId: string, payload: UpdateEventPayload) { + const res = await fetch(`/api/events/${encodeURIComponent(eventId)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Aktualisieren des Termins'); + return data; +} diff --git a/dashboard/src/appointments.tsx b/dashboard/src/appointments.tsx index 370bd84..6d91217 100644 --- a/dashboard/src/appointments.tsx +++ b/dashboard/src/appointments.tsx @@ -18,6 +18,7 @@ import { fetchGroups } from './apiGroups'; import { getGroupColor } from './groupColors'; import { deleteEvent } from './apiEvents'; import CustomEventModal from './components/CustomEventModal'; +import { fetchMediaById } from './apiClients'; // Typ für Gruppe ergänzen type Group = { @@ -25,13 +26,16 @@ type Group = { name: string; }; -// Typ für Event hinzufügen +// Typ für Event ergänzen type Event = { Id: string; Subject: string; StartTime: Date; EndTime: Date; IsAllDay: boolean; + MediaId?: string | number; // Nur die MediaId wird gespeichert! + SlideshowInterval?: number; + WebsiteUrl?: string; }; type RawEvent = { @@ -40,6 +44,9 @@ type RawEvent = { StartTime: string; 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'; @@ -98,6 +105,7 @@ const Appointments: React.FC = () => { const [modalOpen, setModalOpen] = useState(false); const [modalInitialData, setModalInitialData] = useState({}); const [schedulerKey, setSchedulerKey] = useState(0); + const [editMode, setEditMode] = useState(false); // NEU: Editiermodus // Gruppen laden useEffect(() => { @@ -122,6 +130,7 @@ const Appointments: React.FC = () => { 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); }) @@ -156,7 +165,23 @@ const Appointments: React.FC = () => { setModalOpen(false)} + onClose={() => { + setModalOpen(false); + setEditMode(false); // Editiermodus zurücksetzen + }} onSave={async () => { setModalOpen(false); - console.log('onSave wird aufgerufen'); + setEditMode(false); if (selectedGroupId) { const data = await fetchEvents(selectedGroupId); - console.log('Events nach Save:', data); 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); setSchedulerKey(prev => prev + 1); // <-- Key erhöhen @@ -184,6 +212,8 @@ const Appointments: React.FC = () => { }} 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 /> { 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; + console.log('Event zum Bearbeiten:', event); + 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); + } + } + setModalInitialData({ + Id: event.Id, + title: event.Subject, + startDate: event.StartTime, + startTime: event.StartTime, + endTime: event.EndTime, + description: event.Description ?? '', + type: event.Type ?? 'presentation', + repeat: event.Repeat ?? false, + weekdays: event.Weekdays ?? [], + repeatUntil: event.RepeatUntil ?? null, + skipHolidays: event.SkipHolidays ?? false, + media, // Metadaten werden nur bei Bedarf geladen! + slideshowInterval: event.SlideshowInterval ?? 10, + websiteUrl: event.WebsiteUrl ?? '', + }); + console.log('Modal initial data:', { + Id: event.Id, + title: event.Subject, + startDate: event.StartTime, + startTime: event.StartTime, + endTime: event.EndTime, + description: event.Description ?? '', + type: event.Type ?? 'presentation', + repeat: event.Repeat ?? false, + weekdays: event.Weekdays ?? [], + repeatUntil: event.RepeatUntil ?? null, + skipHolidays: event.SkipHolidays ?? false, + media, // Metadaten werden nur bei Bedarf geladen! + slideshowInterval: event.SlideshowInterval ?? 10, + websiteUrl: event.WebsiteUrl ?? '', + }); + setEditMode(true); + setModalOpen(true); + } + }} eventRendered={(args: EventRenderedArgs) => { if (selectedGroupId && args.data && args.data.Id) { const groupColor = getGroupColor(selectedGroupId, groups); @@ -233,6 +318,7 @@ const Appointments: React.FC = () => { StartTime: new Date(e.StartTime), EndTime: new Date(e.EndTime), IsAllDay: e.IsAllDay, + MediaId: e.MediaId, })); setEvents(mapped); }) diff --git a/dashboard/src/components/CustomEventModal.tsx b/dashboard/src/components/CustomEventModal.tsx index 1f5d483..25f86b6 100644 --- a/dashboard/src/components/CustomEventModal.tsx +++ b/dashboard/src/components/CustomEventModal.tsx @@ -5,6 +5,7 @@ import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react- import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns'; import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons'; import CustomSelectUploadEventModal from './CustomSelectUploadEventModal'; +import { updateEvent } from '../apiEvents'; type CustomEventData = { title: string; @@ -17,14 +18,20 @@ type CustomEventData = { weekdays: number[]; repeatUntil: Date | null; skipHolidays: boolean; + media?: { id: string; path: string; name: string } | null; // <--- ergänzt + slideshowInterval?: number; // <--- ergänzt + websiteUrl?: string; // <--- ergänzt }; +// Typ für initialData erweitern, damit Id unterstützt wird type CustomEventModalProps = { open: boolean; onClose: () => void; onSave: (eventData: CustomEventData) => void; - initialData?: Partial; + initialData?: Partial & { Id?: string }; // <--- Id ergänzen groupName: string | { id: string | null; name: string }; + groupColor?: string; + editMode?: boolean; }; const weekdayOptions = [ @@ -50,7 +57,9 @@ const CustomEventModal: React.FC = ({ onClose, onSave, initialData = {}, - groupName, // <--- NEU + groupName, + groupColor, + editMode, }) => { const [title, setTitle] = React.useState(initialData.title || ''); const [startDate, setStartDate] = React.useState(initialData.startDate || null); @@ -65,14 +74,19 @@ 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); + // --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen --- + const [media, setMedia] = React.useState<{ id: string; path: string; name: string } | null>( + initialData.media ?? 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 [slideshowInterval, setSlideshowInterval] = React.useState( + initialData.slideshowInterval ?? 10 + ); + const [websiteUrl, setWebsiteUrl] = React.useState(initialData.websiteUrl ?? ''); const [mediaModalOpen, setMediaModalOpen] = React.useState(false); React.useEffect(() => { @@ -81,15 +95,16 @@ const CustomEventModal: React.FC = ({ 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'); // Immer 'presentation' als Default + setType(initialData.type ?? 'presentation'); setDescription(initialData.description || ''); setRepeat(initialData.repeat || false); setWeekdays(initialData.weekdays || []); setRepeatUntil(initialData.repeatUntil || null); setSkipHolidays(initialData.skipHolidays || false); - setMedia(null); - setSlideshowInterval(10); - setWebsiteUrl(''); + // --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen --- + setMedia(initialData.media ?? null); + setSlideshowInterval(initialData.slideshowInterval ?? 10); + setWebsiteUrl(initialData.websiteUrl ?? ''); } }, [open, initialData]); @@ -124,10 +139,8 @@ const CustomEventModal: React.FC = ({ setErrors({}); - // 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: CustomEventData & { [key: string]: unknown } = { group_id, title, @@ -175,17 +188,24 @@ const CustomEventModal: React.FC = ({ } try { - const res = await fetch('/api/events', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - if (res.ok) { - onSave(payload); - onClose(); + let res; + if (editMode && initialData && typeof initialData.Id === 'string') { + // UPDATE statt CREATE + res = await updateEvent(initialData.Id, payload); } else { - const err = await res.json(); - setErrors({ api: err.error || 'Fehler beim Speichern' }); + // CREATE + res = await fetch('/api/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + res = await res.json(); + } + 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' }); @@ -199,10 +219,21 @@ const CustomEventModal: React.FC = ({ visible={open} width="800px" header={() => ( -
- Neuen Termin anlegen +
+ {editMode ? 'Termin bearbeiten' : 'Neuen Termin anlegen'} {groupName && ( - + für Raumgruppe: {typeof groupName === 'object' ? groupName.name : groupName} )} diff --git a/dashboard/src/termine.tsx b/dashboard/src/termine.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/server/routes/eventmedia.py b/server/routes/eventmedia.py index 4606ce5..c6e7b9f 100644 --- a/server/routes/eventmedia.py +++ b/server/routes/eventmedia.py @@ -163,3 +163,21 @@ def find_by_filename(): 'file_path': media.file_path, 'url': media.url }) + + +@eventmedia_bp.route('/', methods=['GET']) +def get_media_by_id(media_id): + session = Session() + media = session.query(EventMedia).get(media_id) + if not media: + session.close() + return jsonify({'error': 'Not found'}), 404 + result = { + 'id': media.id, + 'file_path': media.file_path, + 'url': media.url, + 'name': media.url, # oder ein anderes Feld für den Namen + 'media_type': media.media_type.name if media.media_type else None + } + session.close() + return jsonify(result) diff --git a/server/routes/events.py b/server/routes/events.py index 1badf85..8ca8848 100644 --- a/server/routes/events.py +++ b/server/routes/events.py @@ -27,11 +27,14 @@ def get_events(): for e in events: result.append({ "Id": str(e.id), - "GroupId": e.group_id, # geändert: gibt group_id zurück + "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 }) session.close() return jsonify(result) @@ -115,3 +118,29 @@ def create_event(): session.add(event) session.commit() return jsonify({"success": True, "event_id": event.id}) + + +@events_bp.route("/", methods=["PUT"]) +def update_event(event_id): + data = request.json + session = Session() + event = session.query(Event).filter_by(id=event_id).first() + if not event: + session.close() + return jsonify({"error": "Termin nicht gefunden"}), 404 + + event.title = data.get("title", event.title) + event.description = data.get("description", event.description) + event.start = datetime.fromisoformat( + data["start"]) if "start" in data else event.start + event.end = datetime.fromisoformat( + data["end"]) if "end" in data else event.end + event.event_type = data.get("event_type", event.event_type) + event.event_media_id = data.get("event_media_id", event.event_media_id) + event.slideshow_interval = data.get( + "slideshow_interval", event.slideshow_interval) + event.created_by = data.get("created_by", event.created_by) + session.commit() + event_id_return = event.id # <-- ID vor session.close() speichern! + session.close() + return jsonify({"success": True, "event_id": event_id_return})