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');
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');
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 { 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 = () => {
<button
className="e-btn e-success mb-4"
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);
}}
>
@@ -164,19 +189,22 @@ const Appointments: React.FC = () => {
</button>
<CustomEventModal
open={modalOpen}
onClose={() => 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
/>
<ScheduleComponent
key={schedulerKey} // <-- dynamischer Key
@@ -201,8 +231,63 @@ const Appointments: React.FC = () => {
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);
})

View File

@@ -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<CustomEventData>;
initialData?: Partial<CustomEventData> & { 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<CustomEventModalProps> = ({
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<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);
// --- 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<number>(10);
const [websiteUrl, setWebsiteUrl] = React.useState<string>('');
const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
initialData.slideshowInterval ?? 10
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
React.useEffect(() => {
@@ -81,15 +95,16 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
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<CustomEventModalProps> = ({
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<CustomEventModalProps> = ({
}
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
res = await res.json();
}
if (res.success) {
onSave(payload);
onClose();
onClose(); // <--- Box nach erfolgreichem Speichern schließen
} else {
const err = await res.json();
setErrors({ api: err.error || 'Fehler beim Speichern' });
setErrors({ api: res.error || 'Fehler beim Speichern' });
}
} catch {
setErrors({ api: 'Netzwerkfehler beim Speichern' });
@@ -199,10 +219,21 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
visible={open}
width="800px"
header={() => (
<div>
Neuen Termin anlegen
<div
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 && (
<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>
</span>
)}

View File

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