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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2025.1.0-alpha.11",
|
||||
"version": "2025.1.0-alpha.12",
|
||||
"copyright": "© 2025 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -26,10 +26,21 @@
|
||||
]
|
||||
},
|
||||
"buildInfo": {
|
||||
"buildDate": "2025-09-20T11:00:00Z",
|
||||
"commitId": "8d1df7199cb7"
|
||||
"buildDate": "2025-10-18T14:00:00Z",
|
||||
"commitId": "9f2ae8b44c3a"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2025.1.0-alpha.12",
|
||||
"date": "2025-10-18",
|
||||
"changes": [
|
||||
"✨ Einstellungen › Events › Präsentationen: Neue Felder für Slide-Show Intervall, Seitenfortschritt (Page-Progress) und Präsentationsfortschritt (Auto-Progress) – inspiriert von Impressive Presenter (-q, -k).",
|
||||
"️ Event-Modal: Präsentations-Einstellungen werden beim Erstellen aus globalen Defaults geladen; beim Bearbeiten aus Event-Daten; individuell pro Event anpassbar.",
|
||||
"🐛 Bugfix: Scheduler sendet jetzt leere retained Messages (`[]`) wenn keine Events mehr aktiv sind (Client-Display wird korrekt gelöscht).",
|
||||
"🔧 Bugfix: Nur aktuell aktive Events werden via MQTT an Clients gesendet (reduziert Datenübertragung).",
|
||||
"📖 Doku: Copilot-Instructions um Präsentations-Settings, Scheduler-Logik und Event-Modal erweitert."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2025.1.0-alpha.11",
|
||||
"date": "2025-10-16",
|
||||
@@ -37,8 +48,7 @@
|
||||
"✨ Einstellungen-Seite: Neues Tab-Layout (Syncfusion) mit rollenbasierter Sichtbarkeit – Tabs: 📅 Akademischer Kalender, 🖥️ Anzeige & Clients, 🎬 Medien & Dateien, 🗓️ Events, ⚙️ System.",
|
||||
"🗓️ Einstellungen › Events: WebUntis/Vertretungsplan – Zusatz-Tabelle (URL) in den Events-Tab verschoben; Aktivieren/Deaktivieren, Speichern und Vorschau; systemweite Einstellung.",
|
||||
"📅 Einstellungen › Akademischer Kalender: Aktive akademische Periode kann direkt gesetzt werden.",
|
||||
"🛠️ Einstellungen (Technik): API-Aufrufe nutzen nun relative /api‑Pfade über den Vite‑Proxy (verhindert CORS bzw. doppeltes /api).",
|
||||
"📖 Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
|
||||
" Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,8 +87,7 @@
|
||||
"date": "2025-09-21",
|
||||
"changes": [
|
||||
"🧭 UI: Periode-Auswahl (Syncfusion) neben Gruppenauswahl; kompaktes Layout",
|
||||
"✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler ‘Ferien im Blick’",
|
||||
"🛠️ API: Endpunkte für akademische Perioden (list, active GET/POST, for_date)",
|
||||
"✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler 'Ferien im Blick'",
|
||||
"📅 Scheduler: Standardmäßig keine Terminierung in Ferien; Block-Darstellung wie Ganztagesereignis; schwarze Textfarbe",
|
||||
"📤 Ferien: Upload von TXT/CSV (headless TXT nutzt Spalten 2–4)",
|
||||
"🔧 UX: Schalter in einer Reihe; Dropdown-Breiten optimiert"
|
||||
@@ -89,11 +98,8 @@
|
||||
"date": "2025-09-20",
|
||||
"changes": [
|
||||
"🗓️ NEU: Akademische Perioden System - Unterstützung für Schuljahre, Semester und Trimester",
|
||||
"🏗️ DATENBANK: Neue 'academic_periods' Tabelle für zeitbasierte Organisation",
|
||||
"🔗 ERWEITERT: Events und Medien können jetzt optional einer akademischen Periode zugeordnet werden",
|
||||
"📊 ARCHITEKTUR: Vollständig rückwärtskompatible Implementierung für schrittweise Einführung",
|
||||
"🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen",
|
||||
"⚙️ TOOLS: Automatische Erstellung von Standard-Schuljahren für österreichische Schulen"
|
||||
"🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user