/** * GoingsOn - Tasks Kanban Module * Board view with drag-and-drop status changes */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; const COLUMNS = [ { status: 'Pending', label: 'Pending' }, { status: 'Started', label: 'Started' }, { status: 'Completed', label: 'Completed' }, ]; // ============ Rendering ============ function render(tasks) { const board = document.getElementById('task-kanban-board'); if (!board) return; // Group tasks by status const grouped = { Pending: [], Started: [], Completed: [] }; for (const task of tasks) { const status = task.status || 'Pending'; if (grouped[status]) { grouped[status].push(task); } } board.innerHTML = COLUMNS.map(col => { const colTasks = grouped[col.status] || []; return `
${col.label} ${colTasks.length}
${colTasks.length === 0 ? '
No tasks
' : colTasks.map(t => renderCard(t)).join('')}
`; }).join(''); } function renderCard(task) { const displayDesc = task.displayDescription || task.description; const progress = task.subtaskProgress ?? 0; return `
${esc(displayDesc)}
${task.projectName ? `${esc(task.projectName)}` : ''} ${task.dueFormatted ? `${esc(task.dueFormatted)}` : ''}
${task.subtaskCount > 0 ? `
` : ''}
`; } // ============ Drag and Drop ============ let draggedTaskId = null; function onDragStart(e) { draggedTaskId = e.target.dataset.taskId; e.target.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', draggedTaskId); } function onDragEnd(e) { e.target.classList.remove('dragging'); draggedTaskId = null; // Remove all drag-over highlights document.querySelectorAll('.kanban-column.drag-over').forEach(col => { col.classList.remove('drag-over'); }); } function onDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const column = e.target.closest('.kanban-column'); if (column) column.classList.add('drag-over'); } function onDragLeave(e) { const column = e.target.closest('.kanban-column'); if (column && !column.contains(e.relatedTarget)) { column.classList.remove('drag-over'); } } async function onDrop(e) { e.preventDefault(); const column = e.target.closest('.kanban-column'); if (!column) return; column.classList.remove('drag-over'); const taskId = e.dataTransfer.getData('text/plain'); if (!taskId) return; const newStatus = column.dataset.status; const tasks = GoingsOn.state.tasks || []; const task = tasks.find(t => t.id === taskId); if (!task || task.status === newStatus) return; await handleDrop(taskId, task, newStatus); } async function handleDrop(taskId, task, newStatus) { try { if (newStatus === 'Started') { await GoingsOn.api.tasks.start(taskId); GoingsOn.ui.showToast('Task started!', 'success'); } else if (newStatus === 'Completed') { await GoingsOn.api.tasks.complete(taskId); GoingsOn.ui.showToast('Task completed!', 'success'); } else if (newStatus === 'Pending') { // Move back to pending via update await GoingsOn.api.tasks.update(taskId, { description: task.description, projectId: task.project_id || task.projectId || null, status: 'Pending', priority: task.priority, due: task.due || null, tags: task.tags || [], recurrence: task.recurrence || 'None', contactId: task.contactId || null, milestoneId: task.milestoneId || null, }); GoingsOn.ui.showToast('Task moved to Pending', 'success'); } // Re-fetch to get accurate server state (recurrence, etc.) await GoingsOn.tasks.load(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to update task'), 'error'); // Re-render to revert visual position await GoingsOn.tasks.load(); } } // ============ Populate Namespace ============ GoingsOn.tasksKanban = { render, onDragStart, onDragEnd, onDragOver, onDragLeave, onDrop, }; })();