import * as api from '../api.js'; import { showModal, closeModal } from '../components/modal.js'; const el = () => document.getElementById('page-tasks'); let currentFilter = null; let selectedTaskId = null; export function initTasks() { el().innerHTML = `

Tasks

`; renderFilters(); document.getElementById('btn-new-task').addEventListener('click', () => showNewTaskModal()); loadTasks(); } function renderFilters() { const statuses = [null, 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; const labels = ['All', 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; const container = document.getElementById('task-filters'); container.innerHTML = statuses.map((s, i) => ` `).join(''); container.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => { currentFilter = btn.dataset.status || null; renderFilters(); loadTasks(); }); }); } async function loadTasks() { try { const tasks = await api.tasks.list(currentFilter); renderTaskList(tasks); } catch (e) { document.getElementById('task-list').innerHTML = `
Failed to load tasks
`; } } function renderTaskList(tasks) { const container = document.getElementById('task-list'); document.getElementById('task-detail').classList.add('hidden'); container.classList.remove('hidden'); if (!tasks.length) { container.innerHTML = `
No tasks found
`; return; } container.innerHTML = tasks.map(t => { const subCount = t.subTasks ? t.subTasks.length : 0; return `
${t.status} ${esc(t.title)} ${subCount > 0 ? `${subCount} subtask${subCount !== 1 ? 's' : ''}` : ''}
${t.category ? esc(t.category) + ' · ' : ''}${formatDate(t.createdAt)}
`; }).join(''); container.querySelectorAll('.task-item').forEach(item => { item.addEventListener('click', () => showTaskDetail(parseInt(item.dataset.id))); }); } async function buildBreadcrumbs(task) { const trail = [{ id: task.id, title: task.title }]; let current = task; while (current.parentTaskId) { try { current = await api.tasks.get(current.parentTaskId); trail.unshift({ id: current.id, title: current.title }); } catch { break; } } return trail; } async function showTaskDetail(id) { selectedTaskId = id; try { const task = await api.tasks.get(id); const container = document.getElementById('task-detail'); document.getElementById('task-list').classList.add('hidden'); container.classList.remove('hidden'); // Build breadcrumb trail const breadcrumbs = await buildBreadcrumbs(task); const breadcrumbHtml = breadcrumbs.length > 1 ? `` : ''; container.innerHTML = ` ${breadcrumbHtml}
${esc(task.title)}
${task.status}
${task.description ? `

${esc(task.description)}

` : ''}
Category
${esc(task.category) || 'None'}
Created
${formatDateTime(task.createdAt)}
Started
${task.startedAt ? formatDateTime(task.startedAt) : 'Not started'}
Completed
${task.completedAt ? formatDateTime(task.completedAt) : '-'}
Subtasks
${task.status !== 'Completed' && task.status !== 'Abandoned' ? `` : ''}
Notes
${task.contextEvents && task.contextEvents.length ? `
Linked Context Events
${task.contextEvents.slice(0, 50).map(e => ` `).join('')}
AppTitleTime
${esc(e.appName)} ${esc(e.windowTitle)} ${formatTime(e.timestamp)}
` : ''}
`; // Render action buttons based on status renderActions(task); // Render subtasks renderSubTasks(task.subTasks || []); // Render notes renderNotes(task.notes || []); // Breadcrumb navigation container.querySelectorAll('.breadcrumb-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); showTaskDetail(parseInt(link.dataset.id)); }); }); // Back button document.getElementById('btn-back-tasks').addEventListener('click', () => { if (task.parentTaskId) { showTaskDetail(task.parentTaskId); } else { container.classList.add('hidden'); document.getElementById('task-list').classList.remove('hidden'); loadTasks(); } }); // Add subtask button const addSubBtn = document.getElementById('btn-add-subtask'); if (addSubBtn) { addSubBtn.addEventListener('click', () => showNewTaskModal(task.id)); } // Add note const noteInput = document.getElementById('note-input'); document.getElementById('btn-add-note').addEventListener('click', () => addNote(task.id, noteInput)); noteInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addNote(task.id, noteInput); }); } catch (e) { alert('Failed to load task: ' + e.message); } } function renderSubTasks(subTasks) { const container = document.getElementById('task-subtasks'); if (!subTasks.length) { container.innerHTML = `
No subtasks
`; return; } container.innerHTML = subTasks.map(st => { const subCount = st.subTasks ? st.subTasks.length : 0; const canStart = st.status === 'Pending' || st.status === 'Paused'; const canComplete = st.status === 'Active' || st.status === 'Paused'; return `
${st.status} ${esc(st.title)} ${subCount > 0 ? `${subCount}` : ''}
${canStart ? `` : ''} ${canComplete ? `` : ''}
`; }).join(''); // Navigate to subtask detail container.querySelectorAll('.subtask-item-title').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); showTaskDetail(parseInt(link.dataset.id)); }); }); // Inline subtask actions container.querySelectorAll('.subtask-action').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const action = btn.dataset.action; const stId = parseInt(btn.dataset.id); try { if (action === 'start') await api.tasks.start(stId); else if (action === 'complete') await api.tasks.complete(stId); showTaskDetail(selectedTaskId); } catch (err) { alert(err.message); } }); }); } function renderActions(task) { const container = document.getElementById('task-actions'); const actions = []; switch (task.status) { case 'Pending': actions.push({ label: 'Start', cls: 'btn-success', action: () => api.tasks.start(task.id) }); actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); break; case 'Active': actions.push({ label: 'Pause', cls: 'btn-warning', action: () => api.tasks.pause(task.id) }); actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); break; case 'Paused': actions.push({ label: 'Resume', cls: 'btn-success', action: () => api.tasks.resume(task.id) }); actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); break; } container.innerHTML = ''; actions.forEach(({ label, cls, action }) => { const btn = document.createElement('button'); btn.className = `btn btn-sm ${cls}`; btn.textContent = label; btn.addEventListener('click', async () => { try { await action(); showTaskDetail(task.id); } catch (e) { alert(e.message); } }); container.appendChild(btn); }); } function renderNotes(notes) { const container = document.getElementById('task-notes'); if (!notes.length) { container.innerHTML = `
No notes yet
`; return; } container.innerHTML = notes.map(n => `
${n.type} ${formatDateTime(n.createdAt)}
${esc(n.content)}
`).join(''); } async function addNote(taskId, input) { const content = input.value.trim(); if (!content) return; try { await api.notes.create(taskId, { content, type: 'General' }); input.value = ''; showTaskDetail(taskId); } catch (e) { alert(e.message); } } function showNewTaskModal(parentTaskId = null) { const title = parentTaskId ? 'New Subtask' : 'New Task'; showModal(title, `
`, [ { label: 'Cancel', onClick: () => {} }, { label: 'Create', cls: 'btn-primary', onClick: async (modal) => { const taskTitle = modal.querySelector('#new-task-title').value.trim(); if (!taskTitle) { alert('Title is required'); throw new Error('cancel'); } const description = modal.querySelector('#new-task-desc').value.trim() || null; const category = modal.querySelector('#new-task-cat').value.trim() || null; const body = { title: taskTitle, description, category }; if (parentTaskId) body.parentTaskId = parentTaskId; await api.tasks.create(body); if (parentTaskId) { showTaskDetail(parentTaskId); } else { loadTasks(); } }, }, ]); setTimeout(() => document.getElementById('new-task-title')?.focus(), 100); } function formatDate(iso) { return new Date(iso).toLocaleDateString(); } function formatDateTime(iso) { const d = new Date(iso); return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function formatTime(iso) { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function esc(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }