docs(settings): Update README + Copilot instructions; bump Program Info to 2025.1.0-alpha.11
README: Add System Settings API endpoints; describe new tabbed Settings layout with role gating; add Vite dev proxy tip to use relative /api paths. Copilot instructions: Note SystemSetting key–value store in data model; document system_settings.py (CRUD + supplement-table convenience endpoint); reference apiSystemSettings.ts; note defaults seeding via init_defaults.py. Program Info: Bump version to 2025.1.0-alpha.11; changelog explicitly tied to the Settings page (Events tab: supplement-table URL moved; Academic Calendar: set active period; proxy note); README docs mention. No functional changes to API or UI code in this commit; documentation and program info only.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2025.1.0-alpha.10",
|
||||
"version": "2025.1.0-alpha.11",
|
||||
"copyright": "© 2025 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -30,6 +30,17 @@
|
||||
"commitId": "8d1df7199cb7"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2025.1.0-alpha.11",
|
||||
"date": "2025-10-16",
|
||||
"changes": [
|
||||
"✨ 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."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2025.1.0-alpha.10",
|
||||
"date": "2025-10-15",
|
||||
|
||||
108
dashboard/src/apiSystemSettings.ts
Normal file
108
dashboard/src/apiSystemSettings.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* API client for system settings
|
||||
*/
|
||||
|
||||
|
||||
export interface SystemSetting {
|
||||
key: string;
|
||||
value: string | null;
|
||||
description: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface SupplementTableSettings {
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all system settings
|
||||
*/
|
||||
export async function getAllSettings(): Promise<{ settings: SystemSetting[] }> {
|
||||
const response = await fetch(`/api/system-settings`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch settings: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting by key
|
||||
*/
|
||||
export async function getSetting(key: string): Promise<SystemSetting> {
|
||||
const response = await fetch(`/api/system-settings/${key}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch setting: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create a setting
|
||||
*/
|
||||
export async function updateSetting(
|
||||
key: string,
|
||||
value: string,
|
||||
description?: string
|
||||
): Promise<SystemSetting> {
|
||||
const response = await fetch(`/api/system-settings/${key}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ value, description }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update setting: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a setting
|
||||
*/
|
||||
export async function deleteSetting(key: string): Promise<{ message: string }> {
|
||||
const response = await fetch(`/api/system-settings/${key}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete setting: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supplement table settings
|
||||
*/
|
||||
export async function getSupplementTableSettings(): Promise<SupplementTableSettings> {
|
||||
const response = await fetch(`/api/system-settings/supplement-table`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch supplement table settings: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update supplement table settings
|
||||
*/
|
||||
export async function updateSupplementTableSettings(
|
||||
url: string,
|
||||
enabled: boolean
|
||||
): Promise<SupplementTableSettings & { message: string }> {
|
||||
const response = await fetch(`/api/system-settings/supplement-table`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ url, enabled }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update supplement table settings: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -1,11 +1,46 @@
|
||||
import React from 'react';
|
||||
import { TabComponent, TabItemDirective, TabItemsDirective } from '@syncfusion/ej2-react-navigations';
|
||||
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { ToastComponent } from '@syncfusion/ej2-react-notifications';
|
||||
import { listHolidays, uploadHolidaysCsv, type Holiday } from './apiHolidays';
|
||||
import { getSupplementTableSettings, updateSupplementTableSettings } from './apiSystemSettings';
|
||||
import { useAuth } from './useAuth';
|
||||
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
|
||||
import { listAcademicPeriods, getActiveAcademicPeriod, setActiveAcademicPeriod, type AcademicPeriod } from './apiAcademicPeriods';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const Einstellungen: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const toastRef = React.useRef<ToastComponent>(null);
|
||||
|
||||
// Holidays state
|
||||
const [file, setFile] = React.useState<File | null>(null);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [message, setMessage] = React.useState<string | null>(null);
|
||||
const [holidays, setHolidays] = React.useState<Holiday[]>([]);
|
||||
const [periods, setPeriods] = React.useState<AcademicPeriod[]>([]);
|
||||
const [activePeriodId, setActivePeriodId] = React.useState<number | null>(null);
|
||||
const periodOptions = React.useMemo(() =>
|
||||
periods.map(p => ({ id: p.id, name: p.display_name || p.name })),
|
||||
[periods]
|
||||
);
|
||||
|
||||
// Supplement table state
|
||||
const [supplementUrl, setSupplementUrl] = React.useState('');
|
||||
const [supplementEnabled, setSupplementEnabled] = React.useState(false);
|
||||
const [supplementBusy, setSupplementBusy] = React.useState(false);
|
||||
|
||||
const showToast = (content: string, cssClass: string = 'e-toast-success') => {
|
||||
if (toastRef.current) {
|
||||
toastRef.current.show({
|
||||
content,
|
||||
cssClass,
|
||||
timeOut: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = React.useCallback(async () => {
|
||||
try {
|
||||
@@ -14,12 +49,46 @@ const Einstellungen: React.FC = () => {
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Laden der Ferien';
|
||||
setMessage(msg);
|
||||
showToast(msg, 'e-toast-danger');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSupplementSettings = React.useCallback(async () => {
|
||||
try {
|
||||
const data = await getSupplementTableSettings();
|
||||
setSupplementUrl(data.url || '');
|
||||
setSupplementEnabled(data.enabled || false);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Laden der Vertretungsplan-Einstellungen';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadAcademicPeriods = React.useCallback(async () => {
|
||||
try {
|
||||
const [list, active] = await Promise.all([
|
||||
listAcademicPeriods(),
|
||||
getActiveAcademicPeriod(),
|
||||
]);
|
||||
setPeriods(list);
|
||||
setActivePeriodId(active ? active.id : null);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Laden der Schuljahre/Perioden';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
if (user) {
|
||||
// Academic periods for all users
|
||||
loadAcademicPeriods();
|
||||
// System settings only for admin/superadmin (will render only if allowed)
|
||||
if (['admin', 'superadmin'].includes(user.role)) {
|
||||
loadSupplementSettings();
|
||||
}
|
||||
}
|
||||
}, [refresh, loadSupplementSettings, loadAcademicPeriods, user]);
|
||||
|
||||
const onUpload = async () => {
|
||||
if (!file) return;
|
||||
@@ -27,59 +96,375 @@ const Einstellungen: React.FC = () => {
|
||||
setMessage(null);
|
||||
try {
|
||||
const res = await uploadHolidaysCsv(file);
|
||||
setMessage(`Import erfolgreich: ${res.inserted} neu, ${res.updated} aktualisiert.`);
|
||||
const msg = `Import erfolgreich: ${res.inserted} neu, ${res.updated} aktualisiert.`;
|
||||
setMessage(msg);
|
||||
showToast(msg, 'e-toast-success');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Import.';
|
||||
setMessage(msg);
|
||||
showToast(msg, 'e-toast-danger');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Einstellungen</h2>
|
||||
<div className="space-y-4">
|
||||
<section className="p-4 border rounded-md">
|
||||
<h3 className="font-semibold mb-2">Schulferien importieren</h3>
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Unterstützte Formate:
|
||||
<br />• CSV mit Kopfzeile: <code>name</code>, <code>start_date</code>,{' '}
|
||||
<code>end_date</code>, optional <code>region</code>
|
||||
<br />• TXT/CSV ohne Kopfzeile mit Spalten: interner Name, <strong>Name</strong>,{' '}
|
||||
<strong>Start (YYYYMMDD)</strong>, <strong>Ende (YYYYMMDD)</strong>, optional interne
|
||||
Info (ignoriert)
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv,.txt,text/plain"
|
||||
onChange={e => setFile(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
<button className="e-btn e-primary" onClick={onUpload} disabled={!file || busy}>
|
||||
{busy ? 'Importiere…' : 'CSV/TXT importieren'}
|
||||
</button>
|
||||
</div>
|
||||
{message && <div className="mt-2 text-sm">{message}</div>}
|
||||
</section>
|
||||
const onSaveSupplementSettings = async () => {
|
||||
setSupplementBusy(true);
|
||||
try {
|
||||
await updateSupplementTableSettings(supplementUrl, supplementEnabled);
|
||||
showToast('Vertretungsplan-Einstellungen erfolgreich gespeichert', 'e-toast-success');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern der Einstellungen';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
} finally {
|
||||
setSupplementBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
<section className="p-4 border rounded-md">
|
||||
<h3 className="font-semibold mb-2">Importierte Ferien</h3>
|
||||
{holidays.length === 0 ? (
|
||||
<div className="text-sm text-gray-600">Keine Einträge vorhanden.</div>
|
||||
) : (
|
||||
<ul className="text-sm list-disc pl-6">
|
||||
{holidays.slice(0, 20).map(h => (
|
||||
<li key={h.id}>
|
||||
{h.name}: {h.start_date} – {h.end_date}
|
||||
{h.region ? ` (${h.region})` : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
const onTestSupplementUrl = () => {
|
||||
if (supplementUrl) {
|
||||
window.open(supplementUrl, '_blank');
|
||||
} else {
|
||||
showToast('Bitte geben Sie zuerst eine URL ein', 'e-toast-warning');
|
||||
}
|
||||
};
|
||||
|
||||
// Determine which tabs to show based on role
|
||||
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
||||
const isSuperadmin = !!(user && user.role === 'superadmin');
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<ToastComponent ref={toastRef} position={{ X: 'Right', Y: 'Top' }} />
|
||||
|
||||
<h2 style={{ marginBottom: 20, fontSize: '24px', fontWeight: 600 }}>Einstellungen</h2>
|
||||
|
||||
<TabComponent heightAdjustMode="Auto">
|
||||
<TabItemsDirective>
|
||||
{/* 📅 Academic Calendar */}
|
||||
<TabItemDirective header={{ text: '📅 Akademischer Kalender' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
{/* Holidays Import */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Schulferien importieren</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<p style={{ marginBottom: 12, fontSize: '14px', color: '#666' }}>
|
||||
Unterstützte Formate:
|
||||
<br />• CSV mit Kopfzeile: <code>name</code>, <code>start_date</code>, <code>end_date</code>, optional <code>region</code>
|
||||
<br />• TXT/CSV ohne Kopfzeile mit Spalten: interner Name, <strong>Name</strong>, <strong>Start (YYYYMMDD)</strong>, <strong>Ende (YYYYMMDD)</strong>, optional interne Info (ignoriert)
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<input type="file" accept=".csv,text/csv,.txt,text/plain" onChange={e => setFile(e.target.files?.[0] ?? null)} />
|
||||
<ButtonComponent cssClass="e-primary" onClick={onUpload} disabled={!file || busy}>
|
||||
{busy ? 'Importiere…' : 'CSV/TXT importieren'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
{message && <div style={{ marginTop: 8, fontSize: '14px' }}>{message}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Imported Holidays List */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Importierte Ferien</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
{holidays.length === 0 ? (
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>Keine Einträge vorhanden.</div>
|
||||
) : (
|
||||
<ul style={{ fontSize: '14px', listStyle: 'disc', paddingLeft: 24 }}>
|
||||
{holidays.slice(0, 20).map(h => (
|
||||
<li key={h.id}>
|
||||
{h.name}: {h.start_date} – {h.end_date}
|
||||
{h.region ? ` (${h.region})` : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Academic Periods */}
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Akademische Perioden</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
{periods.length === 0 ? (
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>Keine Perioden gefunden.</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div style={{ minWidth: 260 }}>
|
||||
<DropDownListComponent
|
||||
dataSource={periodOptions}
|
||||
fields={{ text: 'name', value: 'id' }}
|
||||
value={activePeriodId ?? undefined}
|
||||
change={(e) => setActivePeriodId(Number(e.value))}
|
||||
placeholder="Aktive Periode wählen"
|
||||
popupHeight="250px"
|
||||
/>
|
||||
</div>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
disabled={activePeriodId == null}
|
||||
onClick={async () => {
|
||||
if (activePeriodId == null) return;
|
||||
try {
|
||||
const p = await setActiveAcademicPeriod(activePeriodId);
|
||||
showToast(`Aktive Periode gesetzt: ${p.display_name || p.name}`, 'e-toast-success');
|
||||
await loadAcademicPeriods();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Setzen der aktiven Periode';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Als aktiv setzen
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
|
||||
{/* 🖥️ Display & Clients (Admin+) */}
|
||||
{isAdmin && (
|
||||
<TabItemDirective header={{ text: '🖥️ Anzeige & Clients' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Standard-Einstellungen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Anzeige-Defaults (z. B. Heartbeat-Timeout, Screenshot-Aufbewahrung, Standard-Gruppe).
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Client-Konfiguration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Link to="/clients"><ButtonComponent>Infoscreen-Clients öffnen</ButtonComponent></Link>
|
||||
<Link to="/infoscr_groups"><ButtonComponent>Raumgruppen öffnen</ButtonComponent></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 🎬 Media & Files (Admin+) */}
|
||||
{isAdmin && (
|
||||
<TabItemDirective header={{ text: '🎬 Medien & Dateien' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Upload-Einstellungen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Upload-Limits, erlaubte Dateitypen und Standardspeicherorte.
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Konvertierungsstatus</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter – Konversionsstatus-Übersicht kann hier integriert werden (siehe API /api/conversions/...).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* <20>️ Events (Admin+): per-event-type defaults and WebUntis link settings */}
|
||||
{isAdmin && (
|
||||
<TabItemDirective header={{ text: '<27>️ Events' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
{/* WebUntis / Supplement table URL */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">WebUntis / Vertretungsplan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Vertretungsplan URL
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
placeholder="https://example.com/vertretungsplan"
|
||||
value={supplementUrl}
|
||||
change={(e) => setSupplementUrl(e.value || '')}
|
||||
cssClass="e-outline"
|
||||
width="100%"
|
||||
/>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
|
||||
Diese URL wird verwendet, um die Stundenplan-Änderungstabelle (Vertretungsplan) anzuzeigen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CheckBoxComponent
|
||||
label="Vertretungsplan aktiviert"
|
||||
checked={supplementEnabled}
|
||||
change={(e) => setSupplementEnabled(e.checked || false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={onSaveSupplementSettings}
|
||||
disabled={supplementBusy}
|
||||
>
|
||||
{supplementBusy ? 'Speichere…' : 'Einstellungen speichern'}
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-outline"
|
||||
onClick={onTestSupplementUrl}
|
||||
disabled={!supplementUrl}
|
||||
>
|
||||
Vorschau öffnen
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Presentation defaults */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Website defaults */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Webseiten</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Standardwerte (Lade-Timeout, Intervall, Sandbox/Embedding) für Webseiten.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video defaults */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Videos</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Standardwerte (Autoplay, Loop, Lautstärke) für Videos.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message defaults */}
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Mitteilungen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für Standardwerte (Dauer, Layout/Styling) für Mitteilungen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Other defaults */}
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Sonstige</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
|
||||
Platzhalter für sonstige Eventtypen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* 👥 Users (Admin+) */}
|
||||
{isAdmin && (
|
||||
<TabItemDirective header={{ text: '👥 Benutzer' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Schnellaktionen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Link to="/benutzer"><ButtonComponent cssClass="e-primary">Benutzerverwaltung öffnen</ButtonComponent></Link>
|
||||
<ButtonComponent disabled title="Demnächst">Benutzer einladen</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* ⚙️ System (Superadmin) */}
|
||||
{isSuperadmin && (
|
||||
<TabItemDirective header={{ text: '⚙️ System' }} content={() => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div className="e-card" style={{ marginBottom: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="e-card">
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
</TabItemsDirective>
|
||||
</TabComponent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user