prevent saving past events

add function to show inactive events
This commit is contained in:
2025-07-24 09:35:57 +00:00
parent 4e6451ce80
commit 8bbda836b3
4 changed files with 135 additions and 43 deletions

View File

@@ -8,8 +8,10 @@ export interface Event {
extendedProps: Record<string, unknown>; extendedProps: Record<string, unknown>;
} }
export async function fetchEvents(groupId: string) { export async function fetchEvents(groupId: string, showInactive = false) {
const res = await fetch(`/api/events?group_id=${encodeURIComponent(groupId)}`); const res = await fetch(
`/api/events?group_id=${encodeURIComponent(groupId)}&show_inactive=${showInactive ? '1' : '0'}`
);
const data = await res.json(); const data = await res.json();
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden der Termine'); if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden der Termine');
return data; return data;

View File

@@ -45,8 +45,6 @@ type RawEvent = {
EndTime: string; EndTime: string;
IsAllDay: boolean; IsAllDay: boolean;
MediaId?: string | number; // Nur die MediaId wird gespeichert! MediaId?: string | number; // Nur die MediaId wird gespeichert!
SlideshowInterval?: number;
WebsiteUrl?: string;
}; };
import * as de from 'cldr-data/main/de/ca-gregorian.json'; import * as de from 'cldr-data/main/de/ca-gregorian.json';
@@ -106,6 +104,7 @@ const Appointments: React.FC = () => {
const [modalInitialData, setModalInitialData] = useState({}); const [modalInitialData, setModalInitialData] = useState({});
const [schedulerKey, setSchedulerKey] = useState(0); const [schedulerKey, setSchedulerKey] = useState(0);
const [editMode, setEditMode] = useState(false); // NEU: Editiermodus const [editMode, setEditMode] = useState(false); // NEU: Editiermodus
const [showInactive, setShowInactive] = React.useState(false);
// Gruppen laden // Gruppen laden
useEffect(() => { useEffect(() => {
@@ -119,26 +118,36 @@ const Appointments: React.FC = () => {
.catch(console.error); .catch(console.error);
}, []); }, []);
// Termine für die ausgewählte Gruppe laden // fetchAndSetEvents als useCallback definieren, damit die Dependency korrekt ist:
useEffect(() => { 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,
}));
setEvents(mapped);
} catch (err) {
console.error(err);
}
}, [selectedGroupId, showInactive]);
React.useEffect(() => {
if (selectedGroupId) { if (selectedGroupId) {
fetchEvents(selectedGroupId) // selectedGroupId kann null sein, fetchEvents erwartet aber string
.then((data: RawEvent[]) => { fetchAndSetEvents();
const mapped: Event[] = data.map(e => ({
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);
})
.catch(console.error);
} else { } else {
setEvents([]); setEvents([]);
} }
}, [selectedGroupId]); }, [selectedGroupId, showInactive, fetchAndSetEvents]);
return ( return (
<div> <div>
@@ -155,9 +164,10 @@ const Appointments: React.FC = () => {
fields={{ text: 'name', value: 'id' }} fields={{ text: 'name', value: 'id' }}
placeholder="Gruppe auswählen" placeholder="Gruppe auswählen"
value={selectedGroupId} value={selectedGroupId}
change={e => { change={(e: { value: string }) => {
// <--- Typ für e ergänzt
setEvents([]); // Events sofort leeren setEvents([]); // Events sofort leeren
setSelectedGroupId(e.value as string); setSelectedGroupId(e.value);
}} }}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
@@ -187,6 +197,17 @@ const Appointments: React.FC = () => {
> >
Neuen Termin anlegen Neuen Termin anlegen
</button> </button>
<div style={{ marginBottom: 16 }}>
<label>
<input
type="checkbox"
checked={showInactive}
onChange={e => setShowInactive(e.target.checked)}
style={{ marginRight: 8 }}
/>
Vergangene Termine anzeigen
</label>
</div>
<CustomEventModal <CustomEventModal
open={modalOpen} open={modalOpen}
onClose={() => { onClose={() => {
@@ -197,7 +218,7 @@ const Appointments: React.FC = () => {
setModalOpen(false); setModalOpen(false);
setEditMode(false); setEditMode(false);
if (selectedGroupId) { if (selectedGroupId) {
const data = await fetchEvents(selectedGroupId); const data = await fetchEvents(selectedGroupId, showInactive);
const mapped: Event[] = data.map((e: RawEvent) => ({ const mapped: Event[] = data.map((e: RawEvent) => ({
Id: e.Id, Id: e.Id,
Subject: e.Subject, Subject: e.Subject,
@@ -291,8 +312,17 @@ const Appointments: React.FC = () => {
eventRendered={(args: EventRenderedArgs) => { eventRendered={(args: EventRenderedArgs) => {
if (selectedGroupId && args.data && args.data.Id) { if (selectedGroupId && args.data && args.data.Id) {
const groupColor = getGroupColor(selectedGroupId, groups); const groupColor = getGroupColor(selectedGroupId, groups);
if (groupColor) { const now = new Date();
if (args.data.EndTime && args.data.EndTime < now) {
// Vergangene Termine: Raumgruppenfarbe mit Transparenz und grauer Schrift
args.element.style.backgroundColor = groupColor
? `${groupColor}80` // 80 = ~50% Transparenz für hex-Farben
: '#f3f3f3';
args.element.style.color = '';
} else if (groupColor) {
// Aktuelle/future Termine: normale Raumgruppenfarbe
args.element.style.backgroundColor = groupColor; args.element.style.backgroundColor = groupColor;
args.element.style.color = '';
} }
} }
}} }}

View File

@@ -123,6 +123,23 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich'; if (!endTime) newErrors.endTime = 'Endzeit ist erforderlich';
if (!type) newErrors.type = 'Termintyp 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 (type === 'presentation') {
if (!media) newErrors.media = 'Bitte eine Präsentation auswählen'; if (!media) newErrors.media = 'Bitte eine Präsentation auswählen';
if (!slideshowInterval || slideshowInterval < 1) if (!slideshowInterval || slideshowInterval < 1)
@@ -213,6 +230,19 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
console.log('handleSave called'); 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 ( return (
<DialogComponent <DialogComponent
target="#root" target="#root"
@@ -247,7 +277,11 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
<button className="e-btn e-danger" onClick={onClose}> <button className="e-btn e-danger" onClick={onClose}>
Schließen Schließen
</button> </button>
<button className="e-btn e-success" onClick={handleSave}> <button
className="e-btn e-success"
onClick={handleSave}
disabled={isPast} // <--- Button deaktivieren, wenn Termin in Vergangenheit
>
Termin(e) speichern Termin(e) speichern
</button> </button>
</div> </div>
@@ -285,6 +319,22 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
{errors.startDate && ( {errors.startDate && (
<div style={{ color: 'red', fontSize: 12 }}>{errors.startDate}</div> <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>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>

View File

@@ -14,28 +14,38 @@ def get_events():
session = Session() session = Session()
start = request.args.get("start") start = request.args.get("start")
end = request.args.get("end") end = request.args.get("end")
# geändert: jetzt group_id statt client_uuid
group_id = request.args.get("group_id") group_id = request.args.get("group_id")
query = session.query(Event).filter(Event.is_active == True) show_inactive = request.args.get(
if start and end: "show_inactive", "0") == "1" # Checkbox-Logik
query = query.filter(and_(Event.start < end, Event.end > start))
now = datetime.now(timezone.utc)
events_query = session.query(Event)
if group_id: if group_id:
# geändert: filter auf group_id events_query = events_query.filter(Event.group_id == int(group_id))
query = query.filter(Event.group_id == int(group_id)) events = events_query.all()
events = query.all()
result = [] result = []
for e in events: for e in events:
result.append({ # Zeitzonen-Korrektur für e.end
"Id": str(e.id), if e.end and e.end.tzinfo is None:
"GroupId": e.group_id, end_dt = e.end.replace(tzinfo=timezone.utc)
"Subject": e.title, else:
"StartTime": e.start.isoformat() if e.start else None, end_dt = e.end
"EndTime": e.end.isoformat() if e.end else None,
"IsAllDay": False, # Setze is_active auf False, wenn Termin vorbei ist
"MediaId": e.event_media_id, # <--- Media-ID zurückgeben if end_dt and end_dt < now and e.is_active:
"SlideshowInterval": e.slideshow_interval, e.is_active = False
"WebsiteUrl": None, # Optional: für Website-Typ session.commit()
}) if show_inactive or e.is_active:
result.append({
"Id": str(e.id),
"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,
})
session.close() session.close()
return jsonify(result) return jsonify(result)