/** * Modul: Dashboard * Zweck: Startseite mit Begrüßung, Terminen, Aufgaben, Essen, Notizen und FAB * Abhängigkeiten: /api.js */ import { api } from '/api.js'; // -------------------------------------------------------- // Hilfsfunktionen // -------------------------------------------------------- function greeting(displayName) { const h = new Date().getHours(); const tageszeit = h < 12 ? 'Morgen' : h < 18 ? 'Tag' : 'Abend'; return `Guten ${tageszeit}, ${displayName}`; } function formatDate(date = new Date()) { return date.toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } function formatDateTime(isoString) { if (!isoString) return ''; const d = new Date(isoString); const today = new Date(); const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1); const dateStr = d.toDateString() === today.toDateString() ? 'Heute' : d.toDateString() === tomorrow.toDateString() ? 'Morgen' : d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const timeStr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); return `${dateStr}, ${timeStr} Uhr`; } function formatDueDate(dateStr) { if (!dateStr) return null; const due = new Date(dateStr); const now = new Date(); const diffMs = due - now; const diffH = diffMs / (1000 * 60 * 60); if (diffMs < 0) return { text: 'Überfällig', overdue: true }; if (diffH < 24) return { text: 'Heute fällig', overdue: false }; if (diffH < 48) return { text: 'Morgen fällig', overdue: false }; return { text: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }), overdue: false, }; } const MEAL_LABELS = { breakfast: 'Frühstück', lunch: 'Mittagessen', dinner: 'Abendessen', snack: 'Snack', }; function initials(name = '') { return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase(); } // -------------------------------------------------------- // Skeleton // -------------------------------------------------------- function skeletonWidget(lines = 3) { const lineHtml = Array.from({ length: lines }, (_, i) => `
`).join(''); return `
${lineHtml}
`; } // -------------------------------------------------------- // Widget-Renderer // -------------------------------------------------------- function renderGreeting(user) { return `
${greeting(user.display_name)}
${formatDate()}
`; } function renderUrgentTasks(tasks) { const header = `
Dringende Aufgaben Alle
`; if (!tasks.length) { return `
${header}
Keine dringenden Aufgaben. ✓
`; } const items = tasks.map((t) => { const due = formatDueDate(t.due_date); return `
${t.title}
${due ? `
${due.text}
` : ''}
${t.assigned_color ? `
${initials(t.assigned_name || '')}
` : ''}
`; }).join(''); return `
${header}
${items}
`; } function renderUpcomingEvents(events) { const header = `
Anstehende Termine Alle
`; if (!events.length) { return `
${header}
Keine anstehenden Termine.
`; } const items = events.map((e) => `
${e.title}
${e.all_day ? formatDate(new Date(e.start_datetime)) : formatDateTime(e.start_datetime)} ${e.location ? ` · ${e.location}` : ''}
`).join(''); return `
${header}
${items}
`; } function renderTodayMeals(meals) { const header = `
Heute essen Alle
`; if (!meals.length) { return `
${header}
Kein Essensplan für heute.
`; } const items = meals.map((m) => `
${MEAL_LABELS[m.meal_type]} ${m.title}
`).join(''); return `
${header}
${items}
`; } function renderPinnedNotes(notes) { const header = `
Pinnwand Alle
`; if (!notes.length) { return `
${header}
Keine angepinnten Notizen.
`; } const items = notes.map((n) => `
${n.title ? `
${n.title}
` : ''}
${n.content}
`).join(''); return `
${header}
${items}
`; } // -------------------------------------------------------- // FAB Speed-Dial // -------------------------------------------------------- const FAB_ACTIONS = [ { route: '/tasks', label: 'Aufgabe', icon: 'check-square' }, { route: '/calendar', label: 'Termin', icon: 'calendar-plus' }, { route: '/shopping', label: 'Einkauf', icon: 'shopping-cart' }, { route: '/notes', label: 'Notiz', icon: 'sticky-note' }, ]; function renderFab() { const actionsHtml = FAB_ACTIONS.map((a) => `
${a.label}
`).join(''); return `
`; } function initFab(container) { const fabMain = container.querySelector('#fab-main'); const fabActions = container.querySelector('#fab-actions'); if (!fabMain) return; let open = false; function toggleFab(force) { open = force !== undefined ? force : !open; fabMain.classList.toggle('fab-main--open', open); fabMain.setAttribute('aria-expanded', String(open)); fabActions.classList.toggle('fab-actions--visible', open); fabActions.setAttribute('aria-hidden', String(!open)); fabActions.querySelectorAll('[role="button"]').forEach((el) => { el.tabIndex = open ? 0 : -1; }); if (window.lucide) window.lucide.createIcons(); } fabMain.addEventListener('click', (e) => { e.stopPropagation(); toggleFab(); }); fabActions.querySelectorAll('[data-route]').forEach((el) => { const go = () => { toggleFab(false); window.oikos.navigate(el.dataset.route); }; el.addEventListener('click', go); el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }); }); document.addEventListener('click', () => { if (open) toggleFab(false); }); } // -------------------------------------------------------- // Navigations-Links verdrahten // -------------------------------------------------------- function wireLinks(container) { container.querySelectorAll('[data-route]').forEach((el) => { if (el.id === 'fab-main' || el.closest('#fab-actions')) return; // FAB separat const go = () => window.oikos.navigate(el.dataset.route); if (el.tagName === 'A') { el.addEventListener('click', (e) => { e.preventDefault(); go(); }); } else { el.addEventListener('click', go); el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }); } }); } // -------------------------------------------------------- // Haupt-Render // -------------------------------------------------------- export async function render(container, { user }) { // Sofort Skeleton container.innerHTML = `
${greeting(user.display_name)}
${formatDate()}
${skeletonWidget(3)} ${skeletonWidget(3)} ${skeletonWidget(2)} ${skeletonWidget(3)}
${renderFab()} `; initFab(container); // Daten laden let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] }; try { data = await api.get('/dashboard'); } catch (err) { console.error('[Dashboard] Ladefehler:', err.message); window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning'); } // Widgets rendern container.innerHTML = `
${renderGreeting(user)} ${renderUrgentTasks(data.urgentTasks ?? [])} ${renderUpcomingEvents(data.upcomingEvents ?? [])} ${renderTodayMeals(data.todayMeals ?? [])} ${renderPinnedNotes(data.pinnedNotes ?? [])}
${renderFab()} `; wireLinks(container); initFab(container); if (window.lucide) window.lucide.createIcons(); }