Initial commit - copied workspace after database cleanup
This commit is contained in:
813
dashboard/src/appointments.tsx
Normal file
813
dashboard/src/appointments.tsx
Normal file
@@ -0,0 +1,813 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ScheduleComponent,
|
||||
Day,
|
||||
Week,
|
||||
WorkWeek,
|
||||
Month,
|
||||
Agenda,
|
||||
Inject,
|
||||
ViewsDirective,
|
||||
ViewDirective,
|
||||
} from '@syncfusion/ej2-react-schedule';
|
||||
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
|
||||
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
|
||||
import type {
|
||||
EventRenderedArgs,
|
||||
ActionEventArgs,
|
||||
RenderCellEventArgs,
|
||||
} from '@syncfusion/ej2-react-schedule';
|
||||
import { fetchEvents } from './apiEvents';
|
||||
import { fetchGroups } from './apiGroups';
|
||||
import { getGroupColor } from './groupColors';
|
||||
import { deleteEvent } from './apiEvents';
|
||||
import CustomEventModal from './components/CustomEventModal';
|
||||
import { fetchMediaById } from './apiClients';
|
||||
import { listHolidays, type Holiday } from './apiHolidays';
|
||||
import {
|
||||
getAcademicPeriodForDate,
|
||||
listAcademicPeriods,
|
||||
setActiveAcademicPeriod,
|
||||
} from './apiAcademicPeriods';
|
||||
import {
|
||||
Presentation,
|
||||
Globe,
|
||||
Video,
|
||||
MessageSquare,
|
||||
School,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import caGregorian from './cldr/ca-gregorian.json';
|
||||
import numbers from './cldr/numbers.json';
|
||||
import timeZoneNames from './cldr/timeZoneNames.json';
|
||||
import numberingSystems from './cldr/numberingSystems.json';
|
||||
|
||||
// Typ für Gruppe ergänzen
|
||||
type Group = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
// Typ für Event ergänzen
|
||||
type Event = {
|
||||
Id: string;
|
||||
Subject: string;
|
||||
StartTime: Date;
|
||||
EndTime: Date;
|
||||
IsAllDay: boolean;
|
||||
IsBlock?: boolean; // Syncfusion block appointment
|
||||
isHoliday?: boolean; // marker for styling/logic
|
||||
MediaId?: string | number;
|
||||
SlideshowInterval?: number;
|
||||
WebsiteUrl?: string;
|
||||
Icon?: string; // <--- Icon ergänzen!
|
||||
Type?: string; // <--- Typ ergänzen, falls benötigt
|
||||
};
|
||||
|
||||
type RawEvent = {
|
||||
Id: string;
|
||||
Subject: string;
|
||||
StartTime: string;
|
||||
EndTime: string;
|
||||
IsAllDay: boolean;
|
||||
MediaId?: string | number;
|
||||
Icon?: string; // <--- Icon ergänzen!
|
||||
Type?: string;
|
||||
};
|
||||
|
||||
// CLDR-Daten laden (direkt die JSON-Objekte übergeben)
|
||||
loadCldr(
|
||||
caGregorian as object,
|
||||
numbers as object,
|
||||
timeZoneNames as object,
|
||||
numberingSystems as object
|
||||
);
|
||||
|
||||
// Deutsche Lokalisierung für den Scheduler
|
||||
L10n.load({
|
||||
de: {
|
||||
schedule: {
|
||||
day: 'Tag',
|
||||
week: 'Woche',
|
||||
workWeek: 'Arbeitswoche',
|
||||
month: 'Monat',
|
||||
agenda: 'Agenda',
|
||||
today: 'Heute',
|
||||
noEvents: 'Keine Termine',
|
||||
allDay: 'Ganztägig',
|
||||
start: 'Start',
|
||||
end: 'Ende',
|
||||
event: 'Termin',
|
||||
save: 'Speichern',
|
||||
cancel: 'Abbrechen',
|
||||
delete: 'Löschen',
|
||||
edit: 'Bearbeiten',
|
||||
newEvent: 'Neuer Termin',
|
||||
title: 'Titel',
|
||||
description: 'Beschreibung',
|
||||
location: 'Ort',
|
||||
recurrence: 'Wiederholung',
|
||||
repeat: 'Wiederholen',
|
||||
deleteEvent: 'Termin löschen',
|
||||
deleteContent: 'Möchten Sie diesen Termin wirklich löschen?',
|
||||
moreDetails: 'Mehr Details',
|
||||
addTitle: 'Termintitel',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Kultur setzen
|
||||
setCulture('de');
|
||||
|
||||
// Mapping für Lucide-Icons
|
||||
const iconMap: Record<string, React.ElementType> = {
|
||||
Presentation,
|
||||
Globe,
|
||||
Video,
|
||||
MessageSquare,
|
||||
School,
|
||||
};
|
||||
|
||||
const eventTemplate = (event: Event) => {
|
||||
const IconComponent = iconMap[event.Icon ?? ''] || null;
|
||||
// Zeitangabe formatieren
|
||||
const start = event.StartTime instanceof Date ? event.StartTime : new Date(event.StartTime);
|
||||
const end = event.EndTime instanceof Date ? event.EndTime : new Date(event.EndTime);
|
||||
const timeString = `${start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', color: '#000', marginBottom: 2 }}>
|
||||
{IconComponent && (
|
||||
<span style={{ verticalAlign: 'middle', display: 'inline-block', marginRight: 6 }}>
|
||||
<IconComponent size={18} color="#000" />
|
||||
</span>
|
||||
)}
|
||||
<span style={{ marginTop: 3 }}>{event.Subject}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.95em', color: '#000', marginTop: -2 }}>{timeString}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Appointments: React.FC = () => {
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [holidays, setHolidays] = useState<Holiday[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalInitialData, setModalInitialData] = useState({});
|
||||
const [schedulerKey, setSchedulerKey] = useState(0);
|
||||
const [editMode, setEditMode] = useState(false); // NEU: Editiermodus
|
||||
const [showInactive, setShowInactive] = React.useState(true);
|
||||
const [allowScheduleOnHolidays, setAllowScheduleOnHolidays] = React.useState(false);
|
||||
const [showHolidayList, setShowHolidayList] = React.useState(true);
|
||||
const scheduleRef = React.useRef<ScheduleComponent | null>(null);
|
||||
const [holidaysInView, setHolidaysInView] = React.useState<number>(0);
|
||||
const [schoolYearLabel, setSchoolYearLabel] = React.useState<string>('');
|
||||
const [hasSchoolYearPlan, setHasSchoolYearPlan] = React.useState<boolean>(false);
|
||||
const [periods, setPeriods] = React.useState<{ id: number; label: string }[]>([]);
|
||||
const [activePeriodId, setActivePeriodId] = React.useState<number | null>(null);
|
||||
|
||||
// Gruppen laden
|
||||
useEffect(() => {
|
||||
fetchGroups()
|
||||
.then(data => {
|
||||
// Nur Gruppen mit id != 1 berücksichtigen (nicht zugeordnet ignorieren)
|
||||
const filtered = Array.isArray(data) ? data.filter(g => g.id && g.name && g.id !== 1) : [];
|
||||
setGroups(filtered);
|
||||
if (filtered.length > 0) setSelectedGroupId(filtered[0].id);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Holidays laden
|
||||
useEffect(() => {
|
||||
listHolidays()
|
||||
.then(res => setHolidays(res.holidays || []))
|
||||
.catch(err => console.error('Ferien laden fehlgeschlagen:', err));
|
||||
}, []);
|
||||
|
||||
// Perioden laden (Dropdown)
|
||||
useEffect(() => {
|
||||
listAcademicPeriods()
|
||||
.then(all => {
|
||||
setPeriods(all.map(p => ({ id: p.id, label: p.display_name || p.name })));
|
||||
const active = all.find(p => p.is_active);
|
||||
setActivePeriodId(active ? active.id : null);
|
||||
})
|
||||
.catch(err => console.error('Akademische Perioden laden fehlgeschlagen:', err));
|
||||
}, []);
|
||||
|
||||
// fetchAndSetEvents als useCallback definieren, damit die Dependency korrekt ist:
|
||||
const fetchAndSetEvents = React.useCallback(async () => {
|
||||
if (!selectedGroupId) {
|
||||
setEvents([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await fetchEvents(selectedGroupId, showInactive); // selectedGroupId ist jetzt garantiert string
|
||||
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,
|
||||
Icon: e.Icon, // <--- Icon übernehmen!
|
||||
Type: e.Type, // <--- Typ übernehmen!
|
||||
}));
|
||||
setEvents(mapped);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Termine:', err);
|
||||
}
|
||||
}, [selectedGroupId, showInactive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedGroupId) {
|
||||
// selectedGroupId kann null sein, fetchEvents erwartet aber string
|
||||
fetchAndSetEvents();
|
||||
} else {
|
||||
setEvents([]);
|
||||
}
|
||||
}, [selectedGroupId, showInactive, fetchAndSetEvents]);
|
||||
|
||||
// Helper: prüfe, ob Zeitraum einen Feiertag/Ferienbereich schneidet
|
||||
const isWithinHolidayRange = React.useCallback(
|
||||
(start: Date, end: Date) => {
|
||||
// normalisiere Endzeit minimal (Syncfusion nutzt exklusive Enden in einigen Fällen)
|
||||
const adjEnd = new Date(end);
|
||||
// keine Änderung nötig – unsere eigenen Events sind präzise
|
||||
for (const h of holidays) {
|
||||
// Holiday dates are strings YYYY-MM-DD (local date)
|
||||
const hs = new Date(h.start_date + 'T00:00:00');
|
||||
const he = new Date(h.end_date + 'T23:59:59');
|
||||
if (
|
||||
(start >= hs && start <= he) ||
|
||||
(adjEnd >= hs && adjEnd <= he) ||
|
||||
(start <= hs && adjEnd >= he)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[holidays]
|
||||
);
|
||||
|
||||
// Baue Holiday-Anzeige-Events und Block-Events
|
||||
const holidayDisplayEvents: Event[] = useMemo(() => {
|
||||
if (!showHolidayList) return [];
|
||||
const out: Event[] = [];
|
||||
for (const h of holidays) {
|
||||
const start = new Date(h.start_date + 'T00:00:00');
|
||||
const end = new Date(h.end_date + 'T23:59:59');
|
||||
out.push({
|
||||
Id: `holiday-${h.id}-display`,
|
||||
Subject: h.name,
|
||||
StartTime: start,
|
||||
EndTime: end,
|
||||
IsAllDay: true,
|
||||
isHoliday: true,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}, [holidays, showHolidayList]);
|
||||
|
||||
const holidayBlockEvents: Event[] = useMemo(() => {
|
||||
if (allowScheduleOnHolidays) return [];
|
||||
const out: Event[] = [];
|
||||
for (const h of holidays) {
|
||||
const start = new Date(h.start_date + 'T00:00:00');
|
||||
const end = new Date(h.end_date + 'T23:59:59');
|
||||
out.push({
|
||||
Id: `holiday-${h.id}-block`,
|
||||
Subject: h.name,
|
||||
StartTime: start,
|
||||
EndTime: end,
|
||||
IsAllDay: true,
|
||||
IsBlock: true,
|
||||
isHoliday: true,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}, [holidays, allowScheduleOnHolidays]);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
return [...events, ...holidayDisplayEvents, ...holidayBlockEvents];
|
||||
}, [events, holidayDisplayEvents, holidayBlockEvents]);
|
||||
|
||||
// Aktive akademische Periode für Datum aus dem Backend ermitteln
|
||||
const refreshAcademicPeriodFor = React.useCallback(
|
||||
async (baseDate: Date) => {
|
||||
try {
|
||||
const p = await getAcademicPeriodForDate(baseDate);
|
||||
if (!p) {
|
||||
setSchoolYearLabel('');
|
||||
setHasSchoolYearPlan(false);
|
||||
return;
|
||||
}
|
||||
// Anzeige: bevorzugt display_name, sonst name
|
||||
const label = p.display_name ? p.display_name : p.name;
|
||||
setSchoolYearLabel(label);
|
||||
// Existiert ein Ferienplan innerhalb der Periode?
|
||||
const start = new Date(p.start_date + 'T00:00:00');
|
||||
const end = new Date(p.end_date + 'T23:59:59');
|
||||
let exists = false;
|
||||
for (const h of holidays) {
|
||||
const hs = new Date(h.start_date + 'T00:00:00');
|
||||
const he = new Date(h.end_date + 'T23:59:59');
|
||||
if (hs <= end && he >= start) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setHasSchoolYearPlan(exists);
|
||||
} catch (e) {
|
||||
console.error('Akademische Periode laden fehlgeschlagen:', e);
|
||||
setSchoolYearLabel('');
|
||||
setHasSchoolYearPlan(false);
|
||||
}
|
||||
},
|
||||
[holidays]
|
||||
);
|
||||
|
||||
// Anzahl an Ferienzeiträumen in aktueller Ansicht ermitteln + Perioden-Indikator setzen
|
||||
const updateHolidaysInView = React.useCallback(() => {
|
||||
const inst = scheduleRef.current;
|
||||
if (!inst) {
|
||||
setHolidaysInView(0);
|
||||
return;
|
||||
}
|
||||
const view = inst.currentView as 'Day' | 'Week' | 'WorkWeek' | 'Month' | 'Agenda';
|
||||
const baseDate = inst.selectedDate as Date;
|
||||
if (!baseDate) {
|
||||
setHolidaysInView(0);
|
||||
return;
|
||||
}
|
||||
let rangeStart = new Date(baseDate);
|
||||
let rangeEnd = new Date(baseDate);
|
||||
if (view === 'Day' || view === 'Agenda') {
|
||||
rangeStart.setHours(0, 0, 0, 0);
|
||||
rangeEnd.setHours(23, 59, 59, 999);
|
||||
} else if (view === 'Week' || view === 'WorkWeek') {
|
||||
const day = baseDate.getDay();
|
||||
const diffToMonday = (day + 6) % 7; // Monday=0
|
||||
rangeStart = new Date(baseDate);
|
||||
rangeStart.setDate(baseDate.getDate() - diffToMonday);
|
||||
rangeStart.setHours(0, 0, 0, 0);
|
||||
rangeEnd = new Date(rangeStart);
|
||||
rangeEnd.setDate(rangeStart.getDate() + 6);
|
||||
rangeEnd.setHours(23, 59, 59, 999);
|
||||
} else if (view === 'Month') {
|
||||
rangeStart = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1, 0, 0, 0, 0);
|
||||
rangeEnd = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
}
|
||||
let count = 0;
|
||||
for (const h of holidays) {
|
||||
const hs = new Date(h.start_date + 'T00:00:00');
|
||||
const he = new Date(h.end_date + 'T23:59:59');
|
||||
const overlaps =
|
||||
(hs >= rangeStart && hs <= rangeEnd) ||
|
||||
(he >= rangeStart && he <= rangeEnd) ||
|
||||
(hs <= rangeStart && he >= rangeEnd);
|
||||
if (overlaps) count += 1;
|
||||
}
|
||||
setHolidaysInView(count);
|
||||
// Perioden-Indikator über Backend prüfen
|
||||
refreshAcademicPeriodFor(baseDate);
|
||||
}, [holidays, refreshAcademicPeriodFor]);
|
||||
|
||||
// Aktualisiere Indikator wenn Ferien oder Ansicht (Key) wechseln
|
||||
React.useEffect(() => {
|
||||
updateHolidaysInView();
|
||||
}, [holidays, updateHolidaysInView, schedulerKey]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Terminmanagement</h1>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<label htmlFor="groupDropdown" className="mb-0 mr-2" style={{ whiteSpace: 'nowrap' }}>
|
||||
Raumgruppe auswählen:
|
||||
</label>
|
||||
<DropDownListComponent
|
||||
id="groupDropdown"
|
||||
dataSource={groups}
|
||||
fields={{ text: 'name', value: 'id' }}
|
||||
placeholder="Gruppe auswählen"
|
||||
value={selectedGroupId}
|
||||
width="240px"
|
||||
change={(e: { value: string }) => {
|
||||
// <--- Typ für e ergänzt
|
||||
setEvents([]); // Events sofort leeren
|
||||
setSelectedGroupId(e.value);
|
||||
}}
|
||||
style={{}}
|
||||
/>
|
||||
|
||||
{/* Akademische Periode Selector + Plan-Badge */}
|
||||
<span style={{ marginLeft: 8, whiteSpace: 'nowrap' }}>Periode:</span>
|
||||
<DropDownListComponent
|
||||
id="periodDropdown"
|
||||
dataSource={periods}
|
||||
fields={{ text: 'label', value: 'id' }}
|
||||
placeholder="Periode wählen"
|
||||
value={activePeriodId ?? undefined}
|
||||
width="260px"
|
||||
change={async (e: { value: number }) => {
|
||||
const id = Number(e.value);
|
||||
if (!id) return;
|
||||
try {
|
||||
const updated = await setActiveAcademicPeriod(id);
|
||||
setActivePeriodId(updated.id);
|
||||
// Zum gleichen Tag/Monat (heute) innerhalb der gewählten Periode springen
|
||||
const today = new Date();
|
||||
const targetYear = new Date(updated.start_date).getFullYear();
|
||||
const target = new Date(targetYear, today.getMonth(), today.getDate(), 12, 0, 0);
|
||||
if (scheduleRef.current) {
|
||||
scheduleRef.current.selectedDate = target;
|
||||
scheduleRef.current.dataBind?.();
|
||||
}
|
||||
updateHolidaysInView();
|
||||
} catch (err) {
|
||||
console.error('Aktive Periode setzen fehlgeschlagen:', err);
|
||||
}
|
||||
}}
|
||||
style={{}}
|
||||
/>
|
||||
{/* School-year/period plan badge (adjacent) */}
|
||||
<span
|
||||
title={hasSchoolYearPlan ? 'Ferienplan ist hinterlegt' : 'Kein Ferienplan hinterlegt'}
|
||||
style={{
|
||||
background: hasSchoolYearPlan ? '#dcfce7' : '#f3f4f6',
|
||||
border: hasSchoolYearPlan ? '1px solid #86efac' : '1px solid #e5e7eb',
|
||||
color: '#000',
|
||||
padding: '4px 10px',
|
||||
borderRadius: 16,
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{hasSchoolYearPlan ? (
|
||||
<CheckCircle size={14} color="#166534" />
|
||||
) : (
|
||||
<AlertCircle size={14} color="#6b7280" />
|
||||
)}
|
||||
{schoolYearLabel || 'Periode'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="e-btn e-success mb-4"
|
||||
onClick={() => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
Neuen Termin anlegen
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 24,
|
||||
marginBottom: 16,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showInactive}
|
||||
onChange={e => setShowInactive(e.target.checked)}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
Vergangene Termine anzeigen
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allowScheduleOnHolidays}
|
||||
onChange={e => setAllowScheduleOnHolidays(e.target.checked)}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
Termine an Ferientagen erlauben
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showHolidayList}
|
||||
onChange={e => setShowHolidayList(e.target.checked)}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
Ferien im Kalender anzeigen
|
||||
</label>
|
||||
{/* Right-aligned indicators */}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
{/* Holidays-in-view badge */}
|
||||
<span
|
||||
title="Anzahl der Ferientage/-zeiträume in der aktuellen Ansicht"
|
||||
style={{
|
||||
background: holidaysInView > 0 ? '#ffe8cc' : '#f3f4f6',
|
||||
border: holidaysInView > 0 ? '1px solid #ffcf99' : '1px solid #e5e7eb',
|
||||
color: '#000',
|
||||
padding: '4px 10px',
|
||||
borderRadius: 16,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{holidaysInView > 0 ? `Ferien im Blick: ${holidaysInView}` : 'Keine Ferien in Ansicht'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<CustomEventModal
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
setEditMode(false); // Editiermodus zurücksetzen
|
||||
}}
|
||||
onSave={async () => {
|
||||
setModalOpen(false);
|
||||
setEditMode(false);
|
||||
if (selectedGroupId) {
|
||||
const data = await fetchEvents(selectedGroupId, showInactive);
|
||||
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
|
||||
}
|
||||
}}
|
||||
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
|
||||
blockHolidays={!allowScheduleOnHolidays}
|
||||
isHolidayRange={(s, e) => isWithinHolidayRange(s, e)}
|
||||
/>
|
||||
<ScheduleComponent
|
||||
ref={scheduleRef}
|
||||
key={schedulerKey} // <-- dynamischer Key
|
||||
height="750px"
|
||||
locale="de"
|
||||
currentView="Week"
|
||||
eventSettings={{
|
||||
dataSource: dataSource,
|
||||
fields: { isBlock: 'IsBlock' },
|
||||
template: eventTemplate, // <--- Hier das Template setzen!
|
||||
}}
|
||||
actionComplete={() => updateHolidaysInView()}
|
||||
cellClick={args => {
|
||||
if (!allowScheduleOnHolidays && isWithinHolidayRange(args.startTime, args.endTime)) {
|
||||
args.cancel = true;
|
||||
return; // block creation on holidays
|
||||
}
|
||||
// args.startTime und args.endTime sind Date-Objekte
|
||||
args.cancel = true; // Verhindert die Standardaktion
|
||||
setModalInitialData({
|
||||
startDate: args.startTime,
|
||||
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) => {
|
||||
// Blende Nicht-Ferien-Events aus, falls sie in Ferien fallen und Terminieren nicht erlaubt ist
|
||||
if (!allowScheduleOnHolidays && args.data && !args.data.isHoliday) {
|
||||
const s =
|
||||
args.data.StartTime instanceof Date
|
||||
? args.data.StartTime
|
||||
: new Date(args.data.StartTime);
|
||||
const e =
|
||||
args.data.EndTime instanceof Date ? args.data.EndTime : new Date(args.data.EndTime);
|
||||
if (isWithinHolidayRange(s, e)) {
|
||||
args.cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedGroupId && args.data && args.data.Id) {
|
||||
const groupColor = getGroupColor(selectedGroupId, groups);
|
||||
const now = new Date();
|
||||
|
||||
let IconComponent: React.ElementType | null = null;
|
||||
switch (args.data.Type) {
|
||||
case 'presentation':
|
||||
IconComponent = Presentation;
|
||||
break;
|
||||
case 'website':
|
||||
IconComponent = Globe;
|
||||
break;
|
||||
case 'video':
|
||||
IconComponent = Video;
|
||||
break;
|
||||
case 'message':
|
||||
IconComponent = MessageSquare;
|
||||
break;
|
||||
case 'webuntis':
|
||||
IconComponent = School;
|
||||
break;
|
||||
default:
|
||||
IconComponent = null;
|
||||
}
|
||||
|
||||
// Nur .e-subject verwenden!
|
||||
const titleElement = args.element.querySelector('.e-subject');
|
||||
if (titleElement && IconComponent) {
|
||||
const svgString = renderToStaticMarkup(<IconComponent size={18} color="#78591c" />);
|
||||
// Immer nur den reinen Text nehmen, kein vorhandenes Icon!
|
||||
const subjectText = (titleElement as HTMLElement).textContent ?? '';
|
||||
(titleElement as HTMLElement).innerHTML =
|
||||
`<span style="vertical-align:middle;display:inline-block;margin-right:6px;">${svgString}</span>` +
|
||||
subjectText;
|
||||
}
|
||||
|
||||
// Vergangene Termine: Raumgruppenfarbe
|
||||
if (args.data.EndTime && args.data.EndTime < now) {
|
||||
args.element.style.backgroundColor = groupColor ? `${groupColor}` : '#f3f3f3';
|
||||
args.element.style.color = '#000';
|
||||
} else if (groupColor) {
|
||||
args.element.style.backgroundColor = groupColor;
|
||||
args.element.style.color = '#000';
|
||||
}
|
||||
|
||||
// Spezielle Darstellung für Ferienanzeige-Events
|
||||
if (args.data.isHoliday && !args.data.IsBlock) {
|
||||
args.element.style.backgroundColor = '#ffe8cc'; // sanftes Orange
|
||||
args.element.style.border = '1px solid #ffcf99';
|
||||
args.element.style.color = '#000';
|
||||
}
|
||||
// Gleiche Darstellung für Ferien-Block-Events
|
||||
if (args.data.isHoliday && args.data.IsBlock) {
|
||||
args.element.style.backgroundColor = '#ffe8cc';
|
||||
args.element.style.border = '1px solid #ffcf99';
|
||||
args.element.style.color = '#000';
|
||||
}
|
||||
}
|
||||
}}
|
||||
actionBegin={async (args: ActionEventArgs) => {
|
||||
if (args.requestType === 'eventRemove') {
|
||||
// args.data ist ein Array von zu löschenden Events
|
||||
const toDelete = Array.isArray(args.data) ? args.data : [args.data];
|
||||
for (const ev of toDelete) {
|
||||
try {
|
||||
await deleteEvent(ev.Id); // Deine API-Funktion
|
||||
} catch (err) {
|
||||
// Optional: Fehlerbehandlung
|
||||
console.error('Fehler beim Löschen:', err);
|
||||
}
|
||||
}
|
||||
// Events nach Löschen neu laden
|
||||
if (selectedGroupId) {
|
||||
fetchEvents(selectedGroupId, showInactive)
|
||||
.then((data: RawEvent[]) => {
|
||||
const mapped: Event[] = data.map((e: RawEvent) => ({
|
||||
Id: e.Id,
|
||||
Subject: e.Subject,
|
||||
StartTime: new Date(e.StartTime),
|
||||
EndTime: new Date(e.EndTime),
|
||||
IsAllDay: e.IsAllDay,
|
||||
MediaId: e.MediaId,
|
||||
}));
|
||||
setEvents(mapped);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
// Syncfusion soll das Event nicht selbst löschen
|
||||
args.cancel = true;
|
||||
} else if (
|
||||
(args.requestType === 'eventCreate' || args.requestType === 'eventChange') &&
|
||||
!allowScheduleOnHolidays
|
||||
) {
|
||||
// Verhindere Erstellen/Ändern in Ferienbereichen (Failsafe, falls eingebauter Editor genutzt wird)
|
||||
type PartialEventLike = { StartTime?: Date | string; EndTime?: Date | string };
|
||||
const raw = (args as ActionEventArgs).data as
|
||||
| PartialEventLike
|
||||
| PartialEventLike[]
|
||||
| undefined;
|
||||
const data = Array.isArray(raw) ? raw[0] : raw;
|
||||
if (data && data.StartTime && data.EndTime) {
|
||||
const s = data.StartTime instanceof Date ? data.StartTime : new Date(data.StartTime);
|
||||
const e = data.EndTime instanceof Date ? data.EndTime : new Date(data.EndTime);
|
||||
if (isWithinHolidayRange(s, e)) {
|
||||
args.cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
firstDayOfWeek={1}
|
||||
renderCell={(args: RenderCellEventArgs) => {
|
||||
// Nur für Arbeitszellen (Stunden-/Tageszellen)
|
||||
if (args.elementType === 'workCells') {
|
||||
const now = new Date();
|
||||
// args.element ist vom Typ Element, daher als HTMLElement casten:
|
||||
const cell = args.element as HTMLElement;
|
||||
if (args.date && args.date < now) {
|
||||
cell.style.backgroundColor = '#fff9e3'; // Hellgelb für Vergangenheit
|
||||
cell.style.opacity = '0.7';
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ViewsDirective>
|
||||
<ViewDirective option="Day" />
|
||||
<ViewDirective option="Week" />
|
||||
<ViewDirective option="WorkWeek" />
|
||||
<ViewDirective option="Month" />
|
||||
<ViewDirective option="Agenda" />
|
||||
</ViewsDirective>
|
||||
<Inject services={[Day, Week, WorkWeek, Month, Agenda]} />
|
||||
</ScheduleComponent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Appointments;
|
||||
Reference in New Issue
Block a user