Create and save custom events in database

This commit is contained in:
2025-07-14 16:58:00 +00:00
parent 16581a974f
commit 6e6e5c383a
5 changed files with 397 additions and 39 deletions

View File

@@ -97,6 +97,7 @@ const Appointments: React.FC = () => {
const [events, setEvents] = useState<Event[]>([]); const [events, setEvents] = useState<Event[]>([]);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [modalInitialData, setModalInitialData] = useState({}); const [modalInitialData, setModalInitialData] = useState({});
const [schedulerKey, setSchedulerKey] = useState(0);
// Gruppen laden // Gruppen laden
useEffect(() => { useEffect(() => {
@@ -120,8 +121,8 @@ const Appointments: React.FC = () => {
const mapped: Event[] = data.map(e => ({ const mapped: Event[] = data.map(e => ({
Id: e.Id, Id: e.Id,
Subject: e.Subject, Subject: e.Subject,
StartTime: new Date(e.StartTime), StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'),
EndTime: new Date(e.EndTime), EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'),
IsAllDay: e.IsAllDay, IsAllDay: e.IsAllDay,
})); }));
setEvents(mapped); setEvents(mapped);
@@ -166,16 +167,28 @@ const Appointments: React.FC = () => {
<CustomEventModal <CustomEventModal
open={modalOpen} open={modalOpen}
onClose={() => setModalOpen(false)} onClose={() => setModalOpen(false)}
onSave={eventData => { onSave={async eventData => {
console.log('Event-Daten zum Speichern:', eventData);
// Event speichern (API-Aufruf oder State-Update)
setModalOpen(false); 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} initialData={modalInitialData}
groupName={groups.find(g => g.id === selectedGroupId)?.name ?? ''} groupName={groups.find(g => g.id === selectedGroupId) ?? { id: selectedGroupId, name: '' }}
/> />
<ScheduleComponent <ScheduleComponent
key={selectedGroupId} key={schedulerKey} // <-- dynamischer Key
height="750px" height="750px"
locale="de" locale="de"
currentView="Week" currentView="Week"

View File

@@ -4,6 +4,7 @@ import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-calendars'; import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-calendars';
import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns'; import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons'; import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
import CustomSelectUploadEventModal from './CustomSelectUploadEventModal';
type CustomEventData = { type CustomEventData = {
title: string; title: string;
@@ -21,9 +22,9 @@ type CustomEventData = {
type CustomEventModalProps = { type CustomEventModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (event: CustomEventData) => void; onSave: (eventData: any) => void;
initialData?: Partial<CustomEventData>; initialData?: any;
groupName?: string; // <--- NEU groupName: string | { id: string | null; name: string }; // <- angepasst
}; };
const weekdayOptions = [ const weekdayOptions = [
@@ -42,7 +43,6 @@ const typeOptions = [
{ value: 'video', label: 'Video' }, { value: 'video', label: 'Video' },
{ value: 'message', label: 'Nachricht' }, { value: 'message', label: 'Nachricht' },
{ value: 'webuntis', label: 'WebUntis' }, { value: 'webuntis', label: 'WebUntis' },
{ value: 'other', label: 'Sonstiges' },
]; ];
const CustomEventModal: React.FC<CustomEventModalProps> = ({ const CustomEventModal: React.FC<CustomEventModalProps> = ({
@@ -65,30 +65,57 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
const [repeatUntil, setRepeatUntil] = React.useState(initialData.repeatUntil || null); const [repeatUntil, setRepeatUntil] = React.useState(initialData.repeatUntil || null);
const [skipHolidays, setSkipHolidays] = React.useState(initialData.skipHolidays || false); const [skipHolidays, setSkipHolidays] = React.useState(initialData.skipHolidays || false);
const [errors, setErrors] = React.useState<{ [key: string]: string }>({}); 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<number>(10);
const [websiteUrl, setWebsiteUrl] = React.useState<string>('');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (open && initialData) { if (open) {
setTitle(initialData.title || ''); setTitle(initialData.title || '');
setStartDate(initialData.startDate || null); setStartDate(initialData.startDate || null);
setStartTime(initialData.startTime || new Date(0, 0, 0, 9, 0)); setStartTime(initialData.startTime || new Date(0, 0, 0, 9, 0));
setEndTime(initialData.endTime || new Date(0, 0, 0, 9, 30)); setEndTime(initialData.endTime || new Date(0, 0, 0, 9, 30));
setType(initialData.type || ''); setType(initialData.type ?? 'presentation'); // Immer 'presentation' als Default
setDescription(initialData.description || ''); setDescription(initialData.description || '');
setRepeat(initialData.repeat || false); setRepeat(initialData.repeat || false);
setWeekdays(initialData.weekdays || []); setWeekdays(initialData.weekdays || []);
setRepeatUntil(initialData.repeatUntil || null); setRepeatUntil(initialData.repeatUntil || null);
setSkipHolidays(initialData.skipHolidays || false); setSkipHolidays(initialData.skipHolidays || false);
setMedia(null);
setSlideshowInterval(10);
setWebsiteUrl('');
} }
}, [open, initialData]); }, [open, initialData]);
const handleSave = () => { React.useEffect(() => {
if (!mediaModalOpen && pendingMedia) {
setMedia(pendingMedia);
setPendingMedia(null);
}
}, [mediaModalOpen, pendingMedia]);
const handleSave = async () => {
const newErrors: { [key: string]: string } = {}; const newErrors: { [key: string]: string } = {};
if (!title.trim()) newErrors.title = 'Titel ist erforderlich'; if (!title.trim()) newErrors.title = 'Titel ist erforderlich';
if (!startDate) newErrors.startDate = 'Startdatum ist erforderlich'; if (!startDate) newErrors.startDate = 'Startdatum ist erforderlich';
if (!startTime) newErrors.startTime = 'Startzeit ist erforderlich'; if (!startTime) newErrors.startTime = 'Startzeit ist erforderlich';
if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich'; if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich';
if (!type) newErrors.type = 'Termintyp 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) { if (Object.keys(newErrors).length > 0) {
setErrors(newErrors); setErrors(newErrors);
@@ -96,22 +123,71 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
} }
setErrors({}); 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, title,
startDate,
startTime,
endTime,
type,
description, description,
repeat, start:
weekdays, startDate && startTime
repeatUntil, ? new Date(
skipHolidays, 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 ( return (
<DialogComponent <DialogComponent
target="#root"
visible={open} visible={open}
width="800px" width="800px"
header={() => ( header={() => (
@@ -119,7 +195,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
Neuen Termin anlegen Neuen Termin anlegen
{groupName && ( {groupName && (
<span style={{ fontWeight: 400, fontSize: 16, marginLeft: 16, color: '#888' }}> <span style={{ fontWeight: 400, fontSize: 16, marginLeft: 16, color: '#888' }}>
für Raumgruppe: <b>{groupName}</b> für Raumgruppe: <b>{typeof groupName === 'object' ? groupName.name : groupName}</b>
</span> </span>
)} )}
</div> </div>
@@ -141,6 +217,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
<div style={{ padding: '24px' }}> <div style={{ padding: '24px' }}>
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 260 }}> <div style={{ flex: 1, minWidth: 260 }}>
{/* ...Titel, Beschreibung, Datum, Zeit... */}
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<TextBoxComponent <TextBoxComponent
placeholder="Titel" placeholder="Titel"
@@ -196,18 +273,9 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
)} )}
</div> </div>
</div> </div>
<div style={{ marginBottom: 12 }}>
<DropDownListComponent
dataSource={typeOptions}
fields={{ text: 'label', value: 'value' }}
placeholder="Termintyp"
value={type}
change={e => setType(e.value as string)}
/>
{errors.type && <div style={{ color: 'red', fontSize: 12 }}>{errors.type}</div>}
</div>
</div> </div>
<div style={{ flex: 1, minWidth: 260 }}> <div style={{ flex: 1, minWidth: 260 }}>
{/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */}
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<CheckBoxComponent <CheckBoxComponent
label="Wiederholender Termin" label="Wiederholender Termin"
@@ -248,7 +316,77 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
</div> </div>
</div> </div>
</div> </div>
{/* NEUER ZWEISPALTIGER BEREICH für Termintyp und Zusatzfelder */}
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', marginTop: 8 }}>
<div style={{ flex: 1, minWidth: 260 }}>
<div style={{ marginBottom: 12, marginTop: 16 }}>
<DropDownListComponent
dataSource={typeOptions}
fields={{ text: 'label', value: 'value' }}
placeholder="Termintyp"
value={type}
change={e => setType(e.value as string)}
style={{ width: '100%' }}
/>
{errors.type && <div style={{ color: 'red', fontSize: 12 }}>{errors.type}</div>}
</div> </div>
</div>
<div style={{ flex: 1, minWidth: 260 }}>
<div style={{ marginBottom: 12, minHeight: 60 }}>
{type === 'presentation' && (
<div>
<div style={{ marginBottom: 8, marginTop: 16 }}>
<button
className="e-btn"
onClick={() => setMediaModalOpen(true)}
style={{ width: '100%' }}
>
Medium auswählen/hochladen
</button>
</div>
<div style={{ marginBottom: 8 }}>
<b>Ausgewähltes Medium:</b>{' '}
{media ? (
media.path
) : (
<span style={{ color: '#888' }}>Kein Medium ausgewählt</span>
)}
</div>
<TextBoxComponent
placeholder="Slideshow-Intervall (Sekunden)"
floatLabelType="Auto"
type="number"
value={String(slideshowInterval)}
change={e => setSlideshowInterval(Number(e.value))}
/>
</div>
)}
{type === 'website' && (
<div>
<TextBoxComponent
placeholder="Webseiten-URL"
floatLabelType="Always"
value={websiteUrl}
change={e => setWebsiteUrl(e.value)}
/>
</div>
)}
</div>
</div>
</div>
</div>
{mediaModalOpen && (
<CustomSelectUploadEventModal
open={mediaModalOpen}
onClose={() => setMediaModalOpen(false)}
onSelect={({ id, path, name }) => {
setPendingMedia({ id, path, name });
setMediaModalOpen(false);
}}
selectedFileId={null}
/>
)}
</DialogComponent> </DialogComponent>
); );
}; };

View File

@@ -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<CustomSelectUploadEventModalProps> = 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 (
<DialogComponent
target="#root"
visible={open}
width="700px"
header="Medium auswählen/hochladen"
showCloseIcon={true}
close={onClose}
isModal={true}
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button className="e-btn" onClick={onClose}>
Abbrechen
</button>
<button className="e-btn e-primary" disabled={!selectedFile} onClick={handleSelectClick}>
Auswählen
</button>
</div>
)}
>
<FileManagerComponent
ajaxSettings={{
url: hostUrl + 'operations',
getImageUrl: hostUrl + 'get-image',
uploadUrl: hostUrl + 'upload',
downloadUrl: hostUrl + 'download',
}}
toolbarSettings={{
items: [
'NewFolder',
'Upload',
'Download',
'Rename',
'Delete',
'SortBy',
'Refresh',
'Details',
],
}}
contextMenuSettings={{
file: ['Open', '|', 'Download', '|', 'Rename', 'Delete', '|', 'Details'],
folder: ['Open', '|', 'Rename', 'Delete', '|', 'Details'],
layout: ['SortBy', 'Refresh', '|', 'View', 'Details'],
}}
allowMultiSelection={false}
fileSelect={handleFileSelect}
>
<Inject services={[NavigationPane, DetailsView, Toolbar]} />
</FileManagerComponent>
</DialogComponent>
);
};
export default CustomSelectUploadEventModal;

View File

@@ -143,3 +143,23 @@ def update_media(media_id):
# Event-Zuordnung ggf. ergänzen # Event-Zuordnung ggf. ergänzen
session.commit() session.commit()
return jsonify(media.to_dict()) 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
})

View File

@@ -1,7 +1,8 @@
from database import Session
from sqlalchemy import and_
from models import Event
from flask import Blueprint, request, jsonify 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 import sys
sys.path.append('/workspace') sys.path.append('/workspace')
@@ -47,3 +48,70 @@ def delete_event(event_id):
session.commit() session.commit()
session.close() session.close()
return jsonify({"success": True}) 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})