prevent saving past events
add function to show inactive events
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user