/** * Modul: Einstellungen (Settings) * Zweck: Benutzerkonto, Passwort, Kalender-Sync, Familienmitglieder * Abhängigkeiten: /api.js */ import { api, auth } from '/api.js'; import { t } from '/i18n.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 */ } const googleStatusText = googleStatus.connected ? (googleStatus.lastSync ? t('settings.connectedLastSync', { date: formatDate(googleStatus.lastSync) }) : t('settings.connected')) : googleStatus.configured ? t('settings.notConnected') : t('settings.notConfigured'); const appleStatusText = appleStatus.connected ? (appleStatus.lastSync ? t('settings.connectedLastSync', { date: formatDate(appleStatus.lastSync) }) : t('settings.connected')) : appleStatus.configured ? (appleStatus.lastSync ? t('settings.configuredLastSync', { date: formatDate(appleStatus.lastSync) }) : t('settings.configured')) : t('settings.notConnected'); container.innerHTML = `
${syncOk ? `
${syncOk === 'google' ? t('settings.syncSuccessGoogle') : t('settings.syncSuccessApple')}
` : ''} ${syncErr ? `
${syncErr === 'google' ? t('settings.syncErrorGoogle') : t('settings.syncErrorApple')}
` : ''}

${t('settings.sectionDesign')}

${t('settings.cardAppearance')}

${t('settings.sectionAccount')}

${t('settings.changePassword')}

${t('settings.sectionCalendarSync')}

${t('settings.googleCalendar')}
${googleStatusText}
${googleStatus.configured ? `
${googleStatus.connected ? ` ${user?.role === 'admin' ? `` : ''} ` : ` ${user?.role === 'admin' ? `${t('settings.connectGoogle')}` : `${t('settings.googleOnlyAdmin')}`} `}
` : ''}
${t('settings.appleCalendar')}
${appleStatusText}
${appleStatus.configured ? `
${appleStatus.connected && user?.role === 'admin' ? `` : ''}
` : user?.role === 'admin' ? `
${t('settings.applePasswordHint')}
` : `${t('settings.appleOnlyAdmin')}`}
${user?.role === 'admin' ? `

${t('settings.sectionFamily')}

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

${t('settings.newMemberTitle')}

` : ''}
`; 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, t('settings.passwordMismatch')); 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(t('settings.passwordSavedToast'), '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 = t('settings.synchronizing'); try { await api.post('/calendar/google/sync', {}); window.oikos?.showToast(t('settings.syncSuccess', { provider: 'Google Calendar' }), 'success'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } finally { googleSyncBtn.disabled = false; googleSyncBtn.textContent = t('settings.syncNow'); } }); } // Google Disconnect (Admin) const googleDisconnectBtn = container.querySelector('#google-disconnect-btn'); if (googleDisconnectBtn) { googleDisconnectBtn.addEventListener('click', async () => { if (!confirm(t('settings.googleDisconnectConfirm'))) return; try { await api.delete('/calendar/google/disconnect'); window.oikos?.showToast(t('settings.disconnectedToast', { provider: 'Google Calendar' }), '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 = t('settings.synchronizing'); try { await api.post('/calendar/apple/sync', {}); window.oikos?.showToast(t('settings.syncSuccess', { provider: 'Apple Calendar' }), 'success'); } catch (err) { window.oikos?.showToast(err.message, 'danger'); } finally { appleSyncBtn.disabled = false; appleSyncBtn.textContent = t('settings.syncNow'); } }); } // Apple Disconnect (Admin) const appleDisconnectBtn = container.querySelector('#apple-disconnect-btn'); if (appleDisconnectBtn) { appleDisconnectBtn.addEventListener('click', async () => { if (!confirm(t('settings.appleDisconnectConfirm'))) return; try { await api.delete('/calendar/apple/disconnect'); window.oikos?.showToast(t('settings.disconnectedToast', { provider: 'Apple Calendar' }), '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 = t('settings.appleConnecting'); try { await api.post('/calendar/apple/connect', { url, username, password }); window.oikos?.showToast(t('settings.appleConnectedToast'), 'success'); window.oikos?.navigate('/settings'); } catch (err) { showError(errorEl, err.message); } finally { btn.disabled = false; btn.textContent = t('settings.appleConnectBtn'); } }); } // 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(t('settings.memberAddedToast', { name: res.user.display_name }), '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(t('settings.deleteMemberConfirm', { name }))) return; try { await auth.deleteUser(id); btn.closest('.settings-member').remove(); window.oikos?.showToast(t('settings.memberDeletedToast', { name }), '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' ? t('settings.roleAdmin') : t('settings.roleMember')}
  • `; } 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; }