docs/dev: sync backend rework, MQTT, and devcontainer hygiene
README: add Versioning (unified SemVer, pre-releases, build metadata); emphasize UTC handling and streaming endpoint; add Dev Container notes (UI-only Remote Containers, npm ci, idempotent aliases) TECH-CHANGELOG: backend rework notes (serialization camelCase, UTC normalization, streaming metadata); add component build metadata template (image tags/SHAs) Copilot instructions: integrate maintenance guardrails; reinforce UTC and camelCase conventions; document MQTT topics and scheduler retained payload behavior Devcontainer: map Remote Containers to UI; remove in-container install; switch to npm ci; make aliases idempotent
This commit is contained in:
45
dashboard/package-lock.json
generated
45
dashboard/package-lock.json
generated
@@ -98,6 +98,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -385,6 +386,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -408,6 +410,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2052,6 +2055,7 @@
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2125,6 +2129,7 @@
|
||||
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.43.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
@@ -2357,6 +2362,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2712,6 +2718,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
@@ -3475,6 +3482,7 @@
|
||||
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3536,6 +3544,7 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -4990,18 +4999,6 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -5717,6 +5714,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -5783,6 +5781,7 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -5871,6 +5870,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5880,6 +5880,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -6784,6 +6785,7 @@
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -7022,6 +7024,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7152,6 +7155,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7257,6 +7261,7 @@
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -7350,6 +7355,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7580,21 +7586,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
|
||||
@@ -106,3 +106,34 @@ export async function updateSupplementTableSettings(
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get holiday banner setting
|
||||
*/
|
||||
export async function getHolidayBannerSetting(): Promise<{ enabled: boolean }> {
|
||||
const response = await fetch(`/api/system-settings/holiday-banner`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch holiday banner setting: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update holiday banner setting
|
||||
*/
|
||||
export async function updateHolidayBannerSetting(
|
||||
enabled: boolean
|
||||
): Promise<{ enabled: boolean; message: string }> {
|
||||
const response = await fetch(`/api/system-settings/holiday-banner`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update holiday banner setting: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { fetchGroupsWithClients, restartClient } from './apiClients';
|
||||
import type { Group, Client } from './apiClients';
|
||||
import { fetchEvents } from './apiEvents';
|
||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { ToastComponent } from '@syncfusion/ej2-react-notifications';
|
||||
import { ToastComponent, MessageComponent } from '@syncfusion/ej2-react-notifications';
|
||||
import { listHolidays } from './apiHolidays';
|
||||
import { getActiveAcademicPeriod, type AcademicPeriod } from './apiAcademicPeriods';
|
||||
import { getHolidayBannerSetting } from './apiSystemSettings';
|
||||
|
||||
const REFRESH_INTERVAL = 15000; // 15 Sekunden
|
||||
|
||||
@@ -31,6 +34,15 @@ const Dashboard: React.FC = () => {
|
||||
const [activeEvents, setActiveEvents] = useState<GroupEvents>({});
|
||||
const toastRef = React.useRef<ToastComponent>(null);
|
||||
|
||||
// Holiday status state
|
||||
const [holidayBannerEnabled, setHolidayBannerEnabled] = useState<boolean>(true);
|
||||
const [activePeriod, setActivePeriod] = useState<AcademicPeriod | null>(null);
|
||||
const [holidayOverlapCount, setHolidayOverlapCount] = useState<number>(0);
|
||||
const [holidayFirst, setHolidayFirst] = useState<string | null>(null);
|
||||
const [holidayLast, setHolidayLast] = useState<string | null>(null);
|
||||
const [holidayLoading, setHolidayLoading] = useState<boolean>(false);
|
||||
const [holidayError, setHolidayError] = useState<string | null>(null);
|
||||
|
||||
// Optimiertes Update: Nur bei echten Datenänderungen wird das Grid aktualisiert
|
||||
useEffect(() => {
|
||||
let lastGroups: Group[] = [];
|
||||
@@ -72,6 +84,62 @@ const Dashboard: React.FC = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Load academic period & holidays status
|
||||
useEffect(() => {
|
||||
const loadHolidayStatus = async () => {
|
||||
// Check if banner is enabled first
|
||||
try {
|
||||
const bannerSetting = await getHolidayBannerSetting();
|
||||
setHolidayBannerEnabled(bannerSetting.enabled);
|
||||
if (!bannerSetting.enabled) {
|
||||
return; // Skip loading if disabled
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Banner-Einstellung:', e);
|
||||
// Continue with default (enabled)
|
||||
}
|
||||
|
||||
setHolidayLoading(true);
|
||||
setHolidayError(null);
|
||||
try {
|
||||
const period = await getActiveAcademicPeriod();
|
||||
setActivePeriod(period || null);
|
||||
const holidayData = await listHolidays();
|
||||
const list = holidayData.holidays || [];
|
||||
|
||||
if (period) {
|
||||
// Check for holidays overlapping with active period
|
||||
const ps = new Date(period.start_date + 'T00:00:00');
|
||||
const pe = new Date(period.end_date + 'T23:59:59');
|
||||
const overlapping = list.filter(h => {
|
||||
const hs = new Date(h.start_date + 'T00:00:00');
|
||||
const he = new Date(h.end_date + 'T23:59:59');
|
||||
return hs <= pe && he >= ps;
|
||||
});
|
||||
setHolidayOverlapCount(overlapping.length);
|
||||
if (overlapping.length > 0) {
|
||||
const sorted = overlapping.slice().sort((a, b) => a.start_date.localeCompare(b.start_date));
|
||||
setHolidayFirst(sorted[0].start_date);
|
||||
setHolidayLast(sorted[sorted.length - 1].end_date);
|
||||
} else {
|
||||
setHolidayFirst(null);
|
||||
setHolidayLast(null);
|
||||
}
|
||||
} else {
|
||||
setHolidayOverlapCount(0);
|
||||
setHolidayFirst(null);
|
||||
setHolidayLast(null);
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Ferienstatus konnte nicht geladen werden';
|
||||
setHolidayError(msg);
|
||||
} finally {
|
||||
setHolidayLoading(false);
|
||||
}
|
||||
};
|
||||
loadHolidayStatus();
|
||||
}, []);
|
||||
|
||||
// Fetch currently active events for all groups
|
||||
const fetchActiveEventsForGroups = async (groupsList: Group[]) => {
|
||||
const now = new Date();
|
||||
@@ -344,6 +412,55 @@ const Dashboard: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Format date for holiday display
|
||||
const formatDate = (iso: string | null) => {
|
||||
if (!iso) return '-';
|
||||
try {
|
||||
const d = new Date(iso + 'T00:00:00');
|
||||
return d.toLocaleDateString('de-DE');
|
||||
} catch { return iso; }
|
||||
};
|
||||
|
||||
// Holiday Status Banner Component
|
||||
const HolidayStatusBanner = () => {
|
||||
if (holidayLoading) {
|
||||
return (
|
||||
<MessageComponent severity="Info" variant="Filled">
|
||||
Lade Ferienstatus ...
|
||||
</MessageComponent>
|
||||
);
|
||||
}
|
||||
if (holidayError) {
|
||||
return (
|
||||
<MessageComponent severity="Error" variant="Filled">
|
||||
Fehler beim Laden des Ferienstatus: {holidayError}
|
||||
</MessageComponent>
|
||||
);
|
||||
}
|
||||
if (!activePeriod) {
|
||||
return (
|
||||
<MessageComponent severity="Warning" variant="Outlined">
|
||||
⚠️ Keine aktive akademische Periode gesetzt – Ferienplan nicht verknüpft.
|
||||
</MessageComponent>
|
||||
);
|
||||
}
|
||||
if (holidayOverlapCount > 0) {
|
||||
return (
|
||||
<MessageComponent severity="Success" variant="Filled">
|
||||
✅ Ferienplan vorhanden für <strong>{activePeriod.display_name || activePeriod.name}</strong>: {holidayOverlapCount} Zeitraum{holidayOverlapCount === 1 ? '' : 'e'}
|
||||
{holidayFirst && holidayLast && (
|
||||
<> ({formatDate(holidayFirst)} – {formatDate(holidayLast)})</>
|
||||
)}
|
||||
</MessageComponent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MessageComponent severity="Warning" variant="Filled">
|
||||
⚠️ Kein Ferienplan für <strong>{activePeriod.display_name || activePeriod.name}</strong> importiert. Jetzt unter Einstellungen → 📅 Kalender hochladen.
|
||||
</MessageComponent>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ToastComponent
|
||||
@@ -361,6 +478,13 @@ const Dashboard: React.FC = () => {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Holiday Status Banner */}
|
||||
{holidayBannerEnabled && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<HolidayStatusBanner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Statistics Summary */}
|
||||
{(() => {
|
||||
const globalStats = getGlobalStats();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NumericTextBoxComponent, TextBoxComponent } from '@syncfusion/ej2-react
|
||||
import { ButtonComponent, 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 { getSupplementTableSettings, updateSupplementTableSettings, getHolidayBannerSetting, updateHolidayBannerSetting } from './apiSystemSettings';
|
||||
import { useAuth } from './useAuth';
|
||||
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
|
||||
import { listAcademicPeriods, getActiveAcademicPeriod, setActiveAcademicPeriod, type AcademicPeriod } from './apiAcademicPeriods';
|
||||
@@ -77,6 +77,10 @@ const Einstellungen: React.FC = () => {
|
||||
const [supplementEnabled, setSupplementEnabled] = React.useState(false);
|
||||
const [supplementBusy, setSupplementBusy] = React.useState(false);
|
||||
|
||||
// Holiday banner state
|
||||
const [holidayBannerEnabled, setHolidayBannerEnabled] = React.useState(true);
|
||||
const [holidayBannerBusy, setHolidayBannerBusy] = React.useState(false);
|
||||
|
||||
// Video defaults state (Admin+)
|
||||
const [videoAutoplay, setVideoAutoplay] = React.useState<boolean>(true);
|
||||
const [videoLoop, setVideoLoop] = React.useState<boolean>(true);
|
||||
@@ -116,6 +120,15 @@ const Einstellungen: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadHolidayBannerSetting = React.useCallback(async () => {
|
||||
try {
|
||||
const data = await getHolidayBannerSetting();
|
||||
setHolidayBannerEnabled(data.enabled);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Ferienbanner-Einstellung:', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load video default settings (with fallbacks)
|
||||
const loadVideoSettings = React.useCallback(async () => {
|
||||
try {
|
||||
@@ -156,6 +169,7 @@ const Einstellungen: React.FC = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
loadHolidayBannerSetting(); // Everyone can see this
|
||||
if (user) {
|
||||
// Academic periods for all users
|
||||
loadAcademicPeriods();
|
||||
@@ -166,7 +180,7 @@ const Einstellungen: React.FC = () => {
|
||||
loadVideoSettings();
|
||||
}
|
||||
}
|
||||
}, [refresh, loadSupplementSettings, loadAcademicPeriods, loadPresentationSettings, loadVideoSettings, user]);
|
||||
}, [refresh, loadSupplementSettings, loadAcademicPeriods, loadPresentationSettings, loadVideoSettings, loadHolidayBannerSetting, user]);
|
||||
|
||||
const onUpload = async () => {
|
||||
if (!file) return;
|
||||
@@ -208,6 +222,19 @@ const Einstellungen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveHolidayBannerSetting = async () => {
|
||||
setHolidayBannerBusy(true);
|
||||
try {
|
||||
await updateHolidayBannerSetting(holidayBannerEnabled);
|
||||
showToast('Ferienbanner-Einstellung gespeichert', 'e-toast-success');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
showToast(msg, 'e-toast-danger');
|
||||
} finally {
|
||||
setHolidayBannerBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveVideoSettings = async () => {
|
||||
setVideoBusy(true);
|
||||
try {
|
||||
@@ -291,6 +318,34 @@ const Einstellungen: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Display Settings Card */}
|
||||
<div className="e-card" style={{ marginTop: 20 }}>
|
||||
<div className="e-card-header">
|
||||
<div className="e-card-header-caption">
|
||||
<div className="e-card-header-title">Dashboard-Anzeige</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card-content">
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CheckBoxComponent
|
||||
label="Ferienstatus-Banner auf Dashboard anzeigen"
|
||||
checked={holidayBannerEnabled}
|
||||
change={(e) => setHolidayBannerEnabled(e.checked || false)}
|
||||
/>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: 4, marginLeft: 24 }}>
|
||||
Zeigt eine Information an, ob ein Ferienplan für die aktive Periode importiert wurde.
|
||||
</div>
|
||||
</div>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={onSaveHolidayBannerSetting}
|
||||
disabled={holidayBannerBusy}
|
||||
>
|
||||
{holidayBannerBusy ? 'Speichere…' : 'Einstellung speichern'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user