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>;
|
||||
}
|
||||
|
||||
export async function fetchEvents(groupId: string) {
|
||||
const res = await fetch(`/api/events?group_id=${encodeURIComponent(groupId)}`);
|
||||
export async function fetchEvents(groupId: string, showInactive = false) {
|
||||
const res = await fetch(
|
||||
`/api/events?group_id=${encodeURIComponent(groupId)}&show_inactive=${showInactive ? '1' : '0'}`
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden der Termine');
|
||||
return data;
|
||||
|
||||
@@ -45,8 +45,6 @@ type RawEvent = {
|
||||
EndTime: string;
|
||||
IsAllDay: boolean;
|
||||
MediaId?: string | number; // Nur die MediaId wird gespeichert!
|
||||
SlideshowInterval?: number;
|
||||
WebsiteUrl?: string;
|
||||
};
|
||||
|
||||
import * as de from 'cldr-data/main/de/ca-gregorian.json';
|
||||
@@ -106,6 +104,7 @@ const Appointments: React.FC = () => {
|
||||
const [modalInitialData, setModalInitialData] = useState({});
|
||||
const [schedulerKey, setSchedulerKey] = useState(0);
|
||||
const [editMode, setEditMode] = useState(false); // NEU: Editiermodus
|
||||
const [showInactive, setShowInactive] = React.useState(false);
|
||||
|
||||
// Gruppen laden
|
||||
useEffect(() => {
|
||||
@@ -119,26 +118,36 @@ const Appointments: React.FC = () => {
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Termine für die ausgewählte Gruppe laden
|
||||
useEffect(() => {
|
||||
// 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,
|
||||
}));
|
||||
setEvents(mapped);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, [selectedGroupId, showInactive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedGroupId) {
|
||||
fetchEvents(selectedGroupId)
|
||||
.then((data: RawEvent[]) => {
|
||||
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);
|
||||
// selectedGroupId kann null sein, fetchEvents erwartet aber string
|
||||
fetchAndSetEvents();
|
||||
} else {
|
||||
setEvents([]);
|
||||
}
|
||||
}, [selectedGroupId]);
|
||||
}, [selectedGroupId, showInactive, fetchAndSetEvents]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -155,9 +164,10 @@ const Appointments: React.FC = () => {
|
||||
fields={{ text: 'name', value: 'id' }}
|
||||
placeholder="Gruppe auswählen"
|
||||
value={selectedGroupId}
|
||||
change={e => {
|
||||
change={(e: { value: string }) => {
|
||||
// <--- Typ für e ergänzt
|
||||
setEvents([]); // Events sofort leeren
|
||||
setSelectedGroupId(e.value as string);
|
||||
setSelectedGroupId(e.value);
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
@@ -187,6 +197,17 @@ const Appointments: React.FC = () => {
|
||||
>
|
||||
Neuen Termin anlegen
|
||||
</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
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
@@ -197,7 +218,7 @@ const Appointments: React.FC = () => {
|
||||
setModalOpen(false);
|
||||
setEditMode(false);
|
||||
if (selectedGroupId) {
|
||||
const data = await fetchEvents(selectedGroupId);
|
||||
const data = await fetchEvents(selectedGroupId, showInactive);
|
||||
const mapped: Event[] = data.map((e: RawEvent) => ({
|
||||
Id: e.Id,
|
||||
Subject: e.Subject,
|
||||
@@ -291,8 +312,17 @@ const Appointments: React.FC = () => {
|
||||
eventRendered={(args: EventRenderedArgs) => {
|
||||
if (selectedGroupId && args.data && args.data.Id) {
|
||||
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.color = '';
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -123,6 +123,23 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
|
||||
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)
|
||||
@@ -213,6 +230,19 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
|
||||
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"
|
||||
@@ -247,7 +277,11 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
|
||||
<button className="e-btn e-danger" onClick={onClose}>
|
||||
Schließen
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
@@ -285,6 +319,22 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
|
||||
{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 }}>
|
||||
|
||||
@@ -14,28 +14,38 @@ def get_events():
|
||||
session = Session()
|
||||
start = request.args.get("start")
|
||||
end = request.args.get("end")
|
||||
# geändert: jetzt group_id statt client_uuid
|
||||
group_id = request.args.get("group_id")
|
||||
query = session.query(Event).filter(Event.is_active == True)
|
||||
if start and end:
|
||||
query = query.filter(and_(Event.start < end, Event.end > start))
|
||||
show_inactive = request.args.get(
|
||||
"show_inactive", "0") == "1" # Checkbox-Logik
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
events_query = session.query(Event)
|
||||
if group_id:
|
||||
# geändert: filter auf group_id
|
||||
query = query.filter(Event.group_id == int(group_id))
|
||||
events = query.all()
|
||||
events_query = events_query.filter(Event.group_id == int(group_id))
|
||||
events = events_query.all()
|
||||
|
||||
result = []
|
||||
for e in events:
|
||||
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, # <--- Media-ID zurückgeben
|
||||
"SlideshowInterval": e.slideshow_interval,
|
||||
"WebsiteUrl": None, # Optional: für Website-Typ
|
||||
})
|
||||
# Zeitzonen-Korrektur für e.end
|
||||
if e.end and e.end.tzinfo is None:
|
||||
end_dt = e.end.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
end_dt = e.end
|
||||
|
||||
# Setze is_active auf False, wenn Termin vorbei ist
|
||||
if end_dt and end_dt < now and e.is_active:
|
||||
e.is_active = False
|
||||
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()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user