feat: presentation defaults + scheduler active-only

Add Settings → Events (Presentations) defaults (interval, page-progress,
auto-progress) persisted via /api/system-settings
Seed defaults in init_defaults.py (10/true/true)
Add Event.page_progress and Event.auto_progress (Alembic applied)
CustomEventModal applies defaults on create and saves fields
Scheduler publishes only currently active events per group, clears retained
topics when none, normalizes times to UTC; include flags in payloads
Docs: update README, copilot instructions, and DEV-CHANGELOG
If you can split the commit, even better

feat(dashboard): add presentation defaults UI
feat(api): seed presentation defaults in init_defaults.py
feat(model): add Event.page_progress and Event.auto_progress
feat(scheduler): publish only active events; clear retained topics; UTC
docs: update README and copilot-instructions
chore: update DEV-CHANGELOG
This commit is contained in:
RobbStarkAustria
2025-10-18 15:34:52 +00:00
parent 3487d33a2f
commit c9cc535fc6
12 changed files with 316 additions and 80 deletions

View File

@@ -21,6 +21,8 @@ type CustomEventData = {
skipHolidays: boolean;
media?: { id: string; path: string; name: string } | null; // <--- ergänzt
slideshowInterval?: number; // <--- ergänzt
pageProgress?: boolean; // NEU
autoProgress?: boolean; // NEU
websiteUrl?: string; // <--- ergänzt
};
@@ -38,8 +40,7 @@ type CustomEventModalProps = {
groupName: string | { id: string | null; name: string };
groupColor?: string;
editMode?: boolean;
blockHolidays?: boolean;
isHolidayRange?: (start: Date, end: Date) => boolean;
// Removed unused blockHolidays and isHolidayRange
};
const weekdayOptions = [
@@ -68,8 +69,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
groupName,
groupColor,
editMode,
blockHolidays,
isHolidayRange,
}) => {
const [title, setTitle] = React.useState(initialData.title || '');
const [startDate, setStartDate] = React.useState(initialData.startDate || null);
@@ -98,9 +97,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
path: string;
name: string;
} | null>(null);
// General settings state for presentation
// Removed unused generalLoaded and setGeneralLoaded
// Removed unused generalLoaded/generalSlideshowInterval/generalPageProgress/generalAutoProgress
// Per-event state
const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
initialData.slideshowInterval ?? 10
);
const [pageProgress, setPageProgress] = React.useState<boolean>(
initialData.pageProgress ?? true
);
const [autoProgress, setAutoProgress] = React.useState<boolean>(
initialData.autoProgress ?? true
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
@@ -182,39 +192,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
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;
}
// Holiday blocking logic removed (blockHolidays, isHolidayRange no longer used)
setErrors({});
@@ -269,7 +247,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
startDate,
startTime,
endTime,
// Initialize required fields
repeat: isSingleOccurrence ? false : repeat,
weekdays: isSingleOccurrence ? [] : weekdays,
repeatUntil: isSingleOccurrence ? null : repeatUntil,
@@ -284,6 +261,8 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (type === 'presentation') {
payload.event_media_id = media?.id;
payload.slideshow_interval = slideshowInterval;
payload.page_progress = pageProgress;
payload.auto_progress = autoProgress;
}
if (type === 'website') {
@@ -596,6 +575,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
value={String(slideshowInterval)}
change={e => setSlideshowInterval(Number(e.value))}
/>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen"
checked={pageProgress}
change={e => setPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Automatischer Fortschritt"
checked={autoProgress}
change={e => setAutoProgress(e.checked || false)}
/>
</div>
</div>
)}
{type === 'website' && (

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { TabComponent, TabItemDirective, TabItemsDirective } from '@syncfusion/ej2-react-navigations';
import { NumericTextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
@@ -12,6 +13,49 @@ import { listAcademicPeriods, getActiveAcademicPeriod, setActiveAcademicPeriod,
import { Link } from 'react-router-dom';
const Einstellungen: React.FC = () => {
// Presentation settings state
const [presentationInterval, setPresentationInterval] = React.useState(10);
const [presentationPageProgress, setPresentationPageProgress] = React.useState(true);
const [presentationAutoProgress, setPresentationAutoProgress] = React.useState(true);
const [presentationBusy, setPresentationBusy] = React.useState(false);
// Load settings from backend
const loadPresentationSettings = React.useCallback(async () => {
try {
const keys = [
'presentation_interval',
'presentation_page_progress',
'presentation_auto_progress',
];
const results = await Promise.all(keys.map(k => import('./apiSystemSettings').then(m => m.getSetting(k))));
setPresentationInterval(Number(results[0].value) || 10);
setPresentationPageProgress(results[1].value === 'true');
setPresentationAutoProgress(results[2].value === 'true');
} catch {
showToast('Fehler beim Laden der Präsentations-Einstellungen', 'e-toast-danger');
}
}, []);
React.useEffect(() => {
loadPresentationSettings();
}, [loadPresentationSettings]);
const onSavePresentationSettings = async () => {
setPresentationBusy(true);
try {
const api = await import('./apiSystemSettings');
await Promise.all([
api.updateSetting('presentation_interval', String(presentationInterval)),
api.updateSetting('presentation_page_progress', presentationPageProgress ? 'true' : 'false'),
api.updateSetting('presentation_auto_progress', presentationAutoProgress ? 'true' : 'false'),
]);
showToast('Präsentations-Einstellungen gespeichert', 'e-toast-success');
} catch {
showToast('Fehler beim Speichern der Präsentations-Einstellungen', 'e-toast-danger');
} finally {
setPresentationBusy(false);
}
};
const { user } = useAuth();
const toastRef = React.useRef<ToastComponent>(null);
@@ -358,8 +402,44 @@ const Einstellungen: React.FC = () => {
<div className="e-card-header-title">Präsentationen</div>
</div>
</div>
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
Platzhalter für Standardwerte (Autoplay, Loop, Intervall) für Präsentationen.
<div className="e-card-content" style={{ fontSize: 14 }}>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Slide-Show Intervall (Sekunden)
</label>
<NumericTextBoxComponent
min={1}
max={600}
step={1}
value={presentationInterval}
format="n0"
change={(e: { value: number }) => setPresentationInterval(Number(e.value) || 10)}
placeholder="Intervall in Sekunden"
width="120px"
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationPageProgress}
change={e => setPresentationPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Präsentationsfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationAutoProgress}
change={e => setPresentationAutoProgress(e.checked || false)}
/>
</div>
<ButtonComponent
cssClass="e-primary"
onClick={onSavePresentationSettings}
style={{ marginTop: 8 }}
disabled={presentationBusy}
>
{presentationBusy ? 'Speichere…' : 'Einstellungen speichern'}
</ButtonComponent>
</div>
</div>