group color shown in CustomEventModal
add functionality for edit of events
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user