Files
Ulas Kalayci 6cdef0102c feat(settings): add dedicated Sync tab with CardDAV contacts integration
- Rename Calendar tab to Synchronization with two sections:
  * Calendar Sync (Google, Apple, CalDAV, ICS)
  * Contact Sync (CardDAV) - NEW
- Add visual tab grouping with CSS separators between sections
- Implement CardDAV account management UI:
  * Add/delete accounts
  * Enable/disable addressbooks
  * Manual sync trigger
  * Connection testing
- Add UX improvements:
  * Status badges (success/error/syncing)
  * Empty states with onboarding
  * Inline help tooltips (prepared)
  * Breadcrumb navigation (prepared)
- Update i18n keys in all 14 locales
- All 109 tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-04 21:50:59 +02:00

232 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Settings Navigation Utility
* Zweck: Zweistufige Sidebar-Navigation für Settings
* Pattern: Hauptkategorien (links) + Unterkategorien (Content-Bereich)
*/
import { t } from '/i18n.js';
export const SETTINGS_STORAGE_KEY = 'oikos-settings-section';
/**
* Hauptkategorien mit ihren Unterkategorien
*/
export const SETTINGS_SECTIONS = (user) => [
{
id: 'personal',
labelKey: 'settings.sectionPersonal',
icon: 'user',
pages: [
{ id: 'account', labelKey: 'settings.tabAccount', icon: 'user-circle' },
]
},
{
id: 'modules',
labelKey: 'settings.sectionModules',
icon: 'layout-grid',
pages: [
{ id: 'general', labelKey: 'settings.tabGeneral', icon: 'settings' },
{ id: 'meals', labelKey: 'settings.tabMeals', icon: 'utensils' },
{ id: 'budget', labelKey: 'settings.tabBudget', icon: 'wallet' },
{ id: 'shopping', labelKey: 'settings.tabShopping', icon: 'shopping-cart' },
]
},
{
id: 'sync',
labelKey: 'settings.sectionSync',
icon: 'refresh-cw',
pages: [
{ id: 'sync-calendar', labelKey: 'settings.tabSyncCalendar', icon: 'calendar' },
{ id: 'sync-contacts', labelKey: 'settings.tabSyncContacts', icon: 'users' },
]
},
...(user?.role === 'admin' ? [{
id: 'admin',
labelKey: 'settings.sectionAdmin',
icon: 'shield',
pages: [
{ id: 'family', labelKey: 'settings.tabFamily', icon: 'users' },
{ id: 'api-tokens', labelKey: 'settings.tabApiTokens', icon: 'key' },
{ id: 'backup', labelKey: 'settings.tabBackup', icon: 'database' },
]
}] : [])
];
/**
* Findet Sektion und Seite für gegebene Page-ID
*/
export function findSectionAndPage(pageId, user) {
const sections = SETTINGS_SECTIONS(user);
for (const section of sections) {
const page = section.pages.find(p => p.id === pageId);
if (page) return { section, page };
}
return null;
}
/**
* Gibt letzte aktive Seite zurück (aus sessionStorage)
*/
export function getLastActivePage(user) {
try {
const stored = sessionStorage.getItem(SETTINGS_STORAGE_KEY);
if (stored) {
const found = findSectionAndPage(stored, user);
if (found) return stored;
}
} catch { /* ignore */ }
return 'general';
}
/**
* Speichert aktive Seite
*/
export function setActivePage(pageId) {
try {
sessionStorage.setItem(SETTINGS_STORAGE_KEY, pageId);
} catch { /* ignore */ }
}
/**
* Rendert die Sidebar-Navigation
*/
export function renderSettingsSidebar(container, activePage, user) {
const sections = SETTINGS_SECTIONS(user);
const activeInfo = findSectionAndPage(activePage, user);
const activeSectionId = activeInfo?.section.id;
const sidebar = document.createElement('nav');
sidebar.className = 'settings-sidebar';
sidebar.setAttribute('role', 'navigation');
sidebar.setAttribute('aria-label', t('settings.navigationLabel'));
sections.forEach(section => {
const sectionEl = document.createElement('div');
sectionEl.className = 'settings-sidebar-section';
if (section.id === activeSectionId) {
sectionEl.classList.add('settings-sidebar-section--active');
}
// Section Header
const header = document.createElement('div');
header.className = 'settings-sidebar-section__header';
const headerIcon = document.createElement('i');
headerIcon.dataset.lucide = section.icon;
headerIcon.className = 'settings-sidebar-section__icon';
headerIcon.setAttribute('aria-hidden', 'true');
const headerLabel = document.createElement('span');
headerLabel.className = 'settings-sidebar-section__label';
headerLabel.textContent = t(section.labelKey);
header.appendChild(headerIcon);
header.appendChild(headerLabel);
sectionEl.appendChild(header);
// Pages List
const pagesList = document.createElement('div');
pagesList.className = 'settings-sidebar-pages';
section.pages.forEach(page => {
const pageBtn = document.createElement('button');
pageBtn.className = 'settings-sidebar-page';
pageBtn.type = 'button';
pageBtn.dataset.pageId = page.id;
if (page.id === activePage) {
pageBtn.classList.add('settings-sidebar-page--active');
pageBtn.setAttribute('aria-current', 'page');
}
const pageIcon = document.createElement('i');
pageIcon.dataset.lucide = page.icon;
pageIcon.className = 'settings-sidebar-page__icon';
pageIcon.setAttribute('aria-hidden', 'true');
const pageLabel = document.createElement('span');
pageLabel.className = 'settings-sidebar-page__label';
pageLabel.textContent = t(page.labelKey);
pageBtn.appendChild(pageIcon);
pageBtn.appendChild(pageLabel);
pagesList.appendChild(pageBtn);
});
sectionEl.appendChild(pagesList);
sidebar.appendChild(sectionEl);
});
// Event Delegation
sidebar.addEventListener('click', (e) => {
const pageBtn = e.target.closest('[data-page-id]');
if (!pageBtn) return;
const pageId = pageBtn.dataset.pageId;
if (pageId === activePage) return;
setActivePage(pageId);
// Trigger custom event für Page-Switch
container.dispatchEvent(new CustomEvent('settings-page-change', {
detail: { pageId },
bubbles: true
}));
});
container.appendChild(sidebar);
// Hydrate Lucide Icons
if (window.lucide) window.lucide.createIcons({ el: sidebar });
}
/**
* Rendert Breadcrumb für aktive Seite
*/
export function renderBreadcrumb(container, activePage, user) {
const info = findSectionAndPage(activePage, user);
if (!info) return;
const breadcrumb = document.createElement('nav');
breadcrumb.className = 'settings-breadcrumb';
breadcrumb.setAttribute('aria-label', t('settings.breadcrumbLabel'));
const ol = document.createElement('ol');
ol.className = 'settings-breadcrumb__list';
// Home
const homeItem = document.createElement('li');
homeItem.className = 'settings-breadcrumb__item';
homeItem.textContent = t('settings.title');
ol.appendChild(homeItem);
// Separator
const sep1 = document.createElement('li');
sep1.className = 'settings-breadcrumb__separator';
sep1.setAttribute('aria-hidden', 'true');
sep1.textContent = '';
ol.appendChild(sep1);
// Section
const sectionItem = document.createElement('li');
sectionItem.className = 'settings-breadcrumb__item';
sectionItem.textContent = t(info.section.labelKey);
ol.appendChild(sectionItem);
// Separator
const sep2 = document.createElement('li');
sep2.className = 'settings-breadcrumb__separator';
sep2.setAttribute('aria-hidden', 'true');
sep2.textContent = '';
ol.appendChild(sep2);
// Page
const pageItem = document.createElement('li');
pageItem.className = 'settings-breadcrumb__item settings-breadcrumb__item--current';
pageItem.setAttribute('aria-current', 'page');
pageItem.textContent = t(info.page.labelKey);
ol.appendChild(pageItem);
breadcrumb.appendChild(ol);
container.insertBefore(breadcrumb, container.firstChild);
}