/** * Modul: Einstellungen (Settings) * Zweck: Benutzerkonto, Passwort, Kalender-Sync, Familienmitglieder * Abhängigkeiten: /api.js */ import { api, auth } from '/api.js'; /** * @param {HTMLElement} container * @param {{ user: object }} context */ export async function render(container, { user }) { // URL-Parameter auswerten (z.B. nach OAuth-Callback) const params = new URLSearchParams(location.search); const syncOk = params.get('sync_ok'); const syncErr = params.get('sync_error'); // State für Familienmitglieder + Sync-Status let users = []; let googleStatus = { configured: false, connected: false, lastSync: null }; let appleStatus = { configured: false, lastSync: null }; try { const [usersRes, gStatus, aStatus] = await Promise.allSettled([ user.role === 'admin' ? auth.getUsers() : Promise.resolve({ data: [] }), api.get('/calendar/google/status'), api.get('/calendar/apple/status'), ]); if (usersRes.status === 'fulfilled') users = usersRes.value.data ?? []; if (gStatus.status === 'fulfilled') googleStatus = gStatus.value; if (aStatus.status === 'fulfilled') appleStatus = aStatus.value; } catch (_) { /* non-critical */ } container.innerHTML = `
${syncOk ? `
Kalender-Sync mit ${syncOk === 'google' ? 'Google' : 'Apple'} erfolgreich verbunden.
` : ''} ${syncErr ? `
Verbindung mit ${syncErr === 'google' ? 'Google' : 'Apple'} fehlgeschlagen. Bitte erneut versuchen.
` : ''}

Design

Darstellung

Mein Konto

Passwort ändern

Kalender-Synchronisation

Google Calendar
${googleStatus.connected ? `Verbunden${googleStatus.lastSync ? ` · Zuletzt: ${formatDate(googleStatus.lastSync)}` : ''}` : googleStatus.configured ? 'Nicht verbunden' : 'Nicht konfiguriert (fehlende .env-Variablen)'}
${googleStatus.configured ? `
${googleStatus.connected ? ` ${user?.role === 'admin' ? `` : ''} ` : ` ${user?.role === 'admin' ? `Mit Google verbinden` : 'Nur Admin kann Google Calendar verbinden.'} `}
` : ''}
Apple Calendar (iCloud)
${appleStatus.connected ? `Verbunden${appleStatus.lastSync ? ` · Zuletzt: ${formatDate(appleStatus.lastSync)}` : ''}` : appleStatus.configured ? `Konfiguriert (via .env)${appleStatus.lastSync ? ` · Zuletzt: ${formatDate(appleStatus.lastSync)}` : ''}` : 'Nicht verbunden'}
${appleStatus.configured ? `
${appleStatus.connected && user?.role === 'admin' ? `` : ''}
` : user?.role === 'admin' ? `
Passwort unter appleid.apple.com → Sicherheit erstellen.
` : 'Nur Admin kann Apple Calendar verbinden.'}
${user?.role === 'admin' ? `

Familienmitglieder

    ${users.map(memberHtml).join('')}

Neues Familienmitglied

` : ''}
`; bindEvents(container, user); } // -------------------------------------------------------- // Event-Binding // -------------------------------------------------------- function bindEvents(container, user) { // Theme-Toggle const themeToggle = container.querySelector('#theme-toggle'); if (themeToggle) { themeToggle.addEventListener('click', (e) => { const btn = e.target.closest('[data-theme-value]'); if (!btn) return; const value = btn.dataset.themeValue; applyTheme(value); themeToggle.querySelectorAll('.theme-toggle__btn').forEach(b => b.classList.remove('theme-toggle__btn--active')); btn.classList.add('theme-toggle__btn--active'); }); } // Passwort ändern const passwordForm = container.querySelector('#password-form'); if (passwordForm) { passwordForm.addEventListener('submit', async (e) => { e.preventDefault(); const currentPw = container.querySelector('#current-password').value; const newPw = container.querySelector('#new-password').value; const confirmPw = container.querySelector('#confirm-password').value; const errorEl = container.querySelector('#password-error'); errorEl.hidden = true; if (newPw !== confirmPw) { showError(errorEl, 'Passwörter stimmen nicht überein.'); return; } const btn = passwordForm.querySelector('[type=submit]'); btn.disabled = true; try { await api.patch('/auth/me/password', { current_password: currentPw, new_password: newPw }); passwordForm.reset(); window.oikos?.showToast('Passwort erfolgreich geändert.', 'success'); } catch (err) { showError(errorEl, err.message); } finally { btn.disabled = false; } }); } // Google Sync const googleSyncBtn = container.querySelector('#google-sync-btn'); if (googleSyncBtn) { googleSyncBtn.addEventListener('click', async () => { googleSyncBtn.disabled = true; googleSyncBtn.textContent = 'Synchronisiere…'; try { await api.post('/calendar/google/sync', {}); window.oikos?.showToast('Google Calendar synchronisiert.', 'success'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } finally { googleSyncBtn.disabled = false; googleSyncBtn.textContent = 'Jetzt synchronisieren'; } }); } // Google Disconnect (Admin) const googleDisconnectBtn = container.querySelector('#google-disconnect-btn'); if (googleDisconnectBtn) { googleDisconnectBtn.addEventListener('click', async () => { if (!confirm('Google Calendar-Verbindung trennen?')) return; try { await api.delete('/calendar/google/disconnect'); window.oikos?.showToast('Google Calendar getrennt.', 'default'); window.oikos?.navigate('/settings'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } }); } // Apple Sync const appleSyncBtn = container.querySelector('#apple-sync-btn'); if (appleSyncBtn) { appleSyncBtn.addEventListener('click', async () => { appleSyncBtn.disabled = true; appleSyncBtn.textContent = 'Synchronisiere…'; try { await api.post('/calendar/apple/sync', {}); window.oikos?.showToast('Apple Calendar synchronisiert.', 'success'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } finally { appleSyncBtn.disabled = false; appleSyncBtn.textContent = 'Jetzt synchronisieren'; } }); } // Apple Disconnect (Admin) const appleDisconnectBtn = container.querySelector('#apple-disconnect-btn'); if (appleDisconnectBtn) { appleDisconnectBtn.addEventListener('click', async () => { if (!confirm('Apple Calendar-Verbindung trennen?')) return; try { await api.delete('/calendar/apple/disconnect'); window.oikos?.showToast('Apple Calendar getrennt.', 'default'); window.oikos?.navigate('/settings'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } }); } // Apple Connect-Formular (Admin) const appleConnectForm = container.querySelector('#apple-connect-form'); if (appleConnectForm) { appleConnectForm.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = container.querySelector('#apple-connect-error'); errorEl.hidden = true; const url = container.querySelector('#apple-caldav-url').value.trim(); const username = container.querySelector('#apple-username').value.trim(); const password = container.querySelector('#apple-password').value; const btn = container.querySelector('#apple-connect-btn'); btn.disabled = true; btn.textContent = 'Verbinde…'; try { await api.post('/calendar/apple/connect', { url, username, password }); window.oikos?.showToast('Apple Calendar verbunden.', 'success'); window.oikos?.navigate('/settings'); } catch (err) { showError(errorEl, err.message); } finally { btn.disabled = false; btn.textContent = 'Verbinden & testen'; } }); } // Mitglied hinzufügen (Admin) const addMemberBtn = container.querySelector('#add-member-btn'); if (addMemberBtn) { addMemberBtn.addEventListener('click', () => { container.querySelector('#add-member-form-card').classList.remove('settings-card--hidden'); addMemberBtn.hidden = true; }); } const cancelAddMember = container.querySelector('#cancel-add-member'); if (cancelAddMember) { cancelAddMember.addEventListener('click', () => { container.querySelector('#add-member-form-card').classList.add('settings-card--hidden'); container.querySelector('#add-member-btn').hidden = false; container.querySelector('#add-member-form').reset(); container.querySelector('#member-error').hidden = true; }); } const addMemberForm = container.querySelector('#add-member-form'); if (addMemberForm) { addMemberForm.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = container.querySelector('#member-error'); errorEl.hidden = true; const data = { username: container.querySelector('#new-username').value.trim(), display_name: container.querySelector('#new-display-name').value.trim(), password: container.querySelector('#new-member-password').value, avatar_color: container.querySelector('#new-avatar-color').value, role: container.querySelector('#new-role').value, }; const btn = addMemberForm.querySelector('[type=submit]'); btn.disabled = true; try { const res = await auth.createUser(data); const list = container.querySelector('#members-list'); list.insertAdjacentHTML('beforeend', memberHtml(res.user)); addMemberForm.reset(); container.querySelector('#add-member-form-card').classList.add('settings-card--hidden'); container.querySelector('#add-member-btn').hidden = false; window.oikos?.showToast(`${res.user.display_name} hinzugefügt.`, 'success'); bindDeleteButtons(container, user); } catch (err) { showError(errorEl, err.message); } finally { btn.disabled = false; } }); } bindDeleteButtons(container, user); // Abmelden const logoutBtn = container.querySelector('#logout-btn'); if (logoutBtn) { logoutBtn.addEventListener('click', async () => { try { await auth.logout(); } finally { window.location.href = '/login'; } }); } } function bindDeleteButtons(container, user) { container.querySelectorAll('[data-delete-user]').forEach((btn) => { btn.replaceWith(btn.cloneNode(true)); // Doppelte Listener vermeiden }); container.querySelectorAll('[data-delete-user]').forEach((btn) => { btn.addEventListener('click', async () => { const id = parseInt(btn.dataset.deleteUser, 10); const name = btn.dataset.name; if (!confirm(`${name} wirklich löschen?`)) return; try { await auth.deleteUser(id); btn.closest('.settings-member').remove(); window.oikos?.showToast(`${name} gelöscht.`, 'default'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } }); }); } // -------------------------------------------------------- // Helfer // -------------------------------------------------------- function memberHtml(u) { return `
  • ${initials(u.display_name)}
    ${u.display_name} @${u.username} · ${u.role === 'admin' ? 'Admin' : 'Mitglied'}
  • `; } function initials(name) { if (!name) return '?'; return name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase(); } function formatDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function currentTheme() { return localStorage.getItem('oikos-theme') || 'system'; } function applyTheme(value) { localStorage.setItem('oikos-theme', value); if (value === 'light' || value === 'dark') { document.documentElement.setAttribute('data-theme', value); } else { document.documentElement.removeAttribute('data-theme'); } } function showError(el, msg) { el.textContent = msg; el.hidden = false; }