feat: i18n login, dashboard, tasks pages

This commit is contained in:
Ulas
2026-03-31 22:31:57 +02:00
parent af8f9ccb56
commit f6a4879dd0
3 changed files with 160 additions and 132 deletions
+44 -40
View File
@@ -5,6 +5,7 @@
*/ */
import { api } from '/api.js'; import { api } from '/api.js';
import { t } from '/i18n.js';
// Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert. // Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert.
let _fabController = null; let _fabController = null;
@@ -15,8 +16,9 @@ let _fabController = null;
function greeting(displayName) { function greeting(displayName) {
const h = new Date().getHours(); const h = new Date().getHours();
const tageszeit = h < 12 ? 'Morgen' : h < 18 ? 'Tag' : 'Abend'; if (h < 12) return t('dashboard.greetingMorning', { name: displayName });
return `Guten ${tageszeit}, ${displayName}`; if (h < 18) return t('dashboard.greetingDay', { name: displayName });
return t('dashboard.greetingEvening', { name: displayName });
} }
function formatDate(date = new Date()) { function formatDate(date = new Date()) {
@@ -49,21 +51,21 @@ function formatDueDate(dateStr) {
const diffMs = due - now; const diffMs = due - now;
const diffH = diffMs / (1000 * 60 * 60); const diffH = diffMs / (1000 * 60 * 60);
if (diffMs < 0) return { text: 'Überfällig', overdue: true }; if (diffMs < 0) return { text: t('dashboard.overdue'), overdue: true };
if (diffH < 24) return { text: 'Heute fällig', overdue: false }; if (diffH < 24) return { text: t('dashboard.dueSoon'), overdue: false };
if (diffH < 48) return { text: 'Morgen fällig', overdue: false }; if (diffH < 48) return { text: t('dashboard.dueTomorrow'), overdue: false };
return { return {
text: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }), text: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }),
overdue: false, overdue: false,
}; };
} }
const MEAL_LABELS = { const MEAL_LABELS = () => ({
breakfast: 'Frühstück', breakfast: t('meals.typeBreakfast'),
lunch: 'Mittagessen', lunch: t('meals.typeLunch'),
dinner: 'Abendessen', dinner: t('meals.typeDinner'),
snack: 'Snack', snack: t('meals.typeSnack'),
}; });
const MEAL_ICONS = { const MEAL_ICONS = {
breakfast: 'sunrise', breakfast: 'sunrise',
@@ -76,7 +78,8 @@ function initials(name = '') {
return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase(); 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 const badge = count != null
? `<span class="widget__badge">${count}</span>` ? `<span class="widget__badge">${count}</span>`
: ''; : '';
@@ -122,17 +125,17 @@ function renderGreeting(user, stats = {}) {
if (urgentCount > 0) if (urgentCount > 0)
statChips.push(`<span class="greeting-chip greeting-chip--warn"> statChips.push(`<span class="greeting-chip greeting-chip--warn">
<i data-lucide="alert-circle" style="${chipIcon}" aria-hidden="true"></i> <i data-lucide="alert-circle" style="${chipIcon}" aria-hidden="true"></i>
${urgentCount} dring. Aufgabe${urgentCount > 1 ? 'n' : ''} ${urgentCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: urgentCount }) : t('dashboard.urgentTasksChip', { count: urgentCount })}
</span>`); </span>`);
if (todayEventCount > 0) if (todayEventCount > 0)
statChips.push(`<span class="greeting-chip"> statChips.push(`<span class="greeting-chip">
<i data-lucide="calendar" style="${chipIcon}" aria-hidden="true"></i> <i data-lucide="calendar" style="${chipIcon}" aria-hidden="true"></i>
${todayEventCount} Termin${todayEventCount > 1 ? 'e' : ''} heute ${todayEventCount > 1 ? t('dashboard.eventsChipPlural', { count: todayEventCount }) : t('dashboard.eventsChip', { count: todayEventCount })}
</span>`); </span>`);
if (todayMealTitle) if (todayMealTitle)
statChips.push(`<span class="greeting-chip"> statChips.push(`<span class="greeting-chip">
<i data-lucide="utensils" style="${chipIcon}" aria-hidden="true"></i> <i data-lucide="utensils" style="${chipIcon}" aria-hidden="true"></i>
Heute: ${todayMealTitle} ${t('dashboard.todayMealChip', { title: todayMealTitle })}
</span>`); </span>`);
return ` return `
@@ -149,10 +152,10 @@ function renderGreeting(user, stats = {}) {
function renderUrgentTasks(tasks) { function renderUrgentTasks(tasks) {
if (!tasks.length) { if (!tasks.length) {
return `<div class="widget"> return `<div class="widget">
${widgetHeader('check-square', 'Aufgaben', 0, '/tasks')} ${widgetHeader('check-square', t('nav.tasks'), 0, '/tasks')}
<div class="widget__empty"> <div class="widget__empty">
<i data-lucide="check-circle" class="empty-state__icon" style="color:var(--color-success)" aria-hidden="true"></i> <i data-lucide="check-circle" class="empty-state__icon" style="color:var(--color-success)" aria-hidden="true"></i>
<div>Alles erledigt</div> <div>${t('dashboard.allDone')}</div>
</div> </div>
</div>`; </div>`;
} }
@@ -174,7 +177,7 @@ function renderUrgentTasks(tasks) {
}).join(''); }).join('');
return `<div class="widget"> return `<div class="widget">
${widgetHeader('check-square', 'Aufgaben', tasks.length, '/tasks')} ${widgetHeader('check-square', t('nav.tasks'), tasks.length, '/tasks')}
<div class="widget__body">${items}</div> <div class="widget__body">${items}</div>
</div>`; </div>`;
} }
@@ -182,10 +185,10 @@ function renderUrgentTasks(tasks) {
function renderUpcomingEvents(events) { function renderUpcomingEvents(events) {
if (!events.length) { if (!events.length) {
return `<div class="widget"> return `<div class="widget">
${widgetHeader('calendar', 'Termine', 0, '/calendar')} ${widgetHeader('calendar', t('nav.calendar'), 0, '/calendar')}
<div class="widget__empty"> <div class="widget__empty">
<i data-lucide="calendar-check" class="empty-state__icon" aria-hidden="true"></i> <i data-lucide="calendar-check" class="empty-state__icon" aria-hidden="true"></i>
<div>Keine Termine</div> <div>${t('dashboard.noEvents')}</div>
</div> </div>
</div>`; </div>`;
} }
@@ -194,14 +197,14 @@ function renderUpcomingEvents(events) {
const items = events.map((e) => { const items = events.map((e) => {
const d = new Date(e.start_datetime); const d = new Date(e.start_datetime);
const isToday = d.toDateString() === today; 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 ` return `
<div class="event-item" data-route="/calendar" role="button" tabindex="0"> <div class="event-item" data-route="/calendar" role="button" tabindex="0">
<div class="event-item__bar" style="background-color:${e.color || 'var(--color-accent)'}"></div> <div class="event-item__bar" style="background-color:${e.color || 'var(--color-accent)'}"></div>
<div class="event-item__content"> <div class="event-item__content">
<div class="event-item__title">${e.title}</div> <div class="event-item__title">${e.title}</div>
<div class="event-item__time"> <div class="event-item__time">
<span class="event-time-badge ${isToday ? 'event-time-badge--today' : ''}">${isToday ? 'Heute' : formatDateTime(e.start_datetime).split(',')[0]}</span> <span class="event-time-badge ${isToday ? 'event-time-badge--today' : ''}">${isToday ? t('common.today') : formatDateTime(e.start_datetime).split(',')[0]}</span>
${timeStr} ${timeStr}
${e.location ? ` · ${e.location}` : ''} ${e.location ? ` · ${e.location}` : ''}
</div> </div>
@@ -211,7 +214,7 @@ function renderUpcomingEvents(events) {
}).join(''); }).join('');
return `<div class="widget"> return `<div class="widget">
${widgetHeader('calendar', 'Termine', events.length, '/calendar')} ${widgetHeader('calendar', t('nav.calendar'), events.length, '/calendar')}
<div class="widget__body">${items}</div> <div class="widget__body">${items}</div>
</div>`; </div>`;
} }
@@ -219,19 +222,20 @@ function renderUpcomingEvents(events) {
function renderTodayMeals(meals) { function renderTodayMeals(meals) {
const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack']; const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack'];
const mealLabels = MEAL_LABELS();
const slots = MEAL_ORDER.map((type) => { const slots = MEAL_ORDER.map((type) => {
const meal = meals.find((m) => m.meal_type === type); const meal = meals.find((m) => m.meal_type === type);
return ` return `
<div class="meal-slot ${meal ? 'meal-slot--filled' : ''}" data-route="/meals" role="button" tabindex="0"> <div class="meal-slot ${meal ? 'meal-slot--filled' : ''}" data-route="/meals" role="button" tabindex="0">
<i data-lucide="${MEAL_ICONS[type]}" class="meal-slot__icon" aria-hidden="true"></i> <i data-lucide="${MEAL_ICONS[type]}" class="meal-slot__icon" aria-hidden="true"></i>
<div class="meal-slot__type">${MEAL_LABELS[type]}</div> <div class="meal-slot__type">${mealLabels[type]}</div>
<div class="meal-slot__title">${meal ? meal.title : '—'}</div> <div class="meal-slot__title">${meal ? meal.title : '—'}</div>
</div> </div>
`; `;
}).join(''); }).join('');
return `<div class="widget widget--meals"> return `<div class="widget widget--meals">
${widgetHeader('utensils', 'Heute essen', null, '/meals', 'Woche')} ${widgetHeader('utensils', t('dashboard.todayMeals'), null, '/meals', t('dashboard.weekLink'))}
<div class="meal-slots">${slots}</div> <div class="meal-slots">${slots}</div>
</div>`; </div>`;
} }
@@ -239,10 +243,10 @@ function renderTodayMeals(meals) {
function renderPinnedNotes(notes) { function renderPinnedNotes(notes) {
if (!notes.length) { if (!notes.length) {
return `<div class="widget"> return `<div class="widget">
${widgetHeader('pin', 'Pinnwand', 0, '/notes')} ${widgetHeader('pin', t('nav.notes'), 0, '/notes')}
<div class="widget__empty"> <div class="widget__empty">
<i data-lucide="sticky-note" class="empty-state__icon" aria-hidden="true"></i> <i data-lucide="sticky-note" class="empty-state__icon" aria-hidden="true"></i>
<div>Keine angepinnten Notizen</div> <div>${t('dashboard.noPinnedNotes')}</div>
</div> </div>
</div>`; </div>`;
} }
@@ -256,7 +260,7 @@ function renderPinnedNotes(notes) {
`).join(''); `).join('');
return `<div class="widget widget--wide"> return `<div class="widget widget--wide">
${widgetHeader('pin', 'Pinnwand', notes.length, '/notes')} ${widgetHeader('pin', t('nav.notes'), notes.length, '/notes')}
<div class="notes-grid-widget">${items}</div> <div class="notes-grid-widget">${items}</div>
</div>`; </div>`;
} }
@@ -290,7 +294,7 @@ function renderWeatherWidget(weather) {
return ` return `
<div class="widget weather-widget" id="weather-widget"> <div class="widget weather-widget" id="weather-widget">
<button class="weather-widget__refresh" id="weather-refresh-btn" aria-label="Wetter aktualisieren" title="Aktualisieren"> <button class="weather-widget__refresh" id="weather-refresh-btn" aria-label="${t('dashboard.weatherRefresh')}" title="${t('dashboard.weatherRefreshTitle')}">
<i data-lucide="refresh-cw" style="width:14px;height:14px;" aria-hidden="true"></i> <i data-lucide="refresh-cw" style="width:14px;height:14px;" aria-hidden="true"></i>
</button> </button>
<div class="weather-widget__inner"> <div class="weather-widget__inner">
@@ -300,7 +304,7 @@ function renderWeatherWidget(weather) {
<div class="weather-widget__desc">${current.desc}</div> <div class="weather-widget__desc">${current.desc}</div>
<div class="weather-widget__city">${city}</div> <div class="weather-widget__city">${city}</div>
<div class="weather-widget__meta"> <div class="weather-widget__meta">
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 })}
</div> </div>
</div> </div>
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png" <img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png"
@@ -315,17 +319,17 @@ function renderWeatherWidget(weather) {
// FAB Speed-Dial // FAB Speed-Dial
// -------------------------------------------------------- // --------------------------------------------------------
const FAB_ACTIONS = [ const FAB_ACTIONS = () => [
{ route: '/tasks', label: 'Aufgabe', icon: 'check-square' }, { route: '/tasks', label: t('dashboard.fabTask'), icon: 'check-square' },
{ route: '/calendar', label: 'Termin', icon: 'calendar-plus' }, { route: '/calendar', label: t('dashboard.fabCalendar'), icon: 'calendar-plus' },
{ route: '/shopping', label: 'Einkauf', icon: 'shopping-cart' }, { route: '/shopping', label: t('dashboard.fabShopping'), icon: 'shopping-cart' },
{ route: '/notes', label: 'Notiz', icon: 'sticky-note' }, { route: '/notes', label: t('dashboard.fabNote'), icon: 'sticky-note' },
]; ];
function renderFab() { function renderFab() {
const actionsHtml = FAB_ACTIONS.map((a) => ` const actionsHtml = FAB_ACTIONS().map((a) => `
<div class="fab-action" data-route="${a.route}" role="button" tabindex="-1" <div class="fab-action" data-route="${a.route}" role="button" tabindex="-1"
aria-label="${a.label} hinzufügen"> aria-label="${a.label}">
<span class="fab-action__label">${a.label}</span> <span class="fab-action__label">${a.label}</span>
<button class="fab-action__btn" tabindex="-1" aria-hidden="true"> <button class="fab-action__btn" tabindex="-1" aria-hidden="true">
<i data-lucide="${a.icon}" aria-hidden="true"></i> <i data-lucide="${a.icon}" aria-hidden="true"></i>
@@ -335,7 +339,7 @@ function renderFab() {
return ` return `
<div class="fab-container" id="fab-container"> <div class="fab-container" id="fab-container">
<button class="fab-main" id="fab-main" aria-label="Schnellaktionen" aria-expanded="false"> <button class="fab-main" id="fab-main" aria-label="${t('nav.quickActions')}" aria-expanded="false">
<i data-lucide="plus" aria-hidden="true"></i> <i data-lucide="plus" aria-hidden="true"></i>
</button> </button>
<div class="fab-actions" id="fab-actions" aria-hidden="true"> <div class="fab-actions" id="fab-actions" aria-hidden="true">
@@ -433,7 +437,7 @@ export async function render(container, { user }) {
weather = weatherRes.data ?? null; weather = weatherRes.data ?? null;
} catch (err) { } catch (err) {
console.error('[Dashboard] Ladefehler:', err.message); console.error('[Dashboard] Ladefehler:', err.message);
window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning'); window.oikos?.showToast(t('dashboard.loadError'), 'warning');
} }
const today = new Date().toDateString(); const today = new Date().toDateString();
@@ -449,7 +453,7 @@ export async function render(container, { user }) {
container.innerHTML = ` container.innerHTML = `
<div class="dashboard"> <div class="dashboard">
<h1 class="sr-only">Übersicht</h1> <h1 class="sr-only">${t('dashboard.title')}</h1>
<div class="dashboard__grid"> <div class="dashboard__grid">
${renderGreeting(user, stats)} ${renderGreeting(user, stats)}
${renderWeatherWidget(weather)} ${renderWeatherWidget(weather)}
+12 -11
View File
@@ -5,6 +5,7 @@
*/ */
import { auth } from '/api.js'; import { auth } from '/api.js';
import { t } from '/i18n.js';
/** /**
* Rendert die Login-Seite in den gegebenen Container. * Rendert die Login-Seite in den gegebenen Container.
@@ -15,13 +16,13 @@ export async function render(container) {
<main class="login-page" id="main-content"> <main class="login-page" id="main-content">
<div class="login-hero"> <div class="login-hero">
<h1 class="login-hero__title">Oikos</h1> <h1 class="login-hero__title">Oikos</h1>
<p class="login-hero__tagline">Familienplanung. Sicher. Datenschutzfreundlich. Open Source.</p> <p class="login-hero__tagline">${t('login.tagline')}</p>
</div> </div>
<div class="login-card card card--padded"> <div class="login-card card card--padded">
<form class="login-form" id="login-form" novalidate> <form class="login-form" id="login-form" novalidate>
<div class="form-group"> <div class="form-group">
<label class="label" for="username">Benutzername</label> <label class="label" for="username">${t('login.usernameLabel')}</label>
<input <input
class="input" class="input"
type="text" type="text"
@@ -30,20 +31,20 @@ export async function render(container) {
autocomplete="username" autocomplete="username"
autocapitalize="none" autocapitalize="none"
autocorrect="off" autocorrect="off"
placeholder="benutzername" placeholder="${t('login.usernamePlaceholder')}"
required required
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="label" for="password">Passwort</label> <label class="label" for="password">${t('login.passwordLabel')}</label>
<input <input
class="input" class="input"
type="password" type="password"
id="password" id="password"
name="password" name="password"
autocomplete="current-password" autocomplete="current-password"
placeholder="••••••••" placeholder="${t('login.passwordPlaceholder')}"
required required
/> />
</div> </div>
@@ -51,7 +52,7 @@ export async function render(container) {
<div class="login-error" id="login-error" role="alert" aria-live="polite" hidden></div> <div class="login-error" id="login-error" role="alert" aria-live="polite" hidden></div>
<button type="submit" class="btn btn--primary login-form__submit" id="login-btn"> <button type="submit" class="btn btn--primary login-form__submit" id="login-btn">
Anmelden ${t('login.loginButton')}
</button> </button>
</form> </form>
</div> </div>
@@ -70,24 +71,24 @@ export async function render(container) {
const password = form.password.value; const password = form.password.value;
if (!username || !password) { if (!username || !password) {
showError(errorEl, 'Bitte alle Felder ausfüllen.'); showError(errorEl, t('common.allFieldsRequired'));
return; return;
} }
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.textContent = 'Wird angemeldet …'; submitBtn.textContent = t('login.loggingIn');
try { try {
const result = await auth.login(username, password); const result = await auth.login(username, password);
window.oikos.navigate('/', result.user); window.oikos.navigate('/', result.user);
} catch (err) { } catch (err) {
showError(errorEl, err.status === 429 showError(errorEl, err.status === 429
? 'Zu viele Versuche. Bitte warte kurz.' ? t('login.tooManyAttempts')
: 'Ungültige Anmeldedaten.' : t('login.invalidCredentials')
); );
} finally { } finally {
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.textContent = 'Anmelden'; submitBtn.textContent = t('login.loginButton');
} }
}); });
} }
+104 -81
View File
@@ -8,31 +8,43 @@ import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js'; import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError } from '/components/modal.js'; import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js'; import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
// -------------------------------------------------------- // --------------------------------------------------------
// Konstanten // Konstanten
// -------------------------------------------------------- // --------------------------------------------------------
const PRIORITIES = [ const PRIORITIES = () => [
{ value: 'urgent', label: 'Dringend', color: 'var(--color-priority-urgent)' }, { value: 'urgent', label: t('tasks.priorityUrgent'), color: 'var(--color-priority-urgent)' },
{ value: 'high', label: 'Hoch', color: 'var(--color-priority-high)' }, { value: 'high', label: t('tasks.priorityHigh'), color: 'var(--color-priority-high)' },
{ value: 'medium', label: 'Mittel', color: 'var(--color-priority-medium)' }, { value: 'medium', label: t('tasks.priorityMedium'), color: 'var(--color-priority-medium)' },
{ value: 'low', label: 'Niedrig', color: 'var(--color-priority-low)' }, { value: 'low', label: t('tasks.priorityLow'), color: 'var(--color-priority-low)' },
]; ];
const STATUSES = [ const STATUSES = () => [
{ value: 'open', label: 'Offen' }, { value: 'open', label: t('tasks.statusOpen') },
{ value: 'in_progress', label: 'In Bearbeitung'}, { value: 'in_progress', label: t('tasks.statusInProgress') },
{ value: 'done', label: 'Erledigt' }, { value: 'done', label: t('tasks.statusDone') },
]; ];
const CATEGORIES = [ const CATEGORIES = [
'Haushalt','Schule','Einkauf','Reparatur', 'Haushalt', 'Schule', 'Einkauf', 'Reparatur',
'Gesundheit','Finanzen','Freizeit','Sonstiges', 'Gesundheit', 'Finanzen', 'Freizeit', 'Sonstiges',
]; ];
const PRIORITY_LABELS = Object.fromEntries(PRIORITIES.map((p) => [p.value, p.label])); const CATEGORY_LABELS = () => ({
const STATUS_LABELS = Object.fromEntries(STATUSES.map((s) => [s.value, s.label])); 'Haushalt': t('tasks.categoryHousehold'),
'Schule': t('tasks.categorySchool'),
'Einkauf': t('tasks.categoryShopping'),
'Reparatur': t('tasks.categoryRepair'),
'Gesundheit': t('tasks.categoryHealth'),
'Finanzen': t('tasks.categoryFinance'),
'Freizeit': t('tasks.categoryLeisure'),
'Sonstiges': t('tasks.categoryMisc'),
});
const PRIORITY_LABELS = () => Object.fromEntries(PRIORITIES().map((p) => [p.value, p.label]));
const STATUS_LABELS = () => Object.fromEntries(STATUSES().map((s) => [s.value, s.label]));
// -------------------------------------------------------- // --------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
@@ -49,9 +61,9 @@ function formatDueDate(dateStr) {
now.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0);
const diffDays = Math.round((due - now) / 86400000); const diffDays = Math.round((due - now) / 86400000);
if (diffDays < 0) return { label: `${Math.abs(diffDays)}d überfällig`, cls: 'due-date--overdue' }; if (diffDays < 0) return { label: t('tasks.overdueDay', { count: Math.abs(diffDays) }), cls: 'due-date--overdue' };
if (diffDays === 0) return { label: 'Heute fällig', cls: 'due-date--today' }; if (diffDays === 0) return { label: t('tasks.dueToday'), cls: 'due-date--today' };
if (diffDays === 1) return { label: 'Morgen fällig', cls: '' }; if (diffDays === 1) return { label: t('tasks.dueTomorrow'), cls: '' };
return { label: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }), cls: '' }; return { label: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }), cls: '' };
} }
@@ -67,21 +79,28 @@ function groupBy(tasks, mode) {
} }
// mode === 'due' // mode === 'due'
for (const t of tasks) { const groupOverdue = t('tasks.groupOverdue');
const groupToday = t('tasks.groupToday');
const groupThisWeek = t('tasks.groupThisWeek');
const groupNextWeek = t('tasks.groupNextWeek');
const groupLater = t('tasks.groupLater');
const groupNoDate = t('tasks.groupNoDate');
for (const task of tasks) {
let key; let key;
if (!t.due_date) key = 'Kein Datum'; if (!task.due_date) key = groupNoDate;
else { else {
const diff = Math.round((new Date(t.due_date) - new Date().setHours(0,0,0,0)) / 86400000); const diff = Math.round((new Date(task.due_date) - new Date().setHours(0,0,0,0)) / 86400000);
if (diff < 0) key = 'Überfällig'; if (diff < 0) key = groupOverdue;
else if (diff === 0) key = 'Heute'; else if (diff === 0) key = groupToday;
else if (diff <= 3) key = 'Diese Woche'; else if (diff <= 3) key = groupThisWeek;
else if (diff <= 7) key = 'Nächste Woche'; else if (diff <= 7) key = groupNextWeek;
else key = 'Später'; else key = groupLater;
} }
(groups[key] = groups[key] || []).push(t); (groups[key] = groups[key] || []).push(task);
} }
const order = ['Überfällig', 'Heute', 'Diese Woche', 'Nächste Woche', 'Später', 'Kein Datum']; const order = [groupOverdue, groupToday, groupThisWeek, groupNextWeek, groupLater, groupNoDate];
return order.filter((k) => groups[k]).map((k) => [k, groups[k]]); return order.filter((k) => groups[k]).map((k) => [k, groups[k]]);
} }
@@ -92,7 +111,7 @@ function groupBy(tasks, mode) {
function renderPriorityBadge(priority) { function renderPriorityBadge(priority) {
return `<span class="priority-badge priority-badge--${priority}"> return `<span class="priority-badge priority-badge--${priority}">
<span class="priority-dot priority-dot--${priority}"></span> <span class="priority-dot priority-dot--${priority}"></span>
${PRIORITY_LABELS[priority] ?? priority} ${PRIORITY_LABELS()[priority] ?? priority}
</span>`; </span>`;
} }
@@ -110,11 +129,11 @@ function renderSwipeRow(task, innerHtml) {
<div class="swipe-row" data-swipe-id="${task.id}" data-swipe-status="${task.status}"> <div class="swipe-row" data-swipe-id="${task.id}" data-swipe-status="${task.status}">
<div class="swipe-reveal swipe-reveal--done" aria-hidden="true"> <div class="swipe-reveal swipe-reveal--done" aria-hidden="true">
<i data-lucide="${isDone ? 'rotate-ccw' : 'check'}" style="width:22px;height:22px" aria-hidden="true"></i> <i data-lucide="${isDone ? 'rotate-ccw' : 'check'}" style="width:22px;height:22px" aria-hidden="true"></i>
<span>${isDone ? 'Öffnen' : 'Erledigt'}</span> <span>${isDone ? t('tasks.swipeOpen') : t('tasks.swipeDone')}</span>
</div> </div>
<div class="swipe-reveal swipe-reveal--edit" aria-hidden="true"> <div class="swipe-reveal swipe-reveal--edit" aria-hidden="true">
<i data-lucide="pencil" style="width:22px;height:22px" aria-hidden="true"></i> <i data-lucide="pencil" style="width:22px;height:22px" aria-hidden="true"></i>
<span>Bearbeiten</span> <span>${t('tasks.swipeEdit')}</span>
</div> </div>
${innerHtml} ${innerHtml}
</div>`; </div>`;
@@ -133,7 +152,7 @@ function renderTaskCard(task, opts = {}) {
data-subtask-id="${s.id}"> data-subtask-id="${s.id}">
<button class="subtask-item__checkbox ${s.status === 'done' ? 'subtask-item__checkbox--done' : ''}" <button class="subtask-item__checkbox ${s.status === 'done' ? 'subtask-item__checkbox--done' : ''}"
data-action="toggle-subtask" data-id="${s.id}" data-action="toggle-subtask" data-id="${s.id}"
data-status="${s.status}" aria-label="${s.title} als erledigt markieren"> data-status="${s.status}" aria-label="${t('tasks.subtaskMarkDone', { title: s.title })}">
${s.status === 'done' ? '<i data-lucide="check" style="width:10px;height:10px;color:#fff" aria-hidden="true"></i>' : ''} ${s.status === 'done' ? '<i data-lucide="check" style="width:10px;height:10px;color:#fff" aria-hidden="true"></i>' : ''}
</button> </button>
<span class="subtask-item__title">${s.title}</span> <span class="subtask-item__title">${s.title}</span>
@@ -145,7 +164,7 @@ function renderTaskCard(task, opts = {}) {
<div class="task-card__main"> <div class="task-card__main">
<button class="task-status-btn task-status-btn--${task.status}" <button class="task-status-btn task-status-btn--${task.status}"
data-action="toggle-status" data-id="${task.id}" data-status="${task.status}" data-action="toggle-status" data-id="${task.id}" data-status="${task.status}"
aria-label="${task.title} als erledigt markieren"> aria-label="${t('tasks.markDone', { title: task.title })}">
<i data-lucide="check" class="task-status-btn__check" aria-hidden="true"></i> <i data-lucide="check" class="task-status-btn__check" aria-hidden="true"></i>
</button> </button>
@@ -157,7 +176,7 @@ function renderTaskCard(task, opts = {}) {
${renderPriorityBadge(task.priority)} ${renderPriorityBadge(task.priority)}
${renderDueDate(task.due_date)} ${renderDueDate(task.due_date)}
${task.is_recurring ? '<span class="due-date" aria-label="Wiederkehrend"><i data-lucide="repeat" style="width:12px;height:12px" aria-hidden="true"></i></span>' : ''} ${task.is_recurring ? '<span class="due-date" aria-label="Wiederkehrend"><i data-lucide="repeat" style="width:12px;height:12px" aria-hidden="true"></i></span>' : ''}
${task.category !== 'Sonstiges' ? `<span class="due-date">${task.category}</span>` : ''} ${task.category !== 'Sonstiges' ? `<span class="due-date">${CATEGORY_LABELS()[task.category] ?? task.category}</span>` : ''}
</div> </div>
</div> </div>
@@ -168,14 +187,14 @@ function renderTaskCard(task, opts = {}) {
</div>` : ''} </div>` : ''}
<button class="btn btn--ghost btn--icon" data-action="edit-task" data-id="${task.id}" <button class="btn btn--ghost btn--icon" data-action="edit-task" data-id="${task.id}"
aria-label="Aufgabe bearbeiten" style="min-height:unset;width:36px;height:36px"> aria-label="${t('tasks.editButton')}" style="min-height:unset;width:36px;height:36px">
<i data-lucide="pencil" style="width:16px;height:16px" aria-hidden="true"></i> <i data-lucide="pencil" style="width:16px;height:16px" aria-hidden="true"></i>
</button> </button>
</div> </div>
${progress !== null ? ` ${progress !== null ? `
<div class="subtask-progress" data-action="toggle-subtasks" data-id="${task.id}" <div class="subtask-progress" data-action="toggle-subtasks" data-id="${task.id}"
aria-label="Teilaufgaben anzeigen"> aria-label="${t('tasks.subtaskToggle')}">
<div class="subtask-progress__bar-wrap"> <div class="subtask-progress__bar-wrap">
<div class="subtask-progress__bar-fill" style="width:${progress}%"></div> <div class="subtask-progress__bar-fill" style="width:${progress}%"></div>
</div> </div>
@@ -187,7 +206,7 @@ function renderTaskCard(task, opts = {}) {
id="subtasks-${task.id}"> id="subtasks-${task.id}">
${subtasksHtml} ${subtasksHtml}
<button class="subtask-item__add" data-action="add-subtask" data-parent="${task.id}"> <button class="subtask-item__add" data-action="add-subtask" data-parent="${task.id}">
+ Teilaufgabe hinzufügen ${t('tasks.subtaskAdd')}
</button> </button>
</div>` : ''} </div>` : ''}
</div>`; </div>`;
@@ -200,8 +219,8 @@ function renderTaskGroups(tasks, groupMode) {
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/> <polyline points="22 4 12 14.01 9 11.01"/>
</svg> </svg>
<div class="empty-state__title">Keine Aufgaben — alles erledigt?</div> <div class="empty-state__title">${t('tasks.emptyTitle')}</div>
<div class="empty-state__description">Neue Aufgaben über den + Button erstellen.</div> <div class="empty-state__description">${t('tasks.emptyDescription')}</div>
</div>`; </div>`;
} }
@@ -227,11 +246,12 @@ function renderModalContent({ task = null, users = [] } = {}) {
`<option value="${u.id}" ${task?.assigned_to === u.id ? 'selected' : ''}>${u.display_name}</option>` `<option value="${u.id}" ${task?.assigned_to === u.id ? 'selected' : ''}>${u.display_name}</option>`
).join(''); ).join('');
const catLabels = CATEGORY_LABELS();
const categoryOptions = CATEGORIES.map((c) => const categoryOptions = CATEGORIES.map((c) =>
`<option value="${c}" ${(task?.category ?? 'Sonstiges') === c ? 'selected' : ''}>${c}</option>` `<option value="${c}" ${(task?.category ?? 'Sonstiges') === c ? 'selected' : ''}>${catLabels[c] ?? c}</option>`
).join(''); ).join('');
const priorityOptions = PRIORITIES.map((p) => const priorityOptions = PRIORITIES().map((p) =>
`<option value="${p.value}" ${(task?.priority ?? 'medium') === p.value ? 'selected' : ''}>${p.label}</option>` `<option value="${p.value}" ${(task?.priority ?? 'medium') === p.value ? 'selected' : ''}>${p.label}</option>`
).join(''); ).join('');
@@ -241,36 +261,36 @@ function renderModalContent({ task = null, users = [] } = {}) {
<div class="form-group"> <div class="form-group">
<div class="form-field"> <div class="form-field">
<label class="label" for="task-title">Titel *</label> <label class="label" for="task-title">${t('tasks.titleLabel')}</label>
<input class="input" type="text" id="task-title" name="title" <input class="input" type="text" id="task-title" name="title"
value="${task?.title ?? ''}" placeholder="Was muss erledigt werden?" value="${task?.title ?? ''}" placeholder="${t('tasks.titlePlaceholder')}"
required autocomplete="off"> required autocomplete="off">
<div class="form-field__error"> <div class="form-field__error">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/> stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12" y2="16.01"/> <line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12" y2="16.01"/>
</svg> </svg>
Dieses Feld ist erforderlich. ${t('common.required')}
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="label" for="task-description">Notiz</label> <label class="label" for="task-description">${t('tasks.descriptionLabel')}</label>
<textarea class="input" id="task-description" name="description" <textarea class="input" id="task-description" name="description"
rows="2" placeholder="Optionale Details…" rows="2" placeholder="${t('tasks.descriptionPlaceholder')}"
style="resize:vertical">${task?.description ?? ''}</textarea> style="resize:vertical">${task?.description ?? ''}</textarea>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group" style="margin-bottom:0"> <div class="form-group" style="margin-bottom:0">
<label class="label" for="task-priority">Priorität</label> <label class="label" for="task-priority">${t('tasks.priorityLabel')}</label>
<select class="input" id="task-priority" name="priority" style="min-height:44px"> <select class="input" id="task-priority" name="priority" style="min-height:44px">
${priorityOptions} ${priorityOptions}
</select> </select>
</div> </div>
<div class="form-group" style="margin-bottom:0"> <div class="form-group" style="margin-bottom:0">
<label class="label" for="task-category">Kategorie</label> <label class="label" for="task-category">${t('tasks.categoryLabel')}</label>
<select class="input" id="task-category" name="category" style="min-height:44px"> <select class="input" id="task-category" name="category" style="min-height:44px">
${categoryOptions} ${categoryOptions}
</select> </select>
@@ -279,30 +299,30 @@ function renderModalContent({ task = null, users = [] } = {}) {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-top:var(--space-4)"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-top:var(--space-4)">
<div class="form-group" style="margin-bottom:0"> <div class="form-group" style="margin-bottom:0">
<label class="label" for="task-due-date">Fälligkeit</label> <label class="label" for="task-due-date">${t('tasks.dueDateLabel')}</label>
<input class="input" type="date" id="task-due-date" name="due_date" <input class="input" type="date" id="task-due-date" name="due_date"
value="${task?.due_date ?? ''}"> value="${task?.due_date ?? ''}">
</div> </div>
<div class="form-group" style="margin-bottom:0"> <div class="form-group" style="margin-bottom:0">
<label class="label" for="task-due-time">Uhrzeit</label> <label class="label" for="task-due-time">${t('tasks.dueTimeLabel')}</label>
<input class="input" type="time" id="task-due-time" name="due_time" <input class="input" type="time" id="task-due-time" name="due_time"
value="${task?.due_time ?? ''}"> value="${task?.due_time ?? ''}">
</div> </div>
</div> </div>
<div class="form-group" style="margin-top:var(--space-4)"> <div class="form-group" style="margin-top:var(--space-4)">
<label class="label" for="task-assigned">Zugewiesen an</label> <label class="label" for="task-assigned">${t('tasks.assignedLabel')}</label>
<select class="input" id="task-assigned" name="assigned_to" style="min-height:44px"> <select class="input" id="task-assigned" name="assigned_to" style="min-height:44px">
<option value="">— Niemand —</option> <option value="">${t('tasks.assignedNobody')}</option>
${userOptions} ${userOptions}
</select> </select>
</div> </div>
${isEdit ? ` ${isEdit ? `
<div class="form-group"> <div class="form-group">
<label class="label" for="task-status">Status</label> <label class="label" for="task-status">${t('tasks.statusLabel')}</label>
<select class="input" id="task-status" name="status" style="min-height:44px"> <select class="input" id="task-status" name="status" style="min-height:44px">
${STATUSES.map((s) => ${STATUSES().map((s) =>
`<option value="${s.value}" ${task.status === s.value ? 'selected' : ''}>${s.label}</option>` `<option value="${s.value}" ${task.status === s.value ? 'selected' : ''}>${s.label}</option>`
).join('')} ).join('')}
</select> </select>
@@ -315,9 +335,9 @@ function renderModalContent({ task = null, users = [] } = {}) {
<div class="modal-panel__footer" style="padding:0;border:none;margin-top:var(--space-6)"> <div class="modal-panel__footer" style="padding:0;border:none;margin-top:var(--space-6)">
${isEdit ? ` ${isEdit ? `
<button type="button" class="btn btn--danger" data-action="delete-task" <button type="button" class="btn btn--danger" data-action="delete-task"
data-id="${task.id}">Löschen</button>` : ''} data-id="${task.id}">${t('common.delete')}</button>` : ''}
<button type="submit" class="btn btn--primary" id="task-submit-btn"> <button type="submit" class="btn btn--primary" id="task-submit-btn">
${isEdit ? 'Speichern' : 'Erstellen'} ${isEdit ? t('common.save') : t('common.create')}
</button> </button>
</div> </div>
</form>`; </form>`;
@@ -375,7 +395,7 @@ async function loadTaskForEdit(id) {
function openTaskModal({ task = null, users = [] } = {}, container) { function openTaskModal({ task = null, users = [] } = {}, container) {
const isEdit = !!task; const isEdit = !!task;
openSharedModal({ openSharedModal({
title: isEdit ? 'Aufgabe bearbeiten' : 'Neue Aufgabe', title: isEdit ? t('tasks.editTask') : t('tasks.newTask'),
content: renderModalContent({ task, users }), content: renderModalContent({ task, users }),
size: 'lg', size: 'lg',
onSave(panel) { onSave(panel) {
@@ -408,9 +428,9 @@ async function handleFormSubmit(e, container) {
errorEl.hidden = true; errorEl.hidden = true;
submitBtn.disabled = 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 rrule = getRRuleValues(document, 'task');
const body = { const body = {
@@ -429,10 +449,10 @@ async function handleFormSubmit(e, container) {
try { try {
if (taskId) { if (taskId) {
await api.put(`/tasks/${taskId}`, body); await api.put(`/tasks/${taskId}`, body);
window.oikos.showToast('Aufgabe gespeichert.', 'success'); window.oikos.showToast(t('tasks.savedToast'), 'success');
} else { } else {
await api.post('/tasks', body); await api.post('/tasks', body);
window.oikos.showToast('Aufgabe erstellt.', 'success'); window.oikos.showToast(t('tasks.createdToast'), 'success');
} }
btnSuccess(submitBtn, originalLabel); btnSuccess(submitBtn, originalLabel);
setTimeout(() => closeModal(), 700); setTimeout(() => closeModal(), 700);
@@ -447,11 +467,11 @@ async function handleFormSubmit(e, container) {
} }
async function handleDeleteTask(id, container) { async function handleDeleteTask(id, container) {
if (!confirm('Aufgabe und alle Teilaufgaben löschen?')) return; if (!confirm(t('tasks.deleteConfirm'))) return;
try { try {
await api.delete(`/tasks/${id}`); await api.delete(`/tasks/${id}`);
closeModal(); closeModal();
window.oikos.showToast('Aufgabe gelöscht.', 'default'); window.oikos.showToast(t('tasks.deletedToast'), 'default');
await loadTasks(container); await loadTasks(container);
} catch (err) { } catch (err) {
window.oikos.showToast(err.message, 'danger'); window.oikos.showToast(err.message, 'danger');
@@ -459,7 +479,7 @@ async function handleDeleteTask(id, container) {
} }
async function handleAddSubtask(parentId, container) { async function handleAddSubtask(parentId, container) {
const title = prompt('Teilaufgabe:'); const title = prompt(t('tasks.subtaskPrompt'));
if (!title?.trim()) return; if (!title?.trim()) return;
try { try {
await api.post('/tasks', { title: title.trim(), parent_task_id: parentId }); await api.post('/tasks', { title: title.trim(), parent_task_id: parentId });
@@ -473,10 +493,10 @@ async function handleAddSubtask(parentId, container) {
// Kanban-Ansicht // Kanban-Ansicht
// -------------------------------------------------------- // --------------------------------------------------------
const KANBAN_COLS = [ const KANBAN_COLS = () => [
{ status: 'open', label: 'Offen', colorVar: '--color-text-secondary' }, { status: 'open', label: t('tasks.kanbanOpen'), colorVar: '--color-text-secondary' },
{ status: 'in_progress', label: 'In Bearbeitung', colorVar: '--color-warning' }, { status: 'in_progress', label: t('tasks.kanbanInProgress'), colorVar: '--color-warning' },
{ status: 'done', label: 'Erledigt', colorVar: '--color-success' }, { status: 'done', label: t('tasks.kanbanDone'), colorVar: '--color-success' },
]; ];
function renderKanbanCard(task) { function renderKanbanCard(task) {
@@ -503,8 +523,9 @@ function renderKanban(container) {
const listEl = container.querySelector('#task-list'); const listEl = container.querySelector('#task-list');
if (!listEl) return; if (!listEl) return;
const cols = KANBAN_COLS();
const grouped = {}; 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) { for (const t of state.tasks) {
if (grouped[t.status]) grouped[t.status].push(t); if (grouped[t.status]) grouped[t.status].push(t);
else grouped['open'].push(t); else grouped['open'].push(t);
@@ -512,7 +533,7 @@ function renderKanban(container) {
listEl.innerHTML = ` listEl.innerHTML = `
<div class="kanban-board"> <div class="kanban-board">
${KANBAN_COLS.map((col) => ` ${cols.map((col) => `
<div class="kanban-col" data-status="${col.status}"> <div class="kanban-col" data-status="${col.status}">
<div class="kanban-col__header"> <div class="kanban-col__header">
<span class="kanban-col__title" style="color:${col.colorVar.startsWith('--') ? `var(${col.colorVar})` : col.colorVar}"> <span class="kanban-col__title" style="color:${col.colorVar.startsWith('--') ? `var(${col.colorVar})` : col.colorVar}">
@@ -606,7 +627,7 @@ function wireKanbanDrag(container) {
const task = await loadTaskForEdit(card.dataset.taskId); const task = await loadTaskForEdit(card.dataset.taskId);
openTaskModal({ task, users: state.users }, container); openTaskModal({ task, users: state.users }, container);
} catch (err) { } 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; if (!bar) return;
const chips = []; const chips = [];
const statusLabels = STATUS_LABELS();
const priorityLabels = PRIORITY_LABELS();
if (state.filters.status) { if (state.filters.status) {
chips.push(`<span class="filter-chip filter-chip--active" data-filter="status"> chips.push(`<span class="filter-chip filter-chip--active" data-filter="status">
${STATUS_LABELS[state.filters.status]} ${statusLabels[state.filters.status]}
<span class="filter-chip__remove" aria-hidden="true">×</span> <span class="filter-chip__remove" aria-hidden="true">×</span>
</span>`); </span>`);
} }
if (state.filters.priority) { if (state.filters.priority) {
chips.push(`<span class="filter-chip filter-chip--active" data-filter="priority"> chips.push(`<span class="filter-chip filter-chip--active" data-filter="priority">
${PRIORITY_LABELS[state.filters.priority]} ${priorityLabels[state.filters.priority]}
<span class="filter-chip__remove" aria-hidden="true">×</span> <span class="filter-chip__remove" aria-hidden="true">×</span>
</span>`); </span>`);
} }
@@ -657,12 +680,12 @@ function renderFilters(container) {
// Inaktive Filter-Chips (zum Aktivieren) // Inaktive Filter-Chips (zum Aktivieren)
if (!state.filters.status) { if (!state.filters.status) {
STATUSES.forEach((s) => { STATUSES().forEach((s) => {
chips.push(`<span class="filter-chip" data-filter="status" data-value="${s.value}">${s.label}</span>`); chips.push(`<span class="filter-chip" data-filter="status" data-value="${s.value}">${s.label}</span>`);
}); });
} }
if (!state.filters.priority) { if (!state.filters.priority) {
PRIORITIES.forEach((p) => { PRIORITIES().forEach((p) => {
chips.push(`<span class="filter-chip" data-filter="priority" data-value="${p.value}">${p.label}</span>`); chips.push(`<span class="filter-chip" data-filter="priority" data-value="${p.value}">${p.label}</span>`);
}); });
} }
@@ -802,7 +825,7 @@ function wireSwipeGestures(container) {
const task = await loadTaskForEdit(taskId); const task = await loadTaskForEdit(taskId);
openTaskModal({ task, users: state.users }, container); openTaskModal({ task, users: state.users }, container);
} catch (err) { } catch (err) {
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger'); window.oikos.showToast(t('tasks.loadError'), 'danger');
} }
} else { } else {
@@ -912,7 +935,7 @@ function wireTaskList(container) {
const task = await loadTaskForEdit(id); const task = await loadTaskForEdit(id);
openTaskModal({ task, users: state.users }, container); openTaskModal({ task, users: state.users }, container);
} catch (err) { } 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 = ` container.innerHTML = `
<div class="tasks-page"> <div class="tasks-page">
<div class="tasks-toolbar"> <div class="tasks-toolbar">
<h1 class="tasks-toolbar__title">Aufgaben</h1> <h1 class="tasks-toolbar__title">${t('tasks.title')}</h1>
<div class="tasks-toolbar__actions"> <div class="tasks-toolbar__actions">
<div class="group-toggle" id="view-toggle"> <div class="group-toggle" id="view-toggle">
<button class="group-toggle__btn group-toggle__btn--active" data-view="list" <button class="group-toggle__btn group-toggle__btn--active" data-view="list"
@@ -944,11 +967,11 @@ export async function render(container, { user }) {
</button> </button>
</div> </div>
<div class="group-toggle" id="group-mode-toggle"> <div class="group-toggle" id="group-mode-toggle">
<button class="group-toggle__btn group-toggle__btn--active" data-mode="category">Kategorie</button> <button class="group-toggle__btn group-toggle__btn--active" data-mode="category">${t('tasks.categoryLabel')}</button>
<button class="group-toggle__btn" data-mode="due">Fälligkeit</button> <button class="group-toggle__btn" data-mode="due">${t('tasks.dueDateLabel')}</button>
</div> </div>
<button class="btn btn--primary" id="btn-new-task" style="gap:var(--space-1)"> <button class="btn btn--primary" id="btn-new-task" style="gap:var(--space-1)">
<i data-lucide="plus" style="width:18px;height:18px" aria-hidden="true"></i> Neu <i data-lucide="plus" style="width:18px;height:18px" aria-hidden="true"></i> ${t('tasks.newTask')}
</button> </button>
</div> </div>
</div> </div>
@@ -963,7 +986,7 @@ export async function render(container, { user }) {
<div class="skeleton skeleton-line skeleton-line--short" style="height:12px"></div> <div class="skeleton skeleton-line skeleton-line--short" style="height:12px"></div>
</div>`).join('')} </div>`).join('')}
</div> </div>
<button class="page-fab" id="fab-new-task" aria-label="Neue Aufgabe"> <button class="page-fab" id="fab-new-task" aria-label="${t('tasks.newTask')}">
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i> <i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
</button> </button>
</div> </div>
@@ -981,7 +1004,7 @@ export async function render(container, { user }) {
state.users = metaData.users ?? []; state.users = metaData.users ?? [];
} catch (err) { } catch (err) {
console.error('[Tasks] Ladefehler:', err.message); 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.tasks = [];
state.users = []; state.users = [];
} }