(() => { const SECTIONS = [ { id: 'desktop', label: 'Desktop Type' }, { id: 'profile', label: 'Benutzerdaten' }, { id: 'apps', label: 'App-Installation' }, { id: 'widgets', label: 'Infobereich' }, ]; const escapeHtml = (value) => String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); const clone = (value) => JSON.parse(JSON.stringify(value)); const sameSelection = (left, right) => { const nextLeft = [...left].sort(); const nextRight = [...right].sort(); return JSON.stringify(nextLeft) === JSON.stringify(nextRight); }; const summarizeSection = (sectionId) => { if (sectionId === 'desktop') { return 'Windows-aehnlicher Standard fuer Desktop-Typ und Skin-Auswahl.'; } if (sectionId === 'profile') { return 'Lokales Profil fuer Name, E-Mail, Telefon und spaetere LDAP-/Keycloak-Synchronisierung.'; } if (sectionId === 'apps') { return 'Bestimmt, welche Desktop-Apps fuer diesen Benutzer sichtbar bleiben.'; } return 'Steuert, welche Elemente im rechten Infobereich des Desktops aktiv sind.'; }; const renderSkinChoices = (state) => state.bootstrap.options.skins.map((skin) => { const isActive = state.draft.desktop.active_skin === skin.skin_id; return ` `; }).join(''); const summarizeSkin = (skinId) => { if (skinId === 'apple') { return 'Top-Bar, rechte Icon-Anordnung und Finder-nahe Fensterelemente.'; } if (skinId === 'linux') { return 'Klassische Arbeitsflaeche mit neutralem Fensterstil und Linux-Taskbar.'; } return 'Standardisiertes Control-Panel-Muster mit Windows-naher Einstellungslogik.'; }; const renderAppItems = (state) => state.bootstrap.options.apps.map((app) => { const checked = state.draft.apps.enabled_ids.includes(app.app_id); return ` `; }).join(''); const renderWidgetItems = (state) => state.bootstrap.options.widgets.map((widget) => { const checked = state.draft.widgets.active_ids.includes(widget.widget_id); return ` `; }).join(''); const renderProfileFields = (state) => `
${renderField('name', 'Name', state.draft.profile.name)} ${renderField('email', 'E-Mail', state.draft.profile.email, 'email')} ${renderField('phone', 'Telefon', state.draft.profile.phone, 'tel')} ${renderField('birthdate', 'Geburtsdatum', state.draft.profile.birthdate, 'date')} ${renderField('title', 'Titel', state.draft.profile.title)} ${renderField('department', 'Abteilung', state.draft.profile.department)} ${renderField('city', 'Ort', state.draft.profile.city)} ${renderField('website', 'Website', state.draft.profile.website, 'url')}
`; const renderField = (fieldId, label, value, type = 'text') => `
`; const sectionContent = (state) => { if (state.activeSection === 'desktop') { return `

Desktop Type

Skin-Auswahl

Die direkte Skin-Umschaltung liegt jetzt im Setup. Die Auswahl wird pro Benutzer gespeichert.

${renderSkinChoices(state)}
`; } if (state.activeSection === 'profile') { return `

Benutzerdaten

Persoenliches Profil

Diese Daten werden zunaechst lokal gespeichert und spaeter an LDAP oder Keycloak angebunden.

${renderProfileFields(state)}
`; } if (state.activeSection === 'apps') { return `

App-Auswahl

Sichtbare Desktop-Apps

Aenderungen an der App-Auswahl werden nach dem Speichern mit einem Desktop-Reload sauber uebernommen.

${renderAppItems(state)}
`; } return `

Infobereich

Rechter Desktopbereich

Diese Inhalte liegen aktuell im rechten Infobereich des Desktops und werden hier benutzerbezogen ein- oder ausgeblendet.

${renderWidgetItems(state)}
`; }; const renderApp = (root, state) => { if (state.loading) { root.innerHTML = '

Einstellungen werden geladen.

'; return; } if (state.error && !state.bootstrap) { root.innerHTML = `

${escapeHtml(state.error)}

`; return; } root.innerHTML = `

Desktop Setup

${escapeHtml(sectionTitle(state.activeSection))}

${escapeHtml(summarizeSection(state.activeSection))}

LDAP: geplant Keycloak: geplant Scope: ${escapeHtml(state.bootstrap.meta.storage_scope || 'guest')}
${sectionContent(state)}

Skin- und App-Aenderungen koennen einen Reload ausloesen, damit Fenster, Icons und Menue sauber synchron bleiben.

${escapeHtml(state.error || state.success || '')}

`; }; const sectionTitle = (sectionId) => { const current = SECTIONS.find((section) => section.id === sectionId); return current ? current.label : 'Setup'; }; const setDraftValue = (draft, path, value) => { const segments = path.split('.'); let cursor = draft; segments.slice(0, -1).forEach((segment) => { cursor = cursor[segment]; }); cursor[segments[segments.length - 1]] = value; }; const toggleSelection = (collection, value) => { const next = new Set(collection); if (next.has(value)) { next.delete(value); } else { next.add(value); } return Array.from(next); }; const syncSelectionWithCheckedState = (collection, value, isChecked) => { const next = new Set(collection); if (isChecked) { next.add(value); } else { next.delete(value); } return Array.from(next); }; const buildPatch = (state) => ({ profile: state.draft.profile, desktop: { active_skin: state.draft.desktop.active_skin, }, apps: { enabled_ids: state.draft.apps.enabled_ids, }, widgets: { active_ids: state.draft.widgets.active_ids, }, }); const requiresReload = (before, after) => before.desktop.active_skin !== after.desktop.active_skin || !sameSelection(before.apps.enabled_ids, after.apps.enabled_ids); const attachInteractions = (root, state, options) => { root.addEventListener('click', async (event) => { const button = event.target instanceof Element ? event.target.closest('[data-action]') : null; if (!button) { return; } const action = button.dataset.action; if (action === 'open-section') { state.activeSection = button.dataset.sectionId || 'desktop'; renderApp(root, state); return; } if (action === 'select-skin') { state.draft.desktop.active_skin = button.dataset.skinId || state.draft.desktop.active_skin; renderApp(root, state); return; } if (action === 'save-settings') { if (!options.apiBase || state.saving) { return; } state.saving = true; state.error = ''; state.success = ''; renderApp(root, state); try { const before = clone(state.bootstrap.preferences); const response = await window.fetch(options.apiBase, { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(buildPatch(state)), }); if (!response.ok) { throw new Error(`save-failed:${response.status}`); } const payload = await response.json(); const after = payload.preferences; const ldapSync = payload.meta?.ldap_profile_sync || null; state.bootstrap = payload; state.draft = clone(after); const baseMessage = payload.meta?.requires_reload ? 'Gespeichert. Desktop wird fuer Skin- oder App-Aenderungen neu geladen.' : 'Einstellungen wurden gespeichert.'; const ldapMessage = ldapSync && ldapSync.attempted ? ` LDAP: ${ldapSync.message || (ldapSync.success ? 'Synchronisiert.' : 'Nicht synchronisiert.')}` : ''; state.success = `${baseMessage}${ldapMessage}`; state.saving = false; renderApp(root, state); if (window.desktopShell?.dispatchUserSettingsUpdate) { window.desktopShell.dispatchUserSettingsUpdate({ preferences: after, requiresReload: payload.meta?.requires_reload || requiresReload(before, after), }); } return; } catch (error) { state.saving = false; state.error = 'Speichern fehlgeschlagen. Die lokalen Einstellungen bleiben unveraendert.'; renderApp(root, state); return; } } }); root.addEventListener('change', (event) => { const target = event.target; if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) { return; } if (target.dataset.field) { setDraftValue(state.draft, target.dataset.field, target.value); return; } if (target.dataset.appId) { state.draft.apps.enabled_ids = syncSelectionWithCheckedState( state.draft.apps.enabled_ids, target.dataset.appId, target.checked ); renderApp(root, state); return; } if (target.dataset.widgetId) { state.draft.widgets.active_ids = syncSelectionWithCheckedState( state.draft.widgets.active_ids, target.dataset.widgetId, target.checked ); renderApp(root, state); } }); }; window.initUserSelfManagementApp = (root, options = {}) => { if (!(root instanceof HTMLElement) || root.dataset.usmInitialized === 'true') { return; } root.dataset.usmInitialized = 'true'; const state = { loading: true, saving: false, error: '', success: '', activeSection: 'desktop', bootstrap: null, draft: null, }; attachInteractions(root, state, options); renderApp(root, state); if (!options.apiBase) { state.loading = false; state.error = 'Die Setup-API ist nicht konfiguriert.'; renderApp(root, state); return; } window.fetch(options.apiBase, { headers: { Accept: 'application/json', }, }) .then((response) => { if (!response.ok) { throw new Error(`load-failed:${response.status}`); } return response.json(); }) .then((payload) => { state.loading = false; state.bootstrap = payload; state.draft = clone(payload.preferences); renderApp(root, state); }) .catch(() => { state.loading = false; state.error = 'Die Einstellungen konnten nicht geladen werden.'; renderApp(root, state); }); }; })();