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
.gitignore
vendored
1
.gitignore
vendored
@@ -74,3 +74,4 @@ dashboard/pages/test.py
|
|||||||
dashboard/sidebar_test.py
|
dashboard/sidebar_test.py
|
||||||
dashboard/assets/responsive-sidebar.css
|
dashboard/assets/responsive-sidebar.css
|
||||||
dashboard/src/nested_tabs.js
|
dashboard/src/nested_tabs.js
|
||||||
|
scheduler/scheduler.log.2
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"appName": "Infoscreen-Management",
|
"appName": "Infoscreen-Management",
|
||||||
"version": "2025.1.0-alpha.13",
|
"version": "2026.1.0-alpha.13",
|
||||||
"copyright": "© 2025 Third-Age-Applications",
|
"copyright": "© 2026 Third-Age-Applications",
|
||||||
"supportContact": "support@third-age-applications.com",
|
"supportContact": "support@third-age-applications.com",
|
||||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||||
"techStack": {
|
"techStack": {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { useToast } from './components/ToastProvider';
|
|||||||
const Layout: React.FC = () => {
|
const Layout: React.FC = () => {
|
||||||
const [version, setVersion] = useState('');
|
const [version, setVersion] = useState('');
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const [organizationName, setOrganizationName] = useState('');
|
||||||
let sidebarRef: SidebarComponent | null;
|
let sidebarRef: SidebarComponent | null;
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -80,6 +81,25 @@ const Layout: React.FC = () => {
|
|||||||
.catch(err => console.error('Failed to load version info:', err));
|
.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 = () => {
|
const toggleSidebar = () => {
|
||||||
if (sidebarRef) {
|
if (sidebarRef) {
|
||||||
sidebarRef.toggle();
|
sidebarRef.toggle();
|
||||||
@@ -346,9 +366,11 @@ const Layout: React.FC = () => {
|
|||||||
Infoscreen-Management
|
Infoscreen-Management
|
||||||
</span>
|
</span>
|
||||||
<div style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 16 }}>
|
<div style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 16 }}>
|
||||||
|
{organizationName && (
|
||||||
<span className="text-lg font-medium" style={{ color: '#78591c' }}>
|
<span className="text-lg font-medium" style={{ color: '#78591c' }}>
|
||||||
[Organisationsname]
|
{organizationName}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<DropDownButtonComponent
|
<DropDownButtonComponent
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
@@ -137,3 +137,32 @@ export async function updateHolidayBannerSetting(
|
|||||||
}
|
}
|
||||||
return response.json();
|
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 [videoMuted, setVideoMuted] = React.useState<boolean>(false);
|
||||||
const [videoBusy, setVideoBusy] = 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') => {
|
const showToast = (content: string, cssClass: string = 'e-toast-success') => {
|
||||||
if (toastRef.current) {
|
if (toastRef.current) {
|
||||||
toastRef.current.show({
|
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 () => {
|
const loadAcademicPeriods = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [list, active] = await Promise.all([
|
const [list, active] = await Promise.all([
|
||||||
@@ -179,8 +211,13 @@ const Einstellungen: React.FC = () => {
|
|||||||
loadPresentationSettings();
|
loadPresentationSettings();
|
||||||
loadVideoSettings();
|
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 () => {
|
const onUpload = async () => {
|
||||||
if (!file) return;
|
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
|
// Determine which tabs to show based on role
|
||||||
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
||||||
const isSuperadmin = !!(user && user.role === 'superadmin');
|
const isSuperadmin = !!(user && user.role === 'superadmin');
|
||||||
@@ -700,8 +767,30 @@ const Einstellungen: React.FC = () => {
|
|||||||
<div className="e-card-header-title">Organisationsinformationen</div>
|
<div className="e-card-header-title">Organisationsinformationen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
<div className="e-card-content" style={{ padding: '16px' }}>
|
||||||
Platzhalter für Organisationsname, Branding, Standard-Lokalisierung.
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -715,8 +804,35 @@ const Einstellungen: React.FC = () => {
|
|||||||
<div className="e-card-header-title">Erweiterte Konfiguration</div>
|
<div className="e-card-header-title">Erweiterte Konfiguration</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
<div className="e-card-content" style={{ padding: '16px' }}>
|
||||||
Platzhalter für System-weit fortgeschrittene Optionen.
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
|||||||
import logging
|
import logging
|
||||||
from sqlalchemy.orm import sessionmaker, joinedload
|
from sqlalchemy.orm import sessionmaker, joinedload
|
||||||
from sqlalchemy import create_engine, or_, and_, text
|
from sqlalchemy import create_engine, or_, and_, text
|
||||||
from models.models import Event, EventMedia, EventException
|
from models.models import Event, EventMedia, EventException, SystemSetting
|
||||||
from dateutil.rrule import rrulestr
|
from dateutil.rrule import rrulestr
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
@@ -168,6 +168,22 @@ def get_active_events(start: datetime, end: datetime, group_id: int = None):
|
|||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_setting_value(key: str, default: str | None = None) -> str | None:
|
||||||
|
"""Fetch a system setting value by key from DB.
|
||||||
|
|
||||||
|
Returns the setting's string value or the provided default if missing.
|
||||||
|
"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
setting = session.query(SystemSetting).filter_by(key=key).first()
|
||||||
|
return setting.value if setting else default
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"[Scheduler] Failed to read system setting '{key}': {e}")
|
||||||
|
return default
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
def format_event_with_media(event):
|
def format_event_with_media(event):
|
||||||
"""Transform Event + EventMedia into client-expected format"""
|
"""Transform Event + EventMedia into client-expected format"""
|
||||||
event_dict = {
|
event_dict = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from .db_utils import get_active_events
|
from .db_utils import get_active_events, get_system_setting_value
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
@@ -37,6 +37,11 @@ def main():
|
|||||||
|
|
||||||
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
|
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
|
||||||
# 0 = aus; z.B. 600 für alle 10 Min
|
# 0 = aus; z.B. 600 für alle 10 Min
|
||||||
|
# initial value from DB or fallback to env
|
||||||
|
try:
|
||||||
|
db_val = get_system_setting_value("refresh_seconds", None)
|
||||||
|
REFRESH_SECONDS = int(db_val) if db_val is not None else int(os.getenv("REFRESH_SECONDS", "0"))
|
||||||
|
except Exception:
|
||||||
REFRESH_SECONDS = int(os.getenv("REFRESH_SECONDS", "0"))
|
REFRESH_SECONDS = int(os.getenv("REFRESH_SECONDS", "0"))
|
||||||
# Konfigurierbares Zeitfenster in Tagen (Standard: 7)
|
# Konfigurierbares Zeitfenster in Tagen (Standard: 7)
|
||||||
WINDOW_DAYS = int(os.getenv("EVENTS_WINDOW_DAYS", "7"))
|
WINDOW_DAYS = int(os.getenv("EVENTS_WINDOW_DAYS", "7"))
|
||||||
@@ -58,6 +63,12 @@ def main():
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
# refresh interval can change at runtime (superadmin settings)
|
||||||
|
try:
|
||||||
|
db_val = get_system_setting_value("refresh_seconds", None)
|
||||||
|
REFRESH_SECONDS = int(db_val) if db_val is not None else REFRESH_SECONDS
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# Query window: next N days to capture upcoming events and recurring instances
|
# Query window: next N days to capture upcoming events and recurring instances
|
||||||
# Clients need to know what's coming, not just what's active right now
|
# Clients need to know what's coming, not just what's active right now
|
||||||
end_window = now + datetime.timedelta(days=WINDOW_DAYS)
|
end_window = now + datetime.timedelta(days=WINDOW_DAYS)
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ with engine.connect() as conn:
|
|||||||
('video_loop', 'true', 'Loop (Wiederholung) für Videos'),
|
('video_loop', 'true', 'Loop (Wiederholung) für Videos'),
|
||||||
('video_volume', '0.8', 'Standard Lautstärke für Videos (0.0 - 1.0)'),
|
('video_volume', '0.8', 'Standard Lautstärke für Videos (0.0 - 1.0)'),
|
||||||
('holiday_banner_enabled', 'true', 'Ferienstatus-Banner auf Dashboard anzeigen'),
|
('holiday_banner_enabled', 'true', 'Ferienstatus-Banner auf Dashboard anzeigen'),
|
||||||
|
('organization_name', '', 'Name der Organisation (wird im Header angezeigt)'),
|
||||||
|
('refresh_seconds', '0', 'Scheduler Republish-Intervall (Sekunden; 0 deaktiviert)'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, value, description in default_settings:
|
for key, value, description in default_settings:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Provides key-value storage for system-wide configuration.
|
|||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from server.database import Session
|
from server.database import Session
|
||||||
from models.models import SystemSetting
|
from models.models import SystemSetting
|
||||||
from server.permissions import admin_or_higher
|
from server.permissions import admin_or_higher, superadmin_only
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
system_settings_bp = Blueprint('system_settings', __name__, url_prefix='/api/system-settings')
|
system_settings_bp = Blueprint('system_settings', __name__, url_prefix='/api/system-settings')
|
||||||
@@ -31,11 +31,10 @@ def get_all_settings():
|
|||||||
|
|
||||||
|
|
||||||
@system_settings_bp.route('/<key>', methods=['GET'])
|
@system_settings_bp.route('/<key>', methods=['GET'])
|
||||||
@admin_or_higher
|
|
||||||
def get_setting(key):
|
def get_setting(key):
|
||||||
"""
|
"""
|
||||||
Get a specific system setting by key.
|
Get a specific system setting by key.
|
||||||
Admin+ only.
|
Public endpoint - settings are read-only configuration.
|
||||||
"""
|
"""
|
||||||
session = Session()
|
session = Session()
|
||||||
try:
|
try:
|
||||||
@@ -265,3 +264,66 @@ def update_holiday_banner_setting():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
@system_settings_bp.route('/organization-name', methods=['GET'])
|
||||||
|
def get_organization_name():
|
||||||
|
"""
|
||||||
|
Get organization name.
|
||||||
|
Public endpoint - header needs this.
|
||||||
|
"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
setting = session.query(SystemSetting).filter_by(key='organization_name').first()
|
||||||
|
name = setting.value if setting and setting.value else ''
|
||||||
|
|
||||||
|
return jsonify({'name': name}), 200
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
@system_settings_bp.route('/organization-name', methods=['POST'])
|
||||||
|
@superadmin_only
|
||||||
|
def update_organization_name():
|
||||||
|
"""
|
||||||
|
Update organization name.
|
||||||
|
Superadmin only.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"name": "Meine Organisation"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No data provided'}), 400
|
||||||
|
|
||||||
|
name = data.get('name', '')
|
||||||
|
|
||||||
|
# Update or create setting
|
||||||
|
setting = session.query(SystemSetting).filter_by(key='organization_name').first()
|
||||||
|
if setting:
|
||||||
|
setting.value = name
|
||||||
|
else:
|
||||||
|
setting = SystemSetting(
|
||||||
|
key='organization_name',
|
||||||
|
value=name,
|
||||||
|
description='Name der Organisation (wird im Header angezeigt)'
|
||||||
|
)
|
||||||
|
session.add(setting)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'name': name,
|
||||||
|
'message': 'Organization name updated successfully'
|
||||||
|
}), 200
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user