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 [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 = () => {
<CustomEventModal
open={modalOpen}
onClose={() => 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: '' }}
/>
<ScheduleComponent
key={selectedGroupId}
key={schedulerKey} // <-- dynamischer Key
height="750px"
locale="de"
currentView="Week"

View File

@@ -4,6 +4,7 @@ import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-calendars';
import { DropDownListComponent, MultiSelectComponent } from '@syncfusion/ej2-react-dropdowns';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
import CustomSelectUploadEventModal from './CustomSelectUploadEventModal';
type CustomEventData = {
title: string;
@@ -21,9 +22,9 @@ type CustomEventData = {
type CustomEventModalProps = {
open: boolean;
onClose: () => void;
onSave: (event: CustomEventData) => void;
initialData?: Partial<CustomEventData>;
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<CustomEventModalProps> = ({
@@ -65,30 +65,57 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
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<number>(10);
const [websiteUrl, setWebsiteUrl] = React.useState<string>('');
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<CustomEventModalProps> = ({
}
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 (
<DialogComponent
target="#root"
visible={open}
width="800px"
header={() => (
@@ -119,7 +195,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
Neuen Termin anlegen
{groupName && (
<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>
)}
</div>
@@ -141,6 +217,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
<div style={{ padding: '24px' }}>
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 260 }}>
{/* ...Titel, Beschreibung, Datum, Zeit... */}
<div style={{ marginBottom: 12 }}>
<TextBoxComponent
placeholder="Titel"
@@ -196,18 +273,9 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
)}
</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 style={{ flex: 1, minWidth: 260 }}>
{/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */}
<div style={{ marginBottom: 12 }}>
<CheckBoxComponent
label="Wiederholender Termin"
@@ -248,7 +316,77 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
</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 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>
);
};

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
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
})

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 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})