/**
* Modul: Aufgaben (Tasks)
* Zweck: Listenansicht mit Filtern, Gruppierung, CRUD-Modal, Subtask-Verwaltung
* Abhängigkeiten: /api.js
*/
import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError, promptModal, confirmModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate } from '/i18n.js';
import { esc } from '/utils/html.js';
// --------------------------------------------------------
// Konstanten
// --------------------------------------------------------
const PRIORITIES = () => [
{ value: 'urgent', label: t('tasks.priorityUrgent'), color: 'var(--color-priority-urgent)' },
{ value: 'high', label: t('tasks.priorityHigh'), color: 'var(--color-priority-high)' },
{ value: 'medium', label: t('tasks.priorityMedium'), color: 'var(--color-priority-medium)' },
{ value: 'low', label: t('tasks.priorityLow'), color: 'var(--color-priority-low)' },
{ value: 'none', label: t('tasks.priorityNone'), color: 'var(--color-priority-none)' },
];
const STATUSES = () => [
{ value: 'open', label: t('tasks.statusOpen') },
{ value: 'in_progress', label: t('tasks.statusInProgress') },
{ value: 'done', label: t('tasks.statusDone') },
];
const CATEGORIES = [
'Haushalt', 'Schule', 'Einkauf', 'Reparatur',
'Gesundheit', 'Finanzen', 'Freizeit', 'Sonstiges',
];
const CATEGORY_LABELS = () => ({
'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
// --------------------------------------------------------
function initials(name = '') {
return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase();
}
function formatDueDate(dateStr) {
if (!dateStr) return null;
const due = new Date(dateStr);
const now = new Date();
now.setHours(0, 0, 0, 0);
const diffDays = Math.round((due - now) / 86400000);
if (diffDays < 0) return { label: t('tasks.overdueDay', { count: Math.abs(diffDays) }), cls: 'due-date--overdue' };
if (diffDays === 0) return { label: t('tasks.dueToday'), cls: 'due-date--today' };
if (diffDays === 1) return { label: t('tasks.dueTomorrow'), cls: '' };
return { label: formatDate(due), cls: '' };
}
function groupBy(tasks, mode) {
const groups = {};
if (mode === 'category') {
for (const t of tasks) {
const key = t.category || 'Sonstiges';
(groups[key] = groups[key] || []).push(t);
}
return Object.entries(groups).sort(([a], [b]) => a.localeCompare(b, 'de'));
}
// mode === 'due'
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;
if (!task.due_date) key = groupNoDate;
else {
const diff = Math.round((new Date(task.due_date) - new Date().setHours(0,0,0,0)) / 86400000);
if (diff < 0) key = groupOverdue;
else if (diff === 0) key = groupToday;
else if (diff <= 3) key = groupThisWeek;
else if (diff <= 7) key = groupNextWeek;
else key = groupLater;
}
(groups[key] = groups[key] || []).push(task);
}
const order = [groupOverdue, groupToday, groupThisWeek, groupNextWeek, groupLater, groupNoDate];
return order.filter((k) => groups[k]).map((k) => [k, groups[k]]);
}
// --------------------------------------------------------
// Render-Bausteine
// --------------------------------------------------------
function renderPriorityBadge(priority) {
if (priority === 'none') return '';
return `
${PRIORITY_LABELS()[priority] ?? priority}
`;
}
function renderDueDate(dateStr) {
const d = formatDueDate(dateStr);
if (!d) return '';
return `
${d.label}
`;
}
function renderSwipeRow(task, innerHtml) {
const isDone = task.status === 'done';
return `
${isDone ? t('tasks.swipeOpen') : t('tasks.swipeDone')}
${t('tasks.swipeEdit')}
${innerHtml}
`;
}
function renderTaskCard(task, opts = {}) {
const { expandedSubtasks = false } = opts;
const isDone = task.status === 'done';
const progress = task.subtask_total > 0
? Math.round((task.subtask_done / task.subtask_total) * 100)
: null;
const subtasksHtml = task.subtasks?.length
? task.subtasks.map((s) => `
${s.status === 'done' ? ' ' : ''}
${esc(s.title)}
`).join('')
: '';
return `
${esc(task.title)}
${renderPriorityBadge(task.priority)}
${renderDueDate(task.due_date)}
${task.is_recurring ? ` ` : ''}
${task.category !== 'Sonstiges' ? `${CATEGORY_LABELS()[task.category] ?? task.category} ` : ''}
${task.assigned_color ? `
${esc(initials(task.assigned_name ?? ''))}
` : ''}
${progress !== null ? `
${task.subtask_done}/${task.subtask_total}
` : ''}
${task.subtasks !== undefined ? `
${subtasksHtml}
${t('tasks.subtaskAdd')}
` : ''}
`;
}
function renderTaskGroups(tasks, groupMode) {
if (!tasks.length) {
return `
${t('tasks.emptyTitle')}
${t('tasks.emptyDescription')}
`;
}
const groups = groupBy(tasks, groupMode);
const catLabelsMap = CATEGORY_LABELS();
return groups.map(([name, groupTasks]) => `
${groupTasks.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')}
`).join('');
}
// --------------------------------------------------------
// Task-Modal (Erstellen / Bearbeiten)
// --------------------------------------------------------
function renderModalContent({ task = null, users = [] } = {}) {
const isEdit = !!task;
const userOptions = users.map((u) =>
`${esc(u.display_name)} `
).join('');
const catLabels = CATEGORY_LABELS();
const categoryOptions = CATEGORIES.map((c) =>
`${catLabels[c] ?? c} `
).join('');
const priorityOptions = PRIORITIES().map((p) =>
`${p.label} `
).join('');
return `
`;
}
// --------------------------------------------------------
// Seiten-State
// --------------------------------------------------------
let state = {
tasks: [],
users: [],
filters: { status: '', priority: '', assigned_to: '' },
groupMode: 'category', // 'category' | 'due'
viewMode: 'list', // 'list' | 'kanban' (resolved at render time)
expandedTasks: new Set(),
dragTaskId: null,
};
// --------------------------------------------------------
// API-Aktionen
// --------------------------------------------------------
async function loadTasks(container) {
const params = new URLSearchParams();
if (state.filters.status) params.set('status', state.filters.status);
if (state.filters.priority) params.set('priority', state.filters.priority);
if (state.filters.assigned_to) params.set('assigned_to', state.filters.assigned_to);
const query = params.toString() ? `?${params}` : '';
const data = await api.get(`/tasks${query}`);
state.tasks = data.data ?? [];
renderTaskList(container);
}
async function toggleTaskStatus(id, currentStatus) {
const next = currentStatus === 'done' ? 'open' : 'done';
await api.patch(`/tasks/${id}/status`, { status: next });
}
async function toggleSubtaskStatus(id, currentStatus) {
const next = currentStatus === 'done' ? 'open' : 'done';
await api.patch(`/tasks/${id}/status`, { status: next });
}
async function loadTaskForEdit(id) {
const data = await api.get(`/tasks/${id}`);
return data.data;
}
// --------------------------------------------------------
// Modal-Verwaltung (delegiert an Shared Modal-System)
// --------------------------------------------------------
function openTaskModal({ task = null, users = [] } = {}, container) {
const isEdit = !!task;
openSharedModal({
title: isEdit ? t('tasks.editTask') : t('tasks.newTask'),
content: renderModalContent({ task, users }),
size: 'lg',
onSave(panel) {
// RRULE-Events binden
bindRRuleEvents(document, 'task');
// Blur-Validierung für required-Felder aktivieren
wireBlurValidation(panel);
// Form-Events
panel.querySelector('#task-form')
?.addEventListener('submit', (e) => handleFormSubmit(e, container));
panel.querySelector('[data-action="delete-task"]')
?.addEventListener('click', (e) => handleDeleteTask(e.currentTarget.dataset.id, container));
},
});
}
// --------------------------------------------------------
// Formular-Handler
// --------------------------------------------------------
async function handleFormSubmit(e, container) {
e.preventDefault();
const form = e.target;
const errorEl = document.getElementById('task-form-error');
const submitBtn = document.getElementById('task-submit-btn');
const taskId = document.getElementById('task-id').value;
errorEl.hidden = true;
submitBtn.disabled = true;
submitBtn.textContent = t('common.saving');
const originalLabel = taskId ? t('common.save') : t('common.create');
const rrule = getRRuleValues(document, 'task');
const body = {
title: form.title.value.trim(),
description: form.description.value.trim() || null,
priority: form.priority.value,
category: form.category.value,
due_date: form.due_date?.value || null,
due_time: form.due_time?.value || null,
assigned_to: form.assigned_to.value ? Number(form.assigned_to.value) : null,
is_recurring: rrule.is_recurring ? 1 : 0,
recurrence_rule: rrule.recurrence_rule,
};
if (form.status) body.status = form.status.value;
try {
if (taskId) {
await api.put(`/tasks/${taskId}`, body);
window.oikos.showToast(t('tasks.savedToast'), 'success');
} else {
await api.post('/tasks', body);
window.oikos.showToast(t('tasks.createdToast'), 'success');
}
btnSuccess(submitBtn, originalLabel);
setTimeout(() => closeModal(), 700);
await loadTasks(container);
} catch (err) {
errorEl.textContent = err.message;
errorEl.hidden = false;
submitBtn.disabled = false;
submitBtn.textContent = originalLabel;
btnError(submitBtn);
}
}
async function handleDeleteTask(id, container) {
if (!await confirmModal(t('tasks.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/tasks/${id}`);
closeModal();
window.oikos.showToast(t('tasks.deletedToast'), 'default');
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
}
}
async function handleAddSubtask(parentId, container) {
const title = await promptModal(t('tasks.subtaskPrompt'));
if (!title) return;
try {
await api.post('/tasks', { title, parent_task_id: parentId });
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
}
}
// --------------------------------------------------------
// Kanban-Ansicht
// --------------------------------------------------------
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) {
const due = formatDueDate(task.due_date);
return `
${esc(task.title)}
${renderPriorityBadge(task.priority)}
${due ? ` ${due.label} ` : ''}
${task.assigned_color ? `
` : ''}
`;
}
function renderKanban(container) {
const listEl = container.querySelector('#task-list');
if (!listEl) return;
const cols = KANBAN_COLS();
const grouped = {};
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);
}
listEl.innerHTML = `
${cols.map((col) => `
${grouped[col.status].map((t) => renderKanbanCard(t)).join('')}
`).join('')}
`;
if (window.lucide) window.lucide.createIcons();
wireKanbanDrag(container);
updateOverdueBadge();
}
function wireKanbanDrag(container) {
const board = container.querySelector('.kanban-board');
if (!board) return;
board.addEventListener('dragstart', (e) => {
const card = e.target.closest('.kanban-card[data-task-id]');
if (!card) return;
state.dragTaskId = card.dataset.taskId;
card.classList.add('kanban-card--dragging');
e.dataTransfer.effectAllowed = 'move';
});
board.addEventListener('dragend', (e) => {
const card = e.target.closest('.kanban-card[data-task-id]');
if (card) card.classList.remove('kanban-card--dragging');
board.querySelectorAll('.kanban-drop-placeholder').forEach((el) => el.hidden = true);
board.querySelectorAll('.kanban-col__body--over').forEach((el) =>
el.classList.remove('kanban-col__body--over')
);
state.dragTaskId = null;
});
board.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const zone = e.target.closest('[data-drop-zone]');
if (!zone) return;
board.querySelectorAll('.kanban-col__body--over').forEach((el) =>
el.classList.remove('kanban-col__body--over')
);
zone.classList.add('kanban-col__body--over');
});
board.addEventListener('dragleave', (e) => {
const zone = e.target.closest('[data-drop-zone]');
if (zone && !zone.contains(e.relatedTarget)) {
zone.classList.remove('kanban-col__body--over');
}
});
board.addEventListener('drop', async (e) => {
e.preventDefault();
const zone = e.target.closest('[data-drop-zone]');
if (!zone || !state.dragTaskId) return;
zone.classList.remove('kanban-col__body--over');
const newStatus = zone.dataset.dropZone;
const taskId = state.dragTaskId;
const task = state.tasks.find((t) => String(t.id) === String(taskId));
if (!task || task.status === newStatus) return;
// Optimistisches Update
task.status = newStatus;
renderKanban(container);
try {
await api.patch(`/tasks/${taskId}/status`, { status: newStatus });
await loadTasks(container); // sync
} catch (err) {
window.oikos.showToast(err.message, 'danger');
await loadTasks(container);
}
});
// Klick auf Kanban-Card öffnet Edit-Modal
board.addEventListener('click', async (e) => {
if (e.target.closest('[draggable]')) {
const card = e.target.closest('.kanban-card[data-task-id]');
if (!card) return;
try {
const task = await loadTaskForEdit(card.dataset.taskId);
openTaskModal({ task, users: state.users }, container);
} catch (err) {
window.oikos.showToast(t('tasks.loadError'), 'danger');
}
}
});
}
// --------------------------------------------------------
// Partielle DOM-Updates
// --------------------------------------------------------
function renderTaskList(container) {
if (state.viewMode === 'kanban') {
renderKanban(container);
return;
}
const listEl = container.querySelector('#task-list');
if (!listEl) return;
listEl.innerHTML = renderTaskGroups(state.tasks, state.groupMode);
if (window.lucide) window.lucide.createIcons();
stagger(listEl.querySelectorAll('.swipe-row, .kanban-card'));
updateOverdueBadge();
wireSwipeGestures(container);
}
function renderFilters(container) {
const bar = container.querySelector('#filter-bar');
if (!bar) return;
const chips = [];
const statusLabels = STATUS_LABELS();
const priorityLabels = PRIORITY_LABELS();
if (state.filters.status) {
chips.push(`
${statusLabels[state.filters.status]}
×
`);
}
if (state.filters.priority) {
chips.push(`
${priorityLabels[state.filters.priority]}
×
`);
}
if (state.filters.assigned_to) {
const u = state.users.find((u) => u.id === Number(state.filters.assigned_to));
chips.push(`
${u?.display_name ?? 'Person'}
×
`);
}
// Inaktive Filter-Chips (zum Aktivieren)
if (!state.filters.status) {
STATUSES().forEach((s) => {
chips.push(`${s.label} `);
});
}
if (!state.filters.priority) {
PRIORITIES().forEach((p) => {
chips.push(`${p.label} `);
});
}
if (!state.filters.assigned_to && state.users.length > 1) {
state.users.forEach((u) => {
chips.push(`${u.display_name} `);
});
}
bar.innerHTML = chips.join('');
wireFilterChips(container);
}
function updateOverdueBadge() {
const overdue = state.tasks.filter((t) => {
if (!t.due_date || t.status === 'done') return false;
return new Date(t.due_date) < new Date().setHours(0, 0, 0, 0);
}).length;
document.querySelectorAll('[data-route="/tasks"] .nav-badge').forEach((el) => el.remove());
if (overdue > 0) {
document.querySelectorAll('[data-route="/tasks"]').forEach((el) => {
el.insertAdjacentHTML('beforeend', `${overdue} `);
});
}
}
// --------------------------------------------------------
// Swipe-Gesten (Mobil: links = erledigt, rechts = bearbeiten)
// --------------------------------------------------------
const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
const SWIPE_MAX_VERT = 12; // px - vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch)
const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
function wireSwipeGestures(container) {
const listEl = container.querySelector('#task-list');
if (!listEl) return;
listEl.querySelectorAll('.swipe-row').forEach((row) => {
let startX = 0, startY = 0;
let dx = 0;
let locked = false; // false = unentschieden, 'swipe' | 'scroll'
let thresholdHit = false; // Haptic-Feedback am Threshold nur einmal
const card = row.querySelector('.task-card');
if (!card) return;
function resetCard(animate = true) {
card.style.transition = animate ? 'transform 0.25s ease' : '';
card.style.transform = '';
row.classList.remove('swipe-row--swiping');
// Reveal-Panels zurücksetzen
row.querySelector('.swipe-reveal--done').style.opacity = '0';
row.querySelector('.swipe-reveal--edit').style.opacity = '0';
}
row.addEventListener('touchstart', (e) => {
// Geste ignorieren wenn Modal offen
if (document.getElementById('shared-modal-overlay')) return;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
dx = 0;
locked = false;
thresholdHit = false;
card.style.transition = '';
}, { passive: true });
row.addEventListener('touchmove', (e) => {
if (locked === 'scroll') return;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
dx = currentX - startX;
const dy = Math.abs(currentY - startY);
// Scroll-Richtung früh erkennen
if (locked === false) {
if (dy > SWIPE_MAX_VERT && Math.abs(dx) < dy) {
locked = 'scroll';
resetCard(false);
return;
}
if (Math.abs(dx) > SWIPE_MAX_VERT) {
locked = 'swipe';
}
}
if (locked !== 'swipe') return;
// Vertikalen Scroll verhindern sobald Swipe erkannt
if (dy < SWIPE_LOCK_VERT) e.preventDefault();
// Karte verschieben (gedämpft nach THRESHOLD)
const dampened = dx > 0
? Math.min(dx, SWIPE_THRESHOLD + (dx - SWIPE_THRESHOLD) * 0.2)
: Math.max(dx, -(SWIPE_THRESHOLD + (-dx - SWIPE_THRESHOLD) * 0.2));
card.style.transform = `translateX(${dampened}px)`;
row.classList.add('swipe-row--swiping');
// Reveal-Panels einblenden (0 → 1 über Threshold)
const progress = Math.min(Math.abs(dx) / SWIPE_THRESHOLD, 1);
if (dx < 0) {
row.querySelector('.swipe-reveal--done').style.opacity = String(progress);
row.querySelector('.swipe-reveal--edit').style.opacity = '0';
} else {
row.querySelector('.swipe-reveal--edit').style.opacity = String(progress);
row.querySelector('.swipe-reveal--done').style.opacity = '0';
}
// Haptic-Feedback beim Erreichen des Schwellwerts
if (!thresholdHit && Math.abs(dx) >= SWIPE_THRESHOLD) {
thresholdHit = true;
vibrate(15);
}
}, { passive: false });
row.addEventListener('touchend', async () => {
if (locked !== 'swipe') { resetCard(false); return; }
const taskId = row.dataset.swipeId;
const status = row.dataset.swipeStatus;
if (dx < -SWIPE_THRESHOLD) {
// Swipe links → Status-Toggle (offen ↔ erledigt)
card.style.transition = 'transform 0.2s ease';
card.style.transform = 'translateX(-110%)';
vibrate(40);
setTimeout(async () => {
resetCard(false);
try {
await toggleTaskStatus(taskId, status);
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
await loadTasks(container);
}
}, 200);
} else if (dx > SWIPE_THRESHOLD) {
// Swipe rechts → Bearbeiten-Modal
resetCard(true);
vibrate(20);
try {
const task = await loadTaskForEdit(taskId);
openTaskModal({ task, users: state.users }, container);
} catch (err) {
window.oikos.showToast(t('tasks.loadError'), 'danger');
}
} else {
resetCard(true);
}
}, { passive: true });
});
}
// --------------------------------------------------------
// Event-Verdrahtung
// --------------------------------------------------------
function wireFilterChips(container) {
container.querySelectorAll('[data-filter]').forEach((chip) => {
chip.addEventListener('click', async () => {
const filter = chip.dataset.filter;
if (chip.classList.contains('filter-chip--active')) {
state.filters[filter] = '';
} else {
state.filters[filter] = chip.dataset.value;
}
renderFilters(container);
await loadTasks(container);
});
});
}
function wireViewToggle(container) {
const toggle = container.querySelector('#view-toggle');
if (!toggle) return;
toggle.querySelectorAll('[data-view]').forEach((btn) => {
btn.addEventListener('click', () => {
state.viewMode = btn.dataset.view;
localStorage.setItem('oikos-tasks-view', state.viewMode);
toggle.querySelectorAll('[data-view]').forEach((b) =>
b.classList.toggle('group-toggle__btn--active', b.dataset.view === state.viewMode)
);
// Gruppierungs-Toggle nur in Listenansicht sinnvoll
const groupToggle = container.querySelector('#group-mode-toggle');
if (groupToggle) groupToggle.style.display = state.viewMode === 'list' ? '' : 'none';
renderTaskList(container);
});
});
}
function wireGroupToggle(container) {
const toggle = container.querySelector('#group-mode-toggle');
if (!toggle) return;
toggle.querySelectorAll('.group-toggle__btn').forEach((btn) => {
btn.addEventListener('click', () => {
state.groupMode = btn.dataset.mode;
toggle.querySelectorAll('.group-toggle__btn').forEach((b) =>
b.classList.toggle('group-toggle__btn--active', b.dataset.mode === state.groupMode)
);
renderTaskList(container);
});
});
}
function wireNewTaskBtn(container) {
const handler = () => {
openTaskModal({ users: state.users }, container);
};
container.querySelector('#btn-new-task')?.addEventListener('click', handler);
container.querySelector('#fab-new-task')?.addEventListener('click', handler);
}
function wireTaskList(container) {
const listEl = container.querySelector('#task-list');
if (!listEl) return;
listEl.addEventListener('click', async (e) => {
const target = e.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
const id = target.dataset.id;
if (action === 'toggle-status') {
const status = target.dataset.status;
target.classList.toggle('task-status-btn--done', status !== 'done');
target.closest('.task-card')?.classList.toggle('task-card--done', status !== 'done');
try {
await toggleTaskStatus(id, status);
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
await loadTasks(container);
}
}
if (action === 'toggle-subtasks') {
const subtaskList = document.getElementById(`subtasks-${id}`);
if (subtaskList) subtaskList.classList.toggle('subtask-list--visible');
}
if (action === 'toggle-subtask') {
try {
await toggleSubtaskStatus(id, target.dataset.status);
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
}
}
if (action === 'edit-task' || action === 'open-task') {
try {
const task = await loadTaskForEdit(id);
openTaskModal({ task, users: state.users }, container);
} catch (err) {
window.oikos.showToast(t('tasks.loadError'), 'danger');
}
}
if (action === 'add-subtask') {
await handleAddSubtask(target.dataset.parent, container);
}
});
}
// --------------------------------------------------------
// Haupt-Render
// --------------------------------------------------------
export async function render(container, { user }) {
// View-Mode: URL-Parameter > localStorage > Default 'list'
const urlView = new URLSearchParams(window.location.search).get('view');
const savedView = localStorage.getItem('oikos-tasks-view');
state.viewMode = (urlView === 'kanban' || urlView === 'list') ? urlView
: (savedView === 'kanban' || savedView === 'list') ? savedView
: 'list';
const isKanban = state.viewMode === 'kanban';
// Initiales Skeleton (all values are from i18n keys or hardcoded constants, no user data)
container.innerHTML = `
${[1,2,3].map(() => `
`).join('')}
`;
if (window.lucide) window.lucide.createIcons();
// Daten laden
try {
const [tasksData, metaData] = await Promise.all([
api.get('/tasks'),
api.get('/tasks/meta/options'),
]);
state.tasks = tasksData.data ?? [];
state.users = metaData.users ?? [];
} catch (err) {
console.error('[Tasks] Ladefehler:', err.message);
window.oikos.showToast(t('tasks.loadError'), 'danger');
state.tasks = [];
state.users = [];
}
// UI verdrahten
wireViewToggle(container);
wireGroupToggle(container);
wireNewTaskBtn(container);
wireTaskList(container);
renderFilters(container);
renderTaskList(container);
}