/** * GoingsOn - Tasks Render Module * Task row rendering, badges, subtask modal rendering */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttrValue; const escArg = GoingsOn.utils.escapeHandlerArg; const escAttrVal = GoingsOn.utils.escapeAttrValue; // ============ Task Rendering Helpers ============ /** * Render subtask count and annotation badges for a task row. * @param {Object} task - Task object with subtaskCount, subtaskCompleted, annotations * @returns {string} HTML string of badge elements, or empty string if none */ function renderTaskBadges(task) { let badges = ''; const subtaskCount = task.subtaskCount; const completedSubtasks = task.subtaskCompleted; const annotationCount = task.annotations ? task.annotations.length : 0; if (subtaskCount > 0) { badges += `${completedSubtasks}/${subtaskCount}`; } if (annotationCount > 0) { badges += `Notes: ${annotationCount}`; } return badges ? `${badges}` : ''; } /** * Format a task's recurrence pattern for display. * Only shows recurrence when the task also has a due date. * @param {Object} task - Task object with recurrence and due fields * @returns {string} Recurrence label or '-' */ function formatRecurrence(task) { // Only show recurrence if task has both recurrence AND a due date if (task.recurrence && task.recurrence !== 'None' && task.due) { return task.recurrence; } return '-'; } /** * Render a single task row as a div (for virtual scrolling). * @param {Object} t - Task object * @param {number} index - Task index * @returns {string} HTML string */ /** * Format a minute count as a human-readable duration string. * @param {number} mins - Minutes to format * @returns {string} Formatted string (e.g., "1h 30m" or "45m") */ function formatMinutes(mins) { if (mins >= 60) return `${Math.floor(mins / 60)}h ${mins % 60}m`; return `${mins}m`; } /** * Render time tracking badge for a task (active timer or tracked/estimated). * @param {Object} t - Task object with timerActive, estimatedMinutes, actualMinutes * @returns {string} HTML string for the time badge, or empty string */ function renderTimeBadge(t) { if (t.timerActive) { return ''; } if (t.estimatedMinutes || t.actualMinutes) { const actual = t.actualMinutes || 0; const est = t.estimatedMinutes; const isOver = est && actual > est; const label = est ? `${formatMinutes(actual)} / ${formatMinutes(est)}` : formatMinutes(actual); return `${esc(label)}`; } return ''; } /** * Render the at-a-glance status-token dot for a task row. The colour rolls the * task's tokens up: neutral (muted, no tokens), pending (amber, a token is not * yet done — e.g. a commit not pushed), or complete (green, all done). Clicking * opens the commits/token manager. * @param {Object} t - Task with tokenSummary + statusTokens * @returns {string} HTML for the dot button */ function renderTokenDot(t) { const count = (t.statusTokens || []).length; // Only tasks with tokens get a dot — a plain "real life" task stays clean. if (count === 0) return ''; const summary = t.tokenSummary || 'neutral'; let title; if (summary === 'complete') { title = count > 1 ? 'All commits pushed' : 'Commit pushed'; } else { title = count > 1 ? 'Commits associated, not all pushed' : 'Commit associated, not pushed'; } return ``; } function renderTaskRow(t, index) { const progress = t.subtaskProgress ?? 0; const displayDesc = t.displayDescription || t.description; const isSelected = GoingsOn.tasks.selection.isSelected(t.id); const isStarted = t.status === 'Started'; return `
${isStarted ? `` : ''} ${esc(displayDesc)} ${renderTokenDot(t)} ${renderTimeBadge(t)} ${renderTaskBadges(t)} ${t.contactName ? `${esc(t.contactName)}` : ''} ${t.isSnoozed ? `Snoozed` : ''}
${esc(t.projectName) || '-'}
${t.priority.charAt(0)}
${t.dueFormatted || '-'}
${formatRecurrence(t)}
${t.subtaskCount > 0 ? `
` : '-'}
`; } /** * Render and open the subtasks management modal for a task. * Shows progress bar, subtask list, add form, and link-task option. * @param {string} taskId - Parent task ID * @param {Array} subtasks - Array of subtask objects */ function renderSubtasksModal(taskId, subtasks) { const completedCount = subtasks.filter(s => s.isCompleted).length; const totalCount = subtasks.length; const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; const progressBar = totalCount > 0 ? `
Progress ${completedCount}/${totalCount} (${progress}%)
` : ''; const subtasksList = subtasks.length === 0 ? '

No subtasks yet. Add one below!

' : subtasks.map(s => { const isLinked = s.linkedTaskId || s.linked_task_id; const isCompleted = s.is_completed || s.isCompleted; const linkedTaskId = s.linkedTaskId || s.linked_task_id; if (isLinked) { // Linked task subtask - clicking navigates to the linked task return `
[Linked] ${esc(s.text)}
`; } else { // Regular text subtask return `
${esc(s.text)}
`; } }).join(''); const content = ` ${progressBar}
${subtasksList}
`; GoingsOn.ui.openModal('Manage Subtasks', content); } /** * Render and open the status-tokens (commits) manager for a task. * Lists linked commits with a pushed toggle, a primary toggle, and a remove * button, plus an add form. Colours: amber = associated but not pushed, * green = pushed. * @param {string} taskId - Parent task ID * @param {Array} tokens - Array of status-token objects */ function renderStatusTokensModal(taskId, tokens) { const list = (tokens || []).length === 0 ? '

No commits linked yet. Add one below.

' : tokens.map(k => { const pushed = k.state === 'Complete'; const primary = !!k.isPrimary; const ref = k.reference; const p = pushed ? '1' : '0'; const pr = primary ? '1' : '0'; const nextPushed = pushed ? '0' : '1'; const nextPrimary = primary ? '0' : '1'; return `
${esc(ref)}${primary ? ' primary' : ''}
`; }).join(''); const content = `

Link the commit(s) that advanced this task. Amber = associated but not pushed; green = pushed. Mark the one that resolved it as primary.

${list}
`; GoingsOn.ui.openModal('Commits', content); } // ============ Populate GoingsOn.tasksRender Namespace ============ GoingsOn.tasksRender = { renderTaskBadges, renderTokenDot, renderTaskRow, renderSubtasksModal, renderStatusTokensModal, formatRecurrence, }; })();