feat: Add organization name and scheduler refresh interval settings
- Superadmin-only organization name setting displayed in dashboard header - Advanced Options tab with configurable scheduler refresh interval (0 = disabled) - Make system settings GET endpoint public for frontend reads - Scheduler reads refresh_seconds from DB dynamically each loop - Seed default system settings in init_defaults.py
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2025.1.0-alpha.13",
|
||||
"copyright": "© 2025 Third-Age-Applications",
|
||||
"version": "2026.1.0-alpha.13",
|
||||
"copyright": "© 2026 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
"techStack": {
|
||||
|
||||
@@ -61,6 +61,7 @@ import { useToast } from './components/ToastProvider';
|
||||
const Layout: React.FC = () => {
|
||||
const [version, setVersion] = useState('');
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [organizationName, setOrganizationName] = useState('');
|
||||
let sidebarRef: SidebarComponent | null;
|
||||
const { user } = useAuth();
|
||||
const toast = useToast();
|
||||
@@ -80,6 +81,25 @@ const Layout: React.FC = () => {
|
||||
.catch(err => console.error('Failed to load version info:', err));
|
||||
}, []);
|
||||
|
||||
// Load organization name
|
||||
React.useEffect(() => {
|
||||
const loadOrgName = async () => {
|
||||
try {
|
||||
const { getOrganizationName } = await import('./apiSystemSettings');
|
||||
const data = await getOrganizationName();
|
||||
setOrganizationName(data.name || '');
|
||||
} catch (err) {
|
||||
console.error('Failed to load organization name:', err);
|
||||
}
|
||||
};
|
||||
loadOrgName();
|
||||
|
||||
// Listen for organization name updates from Settings page
|
||||
const handleUpdate = () => loadOrgName();
|
||||
window.addEventListener('organizationNameUpdated', handleUpdate);
|
||||
return () => window.removeEventListener('organizationNameUpdated', handleUpdate);
|
||||
}, []);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (sidebarRef) {
|
||||
sidebarRef.toggle();
|
||||
@@ -346,9 +366,11 @@ const Layout: React.FC = () => {
|
||||
Infoscreen-Management
|
||||
</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 16 }}>
|
||||
<span className="text-lg font-medium" style={{ color: '#78591c' }}>
|
||||
[Organisationsname]
|
||||
</span>
|
||||
{organizationName && (
|
||||
<span className="text-lg font-medium" style={{ color: '#78591c' }}>
|
||||
{organizationName}
|
||||
</span>
|
||||
)}
|
||||
{user && (
|
||||
<DropDownButtonComponent
|
||||
items={[
|
||||
|
||||
@@ -137,3 +137,32 @@ export async function updateHolidayBannerSetting(
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization name (public endpoint)
|
||||
*/
|
||||
export async function getOrganizationName(): Promise<{ name: string }> {
|
||||
const response = await fetch(`/api/system-settings/organization-name`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch organization name: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization name (superadmin only)
|
||||
*/
|
||||
export async function updateOrganizationName(name: string): Promise<{ name: string; message: string }> {
|
||||
const response = await fetch(`/api/system-settings/organization-name`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update organization name: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ const Einstellungen: React.FC = () => {
|
||||
const [videoMuted, setVideoMuted] = React.useState<boolean>(false);
|
||||
const [videoBusy, setVideoBusy] = React.useState<boolean>(false);
|
||||
|
||||
// Organization name state (Superadmin only)
|
||||
const [organizationName, setOrganizationName] = React.useState<string>('');
|
||||
const [organizationBusy, setOrganizationBusy] = React.useState<boolean>(false);
|
||||
|
||||
// Advanced options: Scheduler refresh interval (Superadmin)
|
||||
const [refreshSeconds, setRefreshSeconds] = React.useState<number>(0);
|
||||
const [refreshBusy, setRefreshBusy] = React.useState<boolean>(false);
|
||||
|
||||
const showToast = (content: string, cssClass: string = 'e-toast-success') => {
|
||||
if (toastRef.current) {
|
||||
toastRef.current.show({
|
||||
@@ -153,6 +161,30 @@ const Einstellungen: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load organization name (superadmin only, but endpoint is public)
|
||||
const loadOrganizationName = React.useCallback(async () => {
|
||||
try {
|
||||
const api = await import('./apiSystemSettings');
|
||||
const data = await api.getOrganizationName();
|
||||
setOrganizationName(data.name || '');
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden des Organisationsnamens:', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load scheduler refresh seconds (superadmin)
|
||||
const loadRefreshSeconds = React.useCallback(async () => {
|
||||
try {
|
||||
const api = await import('./apiSystemSettings');
|
||||
const setting = await api.getSetting('refresh_seconds');
|
||||
const parsed = Number(setting.value ?? 0);
|
||||
setRefreshSeconds(Number.isFinite(parsed) ? parsed : 0);
|
||||
} catch {
|
||||
// Default to 0 if not present
|
||||
setRefreshSeconds(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadAcademicPeriods = React.useCallback(async () => {
|
||||
try {
|
||||
const [list, active] = await Promise.all([
|
||||
@@ -179,8 +211,13 @@ const Einstellungen: React.FC = () => {
|
||||
loadPresentationSettings();
|
||||
loadVideoSettings();
|
||||
}
|
||||
// Organization name only for superadmin
|
||||
if (user.role === 'superadmin') {
|
||||
loadOrganizationName();
|
||||
loadRefreshSeconds();
|
||||
}
|
||||
}
|
||||
}, [refresh, loadSupplementSettings, loadAcademicPeriods, loadPresentationSettings, loadVideoSettings, loadHolidayBannerSetting, user]);
|
||||
}, [refresh, loadSupplementSettings, loadAcademicPeriods, loadPresentationSettings, loadVideoSettings, loadHolidayBannerSetting, loadOrganizationName, loadRefreshSeconds, user]);
|
||||
|
||||
const onUpload = async () => {
|
||||
if (!file) return;
|
||||
@@ -254,6 +291,36 @@ const Einstellungen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveOrganizationName = async () => {
|
||||
setOrganizationBusy(true);
|
||||
try {
|
||||
const api = await import('./apiSystemSettings');
|
||||
await api.updateOrganizationName(organizationName);
|
||||
showToast('Organisationsname gespeichert', 'e-toast-success');
|
||||
// Trigger a custom event to notify App.tsx to refresh the header
|
||||
window.dispatchEvent(new Event('organizationNameUpdated'));
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern des Organisationsnamens';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
} finally {
|
||||
setOrganizationBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveRefreshSeconds = async () => {
|
||||
setRefreshBusy(true);
|
||||
try {
|
||||
const api = await import('./apiSystemSettings');
|
||||
await api.updateSetting('refresh_seconds', String(refreshSeconds));
|
||||
showToast('Scheduler-Refresh-Intervall gespeichert', 'e-toast-success');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern des Intervalls';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
} finally {
|
||||
setRefreshBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Determine which tabs to show based on role
|
||||
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
||||
const isSuperadmin = !!(user && user.role === 'superadmin');
|
||||
@@ -700,8 +767,30 @@ const Einstellungen: React.FC = () => {
|
||||
<div className="e-card-header-title">Organisationsinformationen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Organisationsname, Branding, Standard-Lokalisierung.
|
||||
<div className="e-card-content" style={{ padding: '16px' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 600 }}>
|
||||
Organisationsname
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
placeholder="z.B. Meine Schule GmbH"
|
||||
value={organizationName}
|
||||
change={(e) => setOrganizationName(e.value || '')}
|
||||
cssClass="e-outline"
|
||||
floatLabelType="Never"
|
||||
style={{ width: '100%', maxWidth: '500px' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8, color: '#666', fontSize: 13 }}>
|
||||
Dieser Name wird im Header des Dashboards angezeigt.
|
||||
</div>
|
||||
</div>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={onSaveOrganizationName}
|
||||
disabled={organizationBusy}
|
||||
>
|
||||
{organizationBusy ? 'Speichern...' : 'Speichern'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -715,8 +804,35 @@ const Einstellungen: React.FC = () => {
|
||||
<div className="e-card-header-title">Erweiterte Konfiguration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für System-weit fortgeschrittene Optionen.
|
||||
<div className="e-card-content" style={{ padding: '16px' }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 600 }}>
|
||||
Scheduler: Refresh-Intervall (Sekunden)
|
||||
</label>
|
||||
<NumericTextBoxComponent
|
||||
value={refreshSeconds}
|
||||
min={0}
|
||||
step={1}
|
||||
format="n0"
|
||||
change={(e) => setRefreshSeconds(Number(e.value ?? 0))}
|
||||
cssClass="e-outline"
|
||||
width={200}
|
||||
/>
|
||||
<div style={{ marginTop: 8, color: '#666', fontSize: 13 }}>
|
||||
Legt fest, wie oft der Scheduler Ereignisse an Clients republiziert, auch wenn sich nichts geändert hat.
|
||||
<br />
|
||||
<strong>0:</strong> Deaktiviert (Scheduler sendet nur bei Änderungen).
|
||||
<br />
|
||||
<strong>>0:</strong> Sekunden zwischen Republizierungen (z.B. 600 = alle 10 Min).
|
||||
<br />
|
||||
Der Scheduler liest die Datenbank immer alle 30 Sekunden neu ab und sendet dann bei Bedarf die Änderungen an den Terminen.
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<ButtonComponent cssClass="e-primary" onClick={onSaveRefreshSeconds} disabled={refreshBusy}>
|
||||
{refreshBusy ? 'Speichern...' : 'Speichern'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user