Initial commit - copied workspace after database cleanup
This commit is contained in:
515
dashboard/src/components/CustomEventModal.tsx
Normal file
515
dashboard/src/components/CustomEventModal.tsx
Normal file
@@ -0,0 +1,515 @@
|
||||
import React from 'react';
|
||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||
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';
|
||||
import { updateEvent } from '../apiEvents';
|
||||
|
||||
type CustomEventData = {
|
||||
title: string;
|
||||
startDate: Date | null;
|
||||
startTime: Date | null;
|
||||
endTime: Date | null;
|
||||
type: string;
|
||||
description: string;
|
||||
repeat: boolean;
|
||||
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> & { Id?: string }; // <--- Id ergänzen
|
||||
groupName: string | { id: string | null; name: string };
|
||||
groupColor?: string;
|
||||
editMode?: boolean;
|
||||
blockHolidays?: boolean;
|
||||
isHolidayRange?: (start: Date, end: Date) => boolean;
|
||||
};
|
||||
|
||||
const weekdayOptions = [
|
||||
{ value: 0, label: 'Montag' },
|
||||
{ value: 1, label: 'Dienstag' },
|
||||
{ value: 2, label: 'Mittwoch' },
|
||||
{ value: 3, label: 'Donnerstag' },
|
||||
{ value: 4, label: 'Freitag' },
|
||||
{ value: 5, label: 'Samstag' },
|
||||
{ value: 6, label: 'Sonntag' },
|
||||
];
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'presentation', label: 'Präsentation' },
|
||||
{ value: 'website', label: 'Website' },
|
||||
{ value: 'video', label: 'Video' },
|
||||
{ value: 'message', label: 'Nachricht' },
|
||||
{ value: 'webuntis', label: 'WebUntis' },
|
||||
];
|
||||
|
||||
const CustomEventModal: React.FC<CustomEventModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
initialData = {},
|
||||
groupName,
|
||||
groupColor,
|
||||
editMode,
|
||||
blockHolidays,
|
||||
isHolidayRange,
|
||||
}) => {
|
||||
const [title, setTitle] = React.useState(initialData.title || '');
|
||||
const [startDate, setStartDate] = React.useState(initialData.startDate || null);
|
||||
const [startTime, setStartTime] = React.useState(
|
||||
initialData.startTime || new Date(0, 0, 0, 9, 0)
|
||||
);
|
||||
const [endTime, setEndTime] = React.useState(initialData.endTime || new Date(0, 0, 0, 9, 30));
|
||||
const [type, setType] = React.useState(initialData.type ?? 'presentation');
|
||||
const [description, setDescription] = React.useState(initialData.description || '');
|
||||
const [repeat, setRepeat] = React.useState(initialData.repeat || false);
|
||||
const [weekdays, setWeekdays] = React.useState<number[]>(initialData.weekdays || []);
|
||||
const [repeatUntil, setRepeatUntil] = React.useState(initialData.repeatUntil || null);
|
||||
const [skipHolidays, setSkipHolidays] = React.useState(initialData.skipHolidays || false);
|
||||
const [errors, setErrors] = React.useState<{ [key: string]: string }>({});
|
||||
// --- 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>(
|
||||
initialData.slideshowInterval ?? 10
|
||||
);
|
||||
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
|
||||
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
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 ?? 'presentation');
|
||||
setDescription(initialData.description || '');
|
||||
setRepeat(initialData.repeat || false);
|
||||
setWeekdays(initialData.weekdays || []);
|
||||
setRepeatUntil(initialData.repeatUntil || null);
|
||||
setSkipHolidays(initialData.skipHolidays || false);
|
||||
// --- KORREKTUR: Media, SlideshowInterval, WebsiteUrl aus initialData übernehmen ---
|
||||
setMedia(initialData.media ?? null);
|
||||
setSlideshowInterval(initialData.slideshowInterval ?? 10);
|
||||
setWebsiteUrl(initialData.websiteUrl ?? '');
|
||||
}
|
||||
}, [open, initialData]);
|
||||
|
||||
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';
|
||||
|
||||
// Vergangenheitsprüfung
|
||||
const startDateTime =
|
||||
startDate && startTime
|
||||
? new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
startTime.getHours(),
|
||||
startTime.getMinutes()
|
||||
)
|
||||
: null;
|
||||
const isPast = startDateTime && startDateTime < new Date();
|
||||
|
||||
if (isPast) {
|
||||
newErrors.startDate = 'Termine in der Vergangenheit sind nicht erlaubt!';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// Holiday blocking: prevent creating when range overlaps
|
||||
if (
|
||||
!editMode &&
|
||||
blockHolidays &&
|
||||
startDate &&
|
||||
startTime &&
|
||||
endTime &&
|
||||
typeof isHolidayRange === 'function'
|
||||
) {
|
||||
const s = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
startTime.getHours(),
|
||||
startTime.getMinutes()
|
||||
);
|
||||
const e = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
endTime.getHours(),
|
||||
endTime.getMinutes()
|
||||
);
|
||||
if (isHolidayRange(s, e)) {
|
||||
newErrors.startDate = 'Dieser Zeitraum liegt in den Ferien und ist gesperrt.';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrors({});
|
||||
|
||||
const group_id = typeof groupName === 'object' && groupName !== null ? groupName.id : groupName;
|
||||
|
||||
const payload: CustomEventData & { [key: string]: unknown } = {
|
||||
group_id,
|
||||
title,
|
||||
description,
|
||||
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,
|
||||
type,
|
||||
startDate,
|
||||
startTime,
|
||||
endTime,
|
||||
repeat,
|
||||
weekdays,
|
||||
repeatUntil,
|
||||
skipHolidays,
|
||||
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 {
|
||||
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),
|
||||
});
|
||||
res = await res.json();
|
||||
}
|
||||
if (res.success) {
|
||||
onSave(payload);
|
||||
onClose(); // <--- Box nach erfolgreichem Speichern schließen
|
||||
} else {
|
||||
setErrors({ api: res.error || 'Fehler beim Speichern' });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ api: 'Netzwerkfehler beim Speichern' });
|
||||
}
|
||||
console.log('handleSave called');
|
||||
};
|
||||
|
||||
// Vergangenheitsprüfung (außerhalb von handleSave, damit im Render verfügbar)
|
||||
const startDateTime =
|
||||
startDate && startTime
|
||||
? new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
startTime.getHours(),
|
||||
startTime.getMinutes()
|
||||
)
|
||||
: null;
|
||||
const isPast = !!(startDateTime && startDateTime < new Date());
|
||||
|
||||
return (
|
||||
<DialogComponent
|
||||
target="#root"
|
||||
visible={open}
|
||||
width="800px"
|
||||
header={() => (
|
||||
<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: '#fff' }}>
|
||||
für Raumgruppe: <b>{typeof groupName === 'object' ? groupName.name : groupName}</b>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
showCloseIcon={true}
|
||||
close={onClose}
|
||||
isModal={true}
|
||||
footerTemplate={() => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button className="e-btn e-danger" onClick={onClose}>
|
||||
Schließen
|
||||
</button>
|
||||
<button
|
||||
className="e-btn e-success"
|
||||
onClick={handleSave}
|
||||
disabled={isPast} // <--- Button deaktivieren, wenn Termin in Vergangenheit
|
||||
>
|
||||
Termin(e) speichern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<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"
|
||||
floatLabelType="Auto"
|
||||
value={title}
|
||||
change={e => setTitle(e.value)}
|
||||
/>
|
||||
{errors.title && <div style={{ color: 'red', fontSize: 12 }}>{errors.title}</div>}
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<TextBoxComponent
|
||||
placeholder="Beschreibung"
|
||||
floatLabelType="Auto"
|
||||
multiline={true}
|
||||
value={description}
|
||||
change={e => setDescription(e.value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<DatePickerComponent
|
||||
placeholder="Startdatum"
|
||||
floatLabelType="Auto"
|
||||
value={startDate ?? undefined}
|
||||
change={e => setStartDate(e.value)}
|
||||
/>
|
||||
{errors.startDate && (
|
||||
<div style={{ color: 'red', fontSize: 12 }}>{errors.startDate}</div>
|
||||
)}
|
||||
{isPast && (
|
||||
<span
|
||||
style={{
|
||||
color: 'orange',
|
||||
fontWeight: 600,
|
||||
marginLeft: 8,
|
||||
display: 'inline-block',
|
||||
background: '#fff3cd',
|
||||
borderRadius: 4,
|
||||
padding: '2px 8px',
|
||||
border: '1px solid #ffeeba',
|
||||
}}
|
||||
>
|
||||
⚠️ Termin liegt in der Vergangenheit!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<TimePickerComponent
|
||||
placeholder="Startzeit"
|
||||
floatLabelType="Auto"
|
||||
value={startTime}
|
||||
step={30}
|
||||
change={e => setStartTime(e.value)}
|
||||
/>
|
||||
{errors.startTime && (
|
||||
<div style={{ color: 'red', fontSize: 12 }}>{errors.startTime}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<TimePickerComponent
|
||||
placeholder="Endzeit"
|
||||
floatLabelType="Auto"
|
||||
value={endTime}
|
||||
step={30}
|
||||
change={e => setEndTime(e.value)}
|
||||
/>
|
||||
{errors.endTime && (
|
||||
<div style={{ color: 'red', fontSize: 12 }}>{errors.endTime}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 260 }}>
|
||||
{/* ...Wiederholung, MultiSelect, Wiederholung bis, Ferien... */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<CheckBoxComponent
|
||||
label="Wiederholender Termin"
|
||||
checked={repeat}
|
||||
change={e => setRepeat(e.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<MultiSelectComponent
|
||||
key={repeat ? 'enabled' : 'disabled'}
|
||||
dataSource={weekdayOptions}
|
||||
fields={{ text: 'label', value: 'value' }}
|
||||
placeholder="Wochentage"
|
||||
value={weekdays}
|
||||
change={e => setWeekdays(e.value as number[])}
|
||||
disabled={!repeat}
|
||||
showDropDownIcon={true}
|
||||
closePopupOnSelect={false}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<DatePickerComponent
|
||||
key={repeat ? 'enabled' : 'disabled'}
|
||||
placeholder="Wiederholung bis"
|
||||
floatLabelType="Auto"
|
||||
value={repeatUntil ?? undefined}
|
||||
change={e => setRepeatUntil(e.value)}
|
||||
disabled={!repeat}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<CheckBoxComponent
|
||||
label="Ferientage berücksichtigen"
|
||||
checked={skipHolidays}
|
||||
change={e => setSkipHolidays(e.checked)}
|
||||
disabled={!repeat}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEventModal;
|
||||
Reference in New Issue
Block a user