group color shown in CustomEventModal

add functionality for edit of events
This commit is contained in:
2025-07-24 07:43:32 +00:00
parent b0e933e895
commit 4e6451ce80
7 changed files with 215 additions and 30 deletions

View File

@@ -97,3 +97,9 @@ export async function deleteClient(uuid: string) {
if (!res.ok) throw new Error('Fehler beim Entfernen des Clients'); if (!res.ok) throw new Error('Fehler beim Entfernen des Clients');
return await res.json(); 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();
}

View File

@@ -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'); if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Löschen des Termins');
return data; 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;
}

View File

@@ -18,6 +18,7 @@ import { fetchGroups } from './apiGroups';
import { getGroupColor } from './groupColors'; import { getGroupColor } from './groupColors';
import { deleteEvent } from './apiEvents'; import { deleteEvent } from './apiEvents';
import CustomEventModal from './components/CustomEventModal'; import CustomEventModal from './components/CustomEventModal';
import { fetchMediaById } from './apiClients';
// Typ für Gruppe ergänzen // Typ für Gruppe ergänzen
type Group = { type Group = {
@@ -25,13 +26,16 @@ type Group = {
name: string; name: string;
}; };
// Typ für Event hinzufügen // Typ für Event ergänzen
type Event = { type Event = {
Id: string; Id: string;
Subject: string; Subject: string;
StartTime: Date; StartTime: Date;
EndTime: Date; EndTime: Date;
IsAllDay: boolean; IsAllDay: boolean;
MediaId?: string | number; // Nur die MediaId wird gespeichert!
SlideshowInterval?: number;
WebsiteUrl?: string;
}; };
type RawEvent = { type RawEvent = {
@@ -40,6 +44,9 @@ type RawEvent = {
StartTime: string; StartTime: string;
EndTime: string; EndTime: string;
IsAllDay: boolean; 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'; 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 [modalOpen, setModalOpen] = useState(false);
const [modalInitialData, setModalInitialData] = useState({}); const [modalInitialData, setModalInitialData] = useState({});
const [schedulerKey, setSchedulerKey] = useState(0); const [schedulerKey, setSchedulerKey] = useState(0);
const [editMode, setEditMode] = useState(false); // NEU: Editiermodus
// Gruppen laden // Gruppen laden
useEffect(() => { useEffect(() => {
@@ -122,6 +130,7 @@ const Appointments: React.FC = () => {
StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'), StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'),
EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'), EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'),
IsAllDay: e.IsAllDay, IsAllDay: e.IsAllDay,
MediaId: e.MediaId,
})); }));
setEvents(mapped); setEvents(mapped);
}) })
@@ -156,7 +165,23 @@ const Appointments: React.FC = () => {
<button <button
className="e-btn e-success mb-4" className="e-btn e-success mb-4"
onClick={() => { onClick={() => {
setModalInitialData({}); const now = new Date();
// Runde auf die nächste halbe Stunde
const minutes = now.getMinutes();
const roundedMinutes = minutes < 30 ? 30 : 0;
const startTime = new Date(now);
startTime.setMinutes(roundedMinutes, 0, 0);
if (roundedMinutes === 0) startTime.setHours(startTime.getHours() + 1);
const endTime = new Date(startTime);
endTime.setMinutes(endTime.getMinutes() + 30);
setModalInitialData({
startDate: startTime,
startTime: startTime,
endTime: endTime,
});
setEditMode(false);
setModalOpen(true); setModalOpen(true);
}} }}
> >
@@ -164,19 +189,22 @@ const Appointments: React.FC = () => {
</button> </button>
<CustomEventModal <CustomEventModal
open={modalOpen} open={modalOpen}
onClose={() => setModalOpen(false)} onClose={() => {
setModalOpen(false);
setEditMode(false); // Editiermodus zurücksetzen
}}
onSave={async () => { onSave={async () => {
setModalOpen(false); setModalOpen(false);
console.log('onSave wird aufgerufen'); setEditMode(false);
if (selectedGroupId) { if (selectedGroupId) {
const data = await fetchEvents(selectedGroupId); const data = await fetchEvents(selectedGroupId);
console.log('Events nach Save:', data);
const mapped: Event[] = data.map((e: RawEvent) => ({ const mapped: Event[] = data.map((e: RawEvent) => ({
Id: e.Id, Id: e.Id,
Subject: e.Subject, Subject: e.Subject,
StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'), StartTime: new Date(e.StartTime.endsWith('Z') ? e.StartTime : e.StartTime + 'Z'),
EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'), EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'),
IsAllDay: e.IsAllDay, IsAllDay: e.IsAllDay,
MediaId: e.MediaId,
})); }));
setEvents(mapped); setEvents(mapped);
setSchedulerKey(prev => prev + 1); // <-- Key erhöhen setSchedulerKey(prev => prev + 1); // <-- Key erhöhen
@@ -184,6 +212,8 @@ const Appointments: React.FC = () => {
}} }}
initialData={modalInitialData} initialData={modalInitialData}
groupName={groups.find(g => g.id === selectedGroupId) ?? { id: selectedGroupId, name: '' }} groupName={groups.find(g => g.id === selectedGroupId) ?? { id: selectedGroupId, name: '' }}
groupColor={selectedGroupId ? getGroupColor(selectedGroupId, groups) : undefined}
editMode={editMode} // NEU: Prop für Editiermodus
/> />
<ScheduleComponent <ScheduleComponent
key={schedulerKey} // <-- dynamischer Key key={schedulerKey} // <-- dynamischer Key
@@ -201,8 +231,63 @@ const Appointments: React.FC = () => {
startTime: args.startTime, startTime: args.startTime,
endTime: args.endTime, endTime: args.endTime,
}); });
setEditMode(false); // NEU: kein Editiermodus
setModalOpen(true); 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) => { eventRendered={(args: EventRenderedArgs) => {
if (selectedGroupId && args.data && args.data.Id) { if (selectedGroupId && args.data && args.data.Id) {
const groupColor = getGroupColor(selectedGroupId, groups); const groupColor = getGroupColor(selectedGroupId, groups);
@@ -233,6 +318,7 @@ const Appointments: React.FC = () => {
StartTime: new Date(e.StartTime), StartTime: new Date(e.StartTime),
EndTime: new Date(e.EndTime), EndTime: new Date(e.EndTime),
IsAllDay: e.IsAllDay, IsAllDay: e.IsAllDay,
MediaId: e.MediaId,
})); }));
setEvents(mapped); setEvents(mapped);
}) })

