Initial commit - copied workspace after database cleanup

This commit is contained in:
RobbStarkAustria
2025-10-10 15:20:14 +00:00
commit 1efe40a03b
142 changed files with 23625 additions and 0 deletions

View 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;