diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js index ba0aaae..855a265 100644 --- a/public/pages/dashboard.js +++ b/public/pages/dashboard.js @@ -5,6 +5,7 @@ */ import { api } from '/api.js'; +import { t } from '/i18n.js'; // Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert. let _fabController = null; @@ -15,8 +16,9 @@ let _fabController = null; function greeting(displayName) { const h = new Date().getHours(); - const tageszeit = h < 12 ? 'Morgen' : h < 18 ? 'Tag' : 'Abend'; - return `Guten ${tageszeit}, ${displayName}`; + if (h < 12) return t('dashboard.greetingMorning', { name: displayName }); + if (h < 18) return t('dashboard.greetingDay', { name: displayName }); + return t('dashboard.greetingEvening', { name: displayName }); } function formatDate(date = new Date()) { @@ -49,21 +51,21 @@ function formatDueDate(dateStr) { 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 }; + if (diffMs < 0) return { text: t('dashboard.overdue'), overdue: true }; + if (diffH < 24) return { text: t('dashboard.dueSoon'), overdue: false }; + if (diffH < 48) return { text: t('dashboard.dueTomorrow'), 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', -}; +const MEAL_LABELS = () => ({ + breakfast: t('meals.typeBreakfast'), + lunch: t('meals.typeLunch'), + dinner: t('meals.typeDinner'), + snack: t('meals.typeSnack'), +}); const MEAL_ICONS = { breakfast: 'sunrise', @@ -76,7 +78,8 @@ function initials(name = '') { return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase(); } -function widgetHeader(icon, title, count, linkHref, linkLabel = 'Alle') { +function widgetHeader(icon, title, count, linkHref, linkLabel) { + linkLabel = linkLabel ?? t('dashboard.allLink'); const badge = count != null ? `${count}` : ''; @@ -122,17 +125,17 @@ function renderGreeting(user, stats = {}) { if (urgentCount > 0) statChips.push(` - ${urgentCount} dring. Aufgabe${urgentCount > 1 ? 'n' : ''} + ${urgentCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: urgentCount }) : t('dashboard.urgentTasksChip', { count: urgentCount })} `); if (todayEventCount > 0) statChips.push(` - ${todayEventCount} Termin${todayEventCount > 1 ? 'e' : ''} heute + ${todayEventCount > 1 ? t('dashboard.eventsChipPlural', { count: todayEventCount }) : t('dashboard.eventsChip', { count: todayEventCount })} `); if (todayMealTitle) statChips.push(` - Heute: ${todayMealTitle} + ${t('dashboard.todayMealChip', { title: todayMealTitle })} `); return ` @@ -149,10 +152,10 @@ function renderGreeting(user, stats = {}) { function renderUrgentTasks(tasks) { if (!tasks.length) { return `
- ${widgetHeader('check-square', 'Aufgaben', 0, '/tasks')} + ${widgetHeader('check-square', t('nav.tasks'), 0, '/tasks')}
-
Alles erledigt
+
${t('dashboard.allDone')}
`; } @@ -174,7 +177,7 @@ function renderUrgentTasks(tasks) { }).join(''); return `
- ${widgetHeader('check-square', 'Aufgaben', tasks.length, '/tasks')} + ${widgetHeader('check-square', t('nav.tasks'), tasks.length, '/tasks')}
${items}
`; } @@ -182,10 +185,10 @@ function renderUrgentTasks(tasks) { function renderUpcomingEvents(events) { if (!events.length) { return `
- ${widgetHeader('calendar', 'Termine', 0, '/calendar')} + ${widgetHeader('calendar', t('nav.calendar'), 0, '/calendar')}
-
Keine Termine
+
${t('dashboard.noEvents')}
`; } @@ -194,14 +197,14 @@ function renderUpcomingEvents(events) { const items = events.map((e) => { const d = new Date(e.start_datetime); const isToday = d.toDateString() === today; - const timeStr = e.all_day ? 'Ganztägig' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr'; + const timeStr = e.all_day ? t('dashboard.allDay') : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr'; return `
${e.title}
- ${isToday ? 'Heute' : formatDateTime(e.start_datetime).split(',')[0]} + ${isToday ? t('common.today') : formatDateTime(e.start_datetime).split(',')[0]} ${timeStr} ${e.location ? ` · ${e.location}` : ''}
@@ -211,7 +214,7 @@ function renderUpcomingEvents(events) { }).join(''); return `
- ${widgetHeader('calendar', 'Termine', events.length, '/calendar')} + ${widgetHeader('calendar', t('nav.calendar'), events.length, '/calendar')}
${items}
`; } @@ -219,19 +222,20 @@ function renderUpcomingEvents(events) { function renderTodayMeals(meals) { const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack']; + const mealLabels = MEAL_LABELS(); const slots = MEAL_ORDER.map((type) => { const meal = meals.find((m) => m.meal_type === type); return `
-
${MEAL_LABELS[type]}
+
${mealLabels[type]}
${meal ? meal.title : '—'}
`; }).join(''); return `
- ${widgetHeader('utensils', 'Heute essen', null, '/meals', 'Woche')} + ${widgetHeader('utensils', t('dashboard.todayMeals'), null, '/meals', t('dashboard.weekLink'))}
${slots}
`; } @@ -239,10 +243,10 @@ function renderTodayMeals(meals) { function renderPinnedNotes(notes) { if (!notes.length) { return `
- ${widgetHeader('pin', 'Pinnwand', 0, '/notes')} + ${widgetHeader('pin', t('nav.notes'), 0, '/notes')}
-
Keine angepinnten Notizen
+
${t('dashboard.noPinnedNotes')}
`; } @@ -256,7 +260,7 @@ function renderPinnedNotes(notes) { `).join(''); return `
- ${widgetHeader('pin', 'Pinnwand', notes.length, '/notes')} + ${widgetHeader('pin', t('nav.notes'), notes.length, '/notes')}
${items}
`; } @@ -290,7 +294,7 @@ function renderWeatherWidget(weather) { return `
-
@@ -300,7 +304,7 @@ function renderWeatherWidget(weather) {
${current.desc}
${city}
- Gefühlt ${current.feels_like}° · ${current.humidity}% · Wind ${current.wind_speed} km/h + ${t('dashboard.weatherFeelsLike', { temp: current.feels_like, humidity: current.humidity, wind: current.wind_speed })}
[ + { route: '/tasks', label: t('dashboard.fabTask'), icon: 'check-square' }, + { route: '/calendar', label: t('dashboard.fabCalendar'), icon: 'calendar-plus' }, + { route: '/shopping', label: t('dashboard.fabShopping'), icon: 'shopping-cart' }, + { route: '/notes', label: t('dashboard.fabNote'), icon: 'sticky-note' }, ]; function renderFab() { - const actionsHtml = FAB_ACTIONS.map((a) => ` + const actionsHtml = FAB_ACTIONS().map((a) => `
+ aria-label="${a.label}"> ${a.label} ${progress !== null ? `
+ aria-label="${t('tasks.subtaskToggle')}">
@@ -187,7 +206,7 @@ function renderTaskCard(task, opts = {}) { id="subtasks-${task.id}"> ${subtasksHtml}
` : ''}
`; @@ -200,8 +219,8 @@ function renderTaskGroups(tasks, groupMode) { -
Keine Aufgaben — alles erledigt?
-
Neue Aufgaben über den + Button erstellen.
+
${t('tasks.emptyTitle')}
+
${t('tasks.emptyDescription')}
`; } @@ -227,11 +246,12 @@ function renderModalContent({ task = null, users = [] } = {}) { `` ).join(''); + const catLabels = CATEGORY_LABELS(); const categoryOptions = CATEGORIES.map((c) => - `` + `` ).join(''); - const priorityOptions = PRIORITIES.map((p) => + const priorityOptions = PRIORITIES().map((p) => `` ).join(''); @@ -241,36 +261,36 @@ function renderModalContent({ task = null, users = [] } = {}) {
- +
- Dieses Feld ist erforderlich. + ${t('common.required')}
- +
- +
- + @@ -279,30 +299,30 @@ function renderModalContent({ task = null, users = [] } = {}) {
- +
- +
- +
${isEdit ? `
- + @@ -315,9 +335,9 @@ function renderModalContent({ task = null, users = [] } = {}) { `; @@ -375,7 +395,7 @@ async function loadTaskForEdit(id) { function openTaskModal({ task = null, users = [] } = {}, container) { const isEdit = !!task; openSharedModal({ - title: isEdit ? 'Aufgabe bearbeiten' : 'Neue Aufgabe', + title: isEdit ? t('tasks.editTask') : t('tasks.newTask'), content: renderModalContent({ task, users }), size: 'lg', onSave(panel) { @@ -408,9 +428,9 @@ async function handleFormSubmit(e, container) { errorEl.hidden = true; submitBtn.disabled = true; - submitBtn.textContent = 'Wird gespeichert…'; + submitBtn.textContent = t('common.saving'); - const originalLabel = taskId ? 'Speichern' : 'Erstellen'; + const originalLabel = taskId ? t('common.save') : t('common.create'); const rrule = getRRuleValues(document, 'task'); const body = { @@ -429,10 +449,10 @@ async function handleFormSubmit(e, container) { try { if (taskId) { await api.put(`/tasks/${taskId}`, body); - window.oikos.showToast('Aufgabe gespeichert.', 'success'); + window.oikos.showToast(t('tasks.savedToast'), 'success'); } else { await api.post('/tasks', body); - window.oikos.showToast('Aufgabe erstellt.', 'success'); + window.oikos.showToast(t('tasks.createdToast'), 'success'); } btnSuccess(submitBtn, originalLabel); setTimeout(() => closeModal(), 700); @@ -447,11 +467,11 @@ async function handleFormSubmit(e, container) { } async function handleDeleteTask(id, container) { - if (!confirm('Aufgabe und alle Teilaufgaben löschen?')) return; + if (!confirm(t('tasks.deleteConfirm'))) return; try { await api.delete(`/tasks/${id}`); closeModal(); - window.oikos.showToast('Aufgabe gelöscht.', 'default'); + window.oikos.showToast(t('tasks.deletedToast'), 'default'); await loadTasks(container); } catch (err) { window.oikos.showToast(err.message, 'danger'); @@ -459,7 +479,7 @@ async function handleDeleteTask(id, container) { } async function handleAddSubtask(parentId, container) { - const title = prompt('Teilaufgabe:'); + const title = prompt(t('tasks.subtaskPrompt')); if (!title?.trim()) return; try { await api.post('/tasks', { title: title.trim(), parent_task_id: parentId }); @@ -473,10 +493,10 @@ async function handleAddSubtask(parentId, container) { // Kanban-Ansicht // -------------------------------------------------------- -const KANBAN_COLS = [ - { status: 'open', label: 'Offen', colorVar: '--color-text-secondary' }, - { status: 'in_progress', label: 'In Bearbeitung', colorVar: '--color-warning' }, - { status: 'done', label: 'Erledigt', colorVar: '--color-success' }, +const KANBAN_COLS = () => [ + { status: 'open', label: t('tasks.kanbanOpen'), colorVar: '--color-text-secondary' }, + { status: 'in_progress', label: t('tasks.kanbanInProgress'), colorVar: '--color-warning' }, + { status: 'done', label: t('tasks.kanbanDone'), colorVar: '--color-success' }, ]; function renderKanbanCard(task) { @@ -503,8 +523,9 @@ function renderKanban(container) { const listEl = container.querySelector('#task-list'); if (!listEl) return; + const cols = KANBAN_COLS(); const grouped = {}; - for (const col of KANBAN_COLS) grouped[col.status] = []; + for (const col of cols) grouped[col.status] = []; for (const t of state.tasks) { if (grouped[t.status]) grouped[t.status].push(t); else grouped['open'].push(t); @@ -512,7 +533,7 @@ function renderKanban(container) { listEl.innerHTML = `
- ${KANBAN_COLS.map((col) => ` + ${cols.map((col) => `
@@ -606,7 +627,7 @@ function wireKanbanDrag(container) { const task = await loadTaskForEdit(card.dataset.taskId); openTaskModal({ task, users: state.users }, container); } catch (err) { - window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger'); + window.oikos.showToast(t('tasks.loadError'), 'danger'); } } }); @@ -635,15 +656,17 @@ function renderFilters(container) { if (!bar) return; const chips = []; + const statusLabels = STATUS_LABELS(); + const priorityLabels = PRIORITY_LABELS(); if (state.filters.status) { chips.push(` - ${STATUS_LABELS[state.filters.status]} + ${statusLabels[state.filters.status]} `); } if (state.filters.priority) { chips.push(` - ${PRIORITY_LABELS[state.filters.priority]} + ${priorityLabels[state.filters.priority]} `); } @@ -657,12 +680,12 @@ function renderFilters(container) { // Inaktive Filter-Chips (zum Aktivieren) if (!state.filters.status) { - STATUSES.forEach((s) => { + STATUSES().forEach((s) => { chips.push(`${s.label}`); }); } if (!state.filters.priority) { - PRIORITIES.forEach((p) => { + PRIORITIES().forEach((p) => { chips.push(`${p.label}`); }); } @@ -802,7 +825,7 @@ function wireSwipeGestures(container) { const task = await loadTaskForEdit(taskId); openTaskModal({ task, users: state.users }, container); } catch (err) { - window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger'); + window.oikos.showToast(t('tasks.loadError'), 'danger'); } } else { @@ -912,7 +935,7 @@ function wireTaskList(container) { const task = await loadTaskForEdit(id); openTaskModal({ task, users: state.users }, container); } catch (err) { - window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger'); + window.oikos.showToast(t('tasks.loadError'), 'danger'); } } @@ -931,7 +954,7 @@ export async function render(container, { user }) { container.innerHTML = `
-

Aufgaben

+

${t('tasks.title')}

- - + +
@@ -963,7 +986,7 @@ export async function render(container, { user }) {
`).join('')}
-
@@ -981,7 +1004,7 @@ export async function render(container, { user }) { state.users = metaData.users ?? []; } catch (err) { console.error('[Tasks] Ladefehler:', err.message); - window.oikos.showToast('Aufgaben konnten nicht geladen werden.', 'danger'); + window.oikos.showToast(t('tasks.loadError'), 'danger'); state.tasks = []; state.users = []; }