View File

@@ -5,6 +5,7 @@ import { DatePickerComponent, TimePickerComponent } from '@syncfusion/ej2-react-
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'; import CustomSelectUploadEventModal from './CustomSelectUploadEventModal';
import { updateEvent } from '../apiEvents';
type CustomEventData = { type CustomEventData = {
title: string; title: string;
@@ -17,14 +18,20 @@ type CustomEventData = {
weekdays: number[]; weekdays: number[];
repeatUntil: Date | null; repeatUntil: Date | null;
skipHolidays: boolean; 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 = { type CustomEventModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (eventData: CustomEventData) => void; onSave: (eventData: CustomEventData) => void;
initialData?: Partial<CustomEventData>; initialData?: Partial<CustomEventData> & { Id?: string }; // <--- Id ergänzen
groupName: string | { id: string | null; name: string }; groupName: string | { id: string | null; name: string };
groupColor?: string;
editMode?: boolean;
}; };
const weekdayOptions = [ const weekdayOptions = [
@@ -50,7 +57,9 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
onClose, onClose,
onSave, onSave,
initialData = {}, initialData = {},
groupName, // <--- NEU groupName,
groupColor,
editMode,
}) => { }) => {
const [title, setTitle] = React.useState(initialData.title || ''); const [title, setTitle] = React.useState(initialData.title || '');
const [startDate, setStartDate] = React.useState(initialData.startDate || null); const [startDate, setStartDate] = React.useState(initialData.startDate || null);
@@ -65,14 +74,19 @@ 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); // --- 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<{ const [pendingMedia, setPendingMedia] = React.useState<{
id: string; id: string;
path: string; path: string;
name: string; name: string;
} | null>(null); } | null>(null);
const [slideshowInterval, setSlideshowInterval] = React.useState<number>(10); const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
const [websiteUrl, setWebsiteUrl] = React.useState<string>(''); initialData.slideshowInterval ?? 10
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false); const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
@@ -81,15 +95,16 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
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 ?? 'presentation'); // Immer 'presentation' als Default setType(initialData.type ?? 'presentation');
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); // --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen ---
setSlideshowInterval(10); setMedia(initialData.media ?? null);
setWebsiteUrl(''); setSlideshowInterval(initialData.slideshowInterval ?? 10);
setWebsiteUrl(initialData.websiteUrl ?? '');
} }
}, [open, initialData]); }, [open, initialData]);
@@ -124,10 +139,8 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
setErrors({}); setErrors({});
// group_id ist jetzt wirklich die ID (z.B. als prop übergeben)
const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName; const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName;
// Daten für das Backend zusammenstellen
const payload: CustomEventData & { [key: string]: unknown } = { const payload: CustomEventData & { [key: string]: unknown } = {
group_id, group_id,
title, title,
@@ -175,17 +188,24 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
} }
try { try {
const res = await fetch('/api/events', { let res;
if (editMode && initialData && typeof initialData.Id === 'string') {
// UPDATE statt CREATE
res = await updateEvent(initialData.Id, payload);
} else {
// CREATE
res = await fetch('/api/events', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (res.ok) { res = await res.json();
}
if (res.success) {
onSave(payload); onSave(payload);
onClose(); onClose(); // <--- Box nach erfolgreichem Speichern schließen
} else { } else {
const err = await res.json(); setErrors({ api: res.error || 'Fehler beim Speichern' });
setErrors({ api: err.error || 'Fehler beim Speichern' });
} }
} catch { } catch {
setErrors({ api: 'Netzwerkfehler beim Speichern' }); setErrors({ api: 'Netzwerkfehler beim Speichern' });
@@ -199,10 +219,21 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
visible={open} visible={open}
width="800px" width="800px"
header={() => ( header={() => (
<div> <div
Neuen Termin anlegen style={{
background: groupColor || '#f5f5f5',
padding: '8px 16px',
borderRadius: '6px 6px 0 0',
color: '#fff',
fontWeight: 600,
fontSize: 20,
display: 'flex',
alignItems: 'center',
}}
>
{editMode ? 'Termin bearbeiten' : 'Neuen Termin anlegen'}
{groupName && ( {groupName && (
<span style={{ fontWeight: 400, fontSize: 16, marginLeft: 16, color: '#888' }}> <span style={{ fontWeight: 400, fontSize: 16, marginLeft: 16, color: '#fff' }}>
für Raumgruppe: <b>{typeof groupName === 'object' ? groupName.name : groupName}</b> für Raumgruppe: <b>{typeof groupName === 'object' ? groupName.name : groupName}</b>
</span> </span>
)} )}

View File

@@ -163,3 +163,21 @@ def find_by_filename():
'file_path': media.file_path, 'file_path': media.file_path,
'url': media.url 'url': media.url
}) })
@eventmedia_bp.route('/<int:media_id>', 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)

View File

@@ -27,11 +27,14 @@ def get_events():
for e in events: for e in events:
result.append({ result.append({
"Id": str(e.id), "Id": str(e.id),
"GroupId": e.group_id, # geändert: gibt group_id zurück "GroupId": e.group_id,
"Subject": e.title, "Subject": e.title,
"StartTime": e.start.isoformat() if e.start else None, "StartTime": e.start.isoformat() if e.start else None,
"EndTime": e.end.isoformat() if e.end else None, "EndTime": e.end.isoformat() if e.end else None,
"IsAllDay": False, "IsAllDay": False,
"MediaId": e.event_media_id, # <--- Media-ID zurückgeben
"SlideshowInterval": e.slideshow_interval,
"WebsiteUrl": None, # Optional: für Website-Typ
}) })
session.close() session.close()
return jsonify(result) return jsonify(result)
@@ -115,3 +118,29 @@ def create_event():
session.add(event) session.add(event)
session.commit() session.commit()
return jsonify({"success": True, "event_id": event.id}) return jsonify({"success": True, "event_id": event.id})
@events_bp.route("/<event_id>", 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})