Skip to main content

max / goingson

11.9 KB · 218 lines History Blame Raw
1 /**
2 * GoingsOn - Tasks Render Module
3 * Task row rendering, badges, subtask modal rendering
4 */
5
6 (function() {
7 'use strict';
8 const esc = GoingsOn.utils.escapeHtml;
9 const escAttr = GoingsOn.utils.escapeAttr;
10
11 // ============ Task Rendering Helpers ============
12
13 /**
14 * Render subtask count and annotation badges for a task row.
15 * @param {Object} task - Task object with subtaskCount, subtaskCompleted, annotations
16 * @returns {string} HTML string of badge elements, or empty string if none
17 */
18 function renderTaskBadges(task) {
19 let badges = '';
20 const subtaskCount = task.subtaskCount;
21 const completedSubtasks = task.subtaskCompleted;
22 const annotationCount = task.annotations ? task.annotations.length : 0;
23
24 if (subtaskCount > 0) {
25 badges += `<span class="task-badge has-items">${completedSubtasks}/${subtaskCount}</span>`;
26 }
27 if (annotationCount > 0) {
28 badges += `<span class="task-badge has-items">Notes: ${annotationCount}</span>`;
29 }
30
31 return badges ? `<span class="task-badges">${badges}</span>` : '';
32 }
33
34 /**
35 * Format a task's recurrence pattern for display.
36 * Only shows recurrence when the task also has a due date.
37 * @param {Object} task - Task object with recurrence and due fields
38 * @returns {string} Recurrence label or '-'
39 */
40 function formatRecurrence(task) {
41 // Only show recurrence if task has both recurrence AND a due date
42 if (task.recurrence && task.recurrence !== 'None' && task.due) {
43 return task.recurrence;
44 }
45 return '-';
46 }
47
48 /**
49 * Render a single task row as a div (for virtual scrolling).
50 * @param {Object} t - Task object
51 * @param {number} index - Task index
52 * @returns {string} HTML string
53 */
54 /**
55 * Format a minute count as a human-readable duration string.
56 * @param {number} mins - Minutes to format
57 * @returns {string} Formatted string (e.g., "1h 30m" or "45m")
58 */
59 function formatMinutes(mins) {
60 if (mins >= 60) return `${Math.floor(mins / 60)}h ${mins % 60}m`;
61 return `${mins}m`;
62 }
63
64 /**
65 * Render time tracking badge for a task (active timer or tracked/estimated).
66 * @param {Object} t - Task object with timerActive, estimatedMinutes, actualMinutes
67 * @returns {string} HTML string for the time badge, or empty string
68 */
69 function renderTimeBadge(t) {
70 if (t.timerActive) {
71 return '<span class="task-timer-active" title="Timer running"></span>';
72 }
73 if (t.estimatedMinutes || t.actualMinutes) {
74 const actual = t.actualMinutes || 0;
75 const est = t.estimatedMinutes;
76 const isOver = est && actual > est;
77 const label = est ? `${formatMinutes(actual)} / ${formatMinutes(est)}` : formatMinutes(actual);
78 return `<span class="task-time-badge${isOver ? ' over-estimate' : ''}" title="Tracked / Estimated">${esc(label)}</span>`;
79 }
80 return '';
81 }
82
83 function renderTaskRow(t, index) {
84 const progress = t.subtaskProgress ?? 0;
85 const displayDesc = t.displayDescription || t.description;
86 const isSelected = GoingsOn.tasks.selection.isSelected(t.id);
87 const isStarted = t.status === 'Started';
88
89 return `
90 <div class="task-row task-${t.status.toLowerCase()} ${t.isSnoozed ? 'task-snoozed' : ''} ${t.isOverdue ? 'task-overdue' : ''} ${isSelected ? 'selected' : ''}"
91 data-id="${escAttr(t.id)}"
92 oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')"
93 tabindex="0" role="row">
94 <div class="task-cell task-description" onclick="GoingsOn.taskOverview.open('${escAttr(t.id)}')">
95 ${isStarted ? `<span class="task-started-icon" title="Started - click to track time" onclick="event.stopPropagation(); GoingsOn.timeTracking.startTimer('${escAttr(t.id)}')"></span>` : ''}
96 <span class="task-description-text">${esc(displayDesc)}</span>
97 ${renderTimeBadge(t)}
98 ${renderTaskBadges(t)}
99 ${t.contactName ? `<span class="contact-badge" title="${escAttr(t.contactName)}">${esc(t.contactName)}</span>` : ''}
100 ${t.isSnoozed ? `<span class="snooze-badge" title="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}" aria-label="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}">Snoozed</span>` : ''}
101 </div>
102 <div class="task-cell task-project">${esc(t.projectName) || '-'}</div>
103 <div class="task-cell priority-${t.priority.toLowerCase()}" aria-label="Priority ${t.priority}">${t.priority.charAt(0)}</div>
104 <div class="task-cell task-due">${t.dueFormatted || '-'}</div>
105 <div class="task-cell task-recurrence">${formatRecurrence(t)}</div>
106 <div class="task-cell task-progress">
107 ${t.subtaskCount > 0 ? `
108 <div class="progress-bar-container" title="${t.subtaskCompleted}/${t.subtaskCount} subtasks"
109 role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100"
110 aria-label="${t.subtaskCompleted} of ${t.subtaskCount} subtasks completed">
111 <div class="progress-bar" style="width: ${progress}%"></div>
112 </div>
113 ` : '<span class="no-subtasks">-</span>'}
114 </div>
115 <div class="task-cell task-actions-cell" onclick="event.stopPropagation();">
116 <input type="checkbox" class="bulk-checkbox" data-id="${escAttr(t.id)}"
117 ${isSelected ? 'checked' : ''}
118 onchange="GoingsOn.tasks.toggleSelection('${escAttr(t.id)}', this, event)"
119 aria-label="Select task">
120 <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')" title="Actions" aria-label="Task actions">&#x22EE;</button>
121 </div>
122 </div>
123 `;
124 }
125
126 /**
127 * Render and open the subtasks management modal for a task.
128 * Shows progress bar, subtask list, add form, and link-task option.
129 * @param {string} taskId - Parent task ID
130 * @param {Array<Object>} subtasks - Array of subtask objects
131 */
132 function renderSubtasksModal(taskId, subtasks) {
133 const completedCount = subtasks.filter(s => s.isCompleted).length;
134 const totalCount = subtasks.length;
135 const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
136
137 const progressBar = totalCount > 0 ? `
138 <div class="mb-1">
139 <div class="text-sm-secondary" style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
140 <span>Progress</span>
141 <span>${completedCount}/${totalCount} (${progress}%)</span>
142 </div>
143 <div class="progress-bar-container" style="height: 12px;">
144 <div class="progress-bar" style="width: ${progress}%"></div>
145 </div>
146 </div>
147 ` : '';
148
149 const subtasksList = subtasks.length === 0
150 ? '<p class="subtasks-empty">No subtasks yet. Add one below!</p>'
151 : subtasks.map(s => {
152 const isLinked = s.linkedTaskId || s.linked_task_id;
153 const isCompleted = s.is_completed || s.isCompleted;
154 const linkedTaskId = s.linkedTaskId || s.linked_task_id;
155
156 if (isLinked) {
157 // Linked task subtask - clicking navigates to the linked task
158 return `
159 <div class="subtask-item subtask-item--linked">
160 <input type="checkbox" ${isCompleted ? 'checked' : ''} disabled class="subtask-checkbox-disabled" title="Completion syncs with linked task">
161 <span class="flex-1" style="cursor: pointer;${isCompleted ? ' text-decoration: line-through; opacity: 0.6;' : ''}" onclick="GoingsOn.tasks.openSubtasks('${escAttr(linkedTaskId)}')" title="Click to open linked task">
162 <span class="subtask-linked-tag">[Linked]</span> ${esc(s.text)}
163 </span>
164 <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" aria-label="Unlink task">&times;</button>
165 </div>
166 `;
167 } else {
168 // Regular text subtask
169 return `
170 <div class="subtask-item">
171 <input type="checkbox" ${isCompleted ? 'checked' : ''} onchange="GoingsOn.tasks.toggleSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" class="subtask-checkbox" aria-label="Toggle subtask completion">
172 <span class="flex-1${isCompleted ? ' subtask-text-done' : ''}">${esc(s.text)}</span>
173 <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" aria-label="Delete subtask">&times;</button>
174 </div>
175 `;
176 }
177 }).join('');
178
179 const content = `
180 ${progressBar}
181 <div style="margin-bottom: 1rem; max-height: 300px; overflow-y: auto;">
182 ${subtasksList}
183 </div>
184 <form onsubmit="GoingsOn.tasks.addSubtask(event, '${escAttr(taskId)}')" class="row-flex row-flex-2">
185 <input type="text" class="form-input flex-1" name="subtask_text" placeholder="Add a subtask...">
186 <button type="submit" class="btn btn-primary">Add</button>
187 </form>
188 <div style="margin-top: 0.5rem;">
189 <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openLinkTaskPicker('${escAttr(taskId)}')" style="width: 100%;">Link Another Task</button>
190 </div>
191 <div class="task-actions-bar">
192 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.openEdit('${escAttr(taskId)}')" title="Edit task fields">Edit</button>
193 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.addAnnotation('${escAttr(taskId)}')" title="Add a note to this task">Note</button>
194 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.timeTracking.startTimer('${escAttr(taskId)}')" title="Start live timer">Track Time</button>
195 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.focusTimer.start('${escAttr(taskId)}')" title="Pomodoro-style focus session">Focus</button>
196 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.dayPlan.openScheduleTaskModal('${escAttr(taskId)}')" title="Block time on day planner">Schedule</button>
197 <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.openPanel('${escAttr(taskId)}', null)" title="Manage file attachments">Files</button>
198 </div>
199 <div class="form-actions" style="margin-top: 0.75rem;">
200 <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openActions('${escAttr(taskId)}')">All Actions</button>
201 <div class="flex-1"></div>
202 <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.closeSubtasksAndRefresh()">Close</button>
203 </div>
204 `;
205 GoingsOn.ui.openModal('Manage Subtasks', content);
206 }
207
208 // ============ Populate GoingsOn.tasksRender Namespace ============
209
210 GoingsOn.tasksRender = {
211 renderTaskBadges,
212 renderTaskRow,
213 renderSubtasksModal,
214 formatRecurrence,
215 };
216
217 })();
218