/**
* Modul: Einstellungen (Settings)
* Zweck: Benutzerkonto, Passwort, Kalender-Sync, Familienmitglieder
* Abhängigkeiten: /api.js
*/
import { api, auth } from '/api.js';
import { t, formatDate, formatTime } from '/i18n.js';
import { esc } from '/utils/html.js';
import '/components/oikos-locale-picker.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 };
let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'] };
try {
const [usersRes, gStatus, aStatus, prefsRes] = await Promise.allSettled([
user.role === 'admin' ? auth.getUsers() : Promise.resolve({ data: [] }),
api.get('/calendar/google/status'),
api.get('/calendar/apple/status'),
api.get('/preferences'),
]);
if (usersRes.status === 'fulfilled') users = usersRes.value.data ?? [];
if (gStatus.status === 'fulfilled') googleStatus = gStatus.value;
if (aStatus.status === 'fulfilled') appleStatus = aStatus.value;
if (prefsRes.status === 'fulfilled') prefs = prefsRes.value.data ?? prefs;
} catch (_) { /* non-critical */ }
const googleStatusText = googleStatus.connected
? (googleStatus.lastSync ? t('settings.connectedLastSync', { date: formatDateTime(googleStatus.lastSync) }) : t('settings.connected'))
: googleStatus.configured ? t('settings.notConnected') : t('settings.notConfigured');
const appleStatusText = appleStatus.connected
? (appleStatus.lastSync ? t('settings.connectedLastSync', { date: formatDateTime(appleStatus.lastSync) }) : t('settings.connected'))
: appleStatus.configured
? (appleStatus.lastSync ? t('settings.configuredLastSync', { date: formatDateTime(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.languageTitle')}
${t('settings.sectionMeals')}
${t('settings.sectionAccount')}
${esc(initials(user?.display_name))}
${esc(user?.display_name)}
@${esc(user?.username)}
${t('settings.changePassword')}
${t('settings.sectionCalendarSync')}
${googleStatus.configured ? `
${googleStatus.connected ? `
${user?.role === 'admin' ? `
` : ''}
` : `
${user?.role === 'admin' ? `
${t('settings.connectGoogle')}` : `
${t('settings.googleOnlyAdmin')}`}
`}
` : ''}
${appleStatus.configured ? `
${appleStatus.connected && user?.role === 'admin' ? `` : ''}
` : user?.role === 'admin' ? `
` : `
${t('settings.appleOnlyAdmin')}`}
${user?.role === 'admin' ? `
${t('settings.sectionFamily')}
${users.map(memberHtml).join('')}
` : ''}
`;
// Meal-Type-Checkboxen initialisieren
const toggles = container.querySelector('#meal-type-toggles');
if (toggles) {
toggles.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
cb.checked = prefs.visible_meal_types.includes(cb.value);
});
}
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');
});
}
// Meal-Type-Toggles
const mealToggles = container.querySelector('#meal-type-toggles');
if (mealToggles) {
mealToggles.addEventListener('change', async () => {
const checked = [...mealToggles.querySelectorAll('input:checked')].map((cb) => cb.value);
if (checked.length === 0) {
window.oikos?.showToast(t('settings.mealTypesMinOne'), 'error');
// Revert: re-check all
mealToggles.querySelectorAll('input').forEach((cb) => { cb.checked = true; });
return;
}
try {
await api.put('/preferences', { visible_meal_types: checked });
window.oikos?.showToast(t('settings.mealTypesSaved'), 'success');
} catch (err) {
window.oikos?.showToast(err.message ?? t('common.errorGeneric'), 'danger');
}
});
}
// 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');
}
});
});
}
function memberHtml(u) {
return `
${initials(u.display_name)}
${esc(u.display_name)}
@${esc(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 formatDateTime(iso) {
if (!iso) return '';
const d = new Date(iso);
return `${formatDate(d)} ${formatTime(d)}`.trim();
}
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;
}