| 1 |
|
| 2 |
* GoingsOn - Task Overview Module |
| 3 |
* Full detail view for a task: metadata, subtasks, time sessions, annotations. |
| 4 |
* For recurring tasks: completion heatmap and streak stats. |
| 5 |
|
| 6 |
|
| 7 |
(function() { |
| 8 |
'use strict'; |
| 9 |
const esc = GoingsOn.utils.escapeHtml; |
| 10 |
const escAttr = GoingsOn.utils.escapeAttr; |
| 11 |
|
| 12 |
let currentTaskId = null; |
| 13 |
let heatmapMonth = null; |
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
* Phase 7 Tier 6 — task detail now opens as a right-side drawer overlay |
| 19 |
* on top of whichever view the user is currently in (task list, contact |
| 20 |
* dashboard, day plan...). The drawer reuses the same render() output as |
| 21 |
* the old full-page #task-overview-view, which is retained for cases |
| 22 |
* where something explicitly navigates to it (router deep links). |
| 23 |
|
| 24 |
async function open(taskId) { |
| 25 |
currentTaskId = taskId; |
| 26 |
heatmapMonth = new Date(); |
| 27 |
|
| 28 |
const drawer = document.getElementById('task-detail-drawer'); |
| 29 |
const content = document.getElementById('task-drawer-content'); |
| 30 |
if (!drawer || !content) { |
| 31 |
|
| 32 |
|
| 33 |
return openLegacy(taskId); |
| 34 |
} |
| 35 |
|
| 36 |
drawer.classList.add('visible'); |
| 37 |
drawer.setAttribute('aria-hidden', 'false'); |
| 38 |
document.addEventListener('keydown', handleDrawerKeydown); |
| 39 |
|
| 40 |
content.innerHTML = '<div class="loading">Loading...</div>'; |
| 41 |
markActiveRow(taskId); |
| 42 |
|
| 43 |
try { |
| 44 |
const data = await GoingsOn.api.tasks.getOverview(taskId); |
| 45 |
render(data); |
| 46 |
} catch (err) { |
| 47 |
content.innerHTML = `<div class="empty-state"><p class="empty-state-text">Failed to load task: ${esc(String(err))}</p></div>`; |
| 48 |
} |
| 49 |
} |
| 50 |
|
| 51 |
|
| 52 |
* Legacy fallback: navigate to the full-page #task-overview-view. Used |
| 53 |
* only when the drawer element isn't in the DOM. Keeps deep-link routes |
| 54 |
* working through the transition. |
| 55 |
|
| 56 |
async function openLegacy(taskId) { |
| 57 |
GoingsOn.navigation.switchView('task-overview'); |
| 58 |
|
| 59 |
const content = document.getElementById('task-overview-content'); |
| 60 |
content.innerHTML = '<div class="loading">Loading...</div>'; |
| 61 |
|
| 62 |
try { |
| 63 |
const data = await GoingsOn.api.tasks.getOverview(taskId); |
| 64 |
renderLegacy(data); |
| 65 |
} catch (err) { |
| 66 |
content.innerHTML = `<div class="empty-state"><p class="empty-state-text">Failed to load task: ${esc(String(err))}</p></div>`; |
| 67 |
} |
| 68 |
} |
| 69 |
|
| 70 |
function close() { |
| 71 |
currentTaskId = null; |
| 72 |
const drawer = document.getElementById('task-detail-drawer'); |
| 73 |
if (drawer && drawer.classList.contains('visible')) { |
| 74 |
drawer.classList.remove('visible'); |
| 75 |
drawer.setAttribute('aria-hidden', 'true'); |
| 76 |
document.removeEventListener('keydown', handleDrawerKeydown); |
| 77 |
clearActiveRow(); |
| 78 |
return; |
| 79 |
} |
| 80 |
|
| 81 |
GoingsOn.navigation.switchView('tasks'); |
| 82 |
} |
| 83 |
|
| 84 |
|
| 85 |
|
| 86 |
function handleDrawerKeydown(e) { |
| 87 |
if (e.key === 'Escape') { |
| 88 |
close(); |
| 89 |
return; |
| 90 |
} |
| 91 |
|
| 92 |
|
| 93 |
const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName) |
| 94 |
|| e.target.isContentEditable; |
| 95 |
if (isTyping) return; |
| 96 |
|
| 97 |
const dir = (e.key === 'j' || e.key === 'J' || e.key === 'ArrowDown') ? 1 |
| 98 |
: (e.key === 'k' || e.key === 'K' || e.key === 'ArrowUp') ? -1 |
| 99 |
: 0; |
| 100 |
if (dir === 0) return; |
| 101 |
e.preventDefault(); |
| 102 |
cycleToSibling(dir); |
| 103 |
} |
| 104 |
|
| 105 |
|
| 106 |
function cycleToSibling(dir) { |
| 107 |
const rows = Array.from(document.querySelectorAll('.task-row[data-id]')); |
| 108 |
if (rows.length === 0) return; |
| 109 |
const currentIdx = rows.findIndex(r => r.dataset.id === currentTaskId); |
| 110 |
let nextIdx = currentIdx + dir; |
| 111 |
if (currentIdx === -1) nextIdx = dir > 0 ? 0 : rows.length - 1; |
| 112 |
if (nextIdx < 0 || nextIdx >= rows.length) return; |
| 113 |
const nextId = rows[nextIdx].dataset.id; |
| 114 |
if (nextId) open(nextId); |
| 115 |
} |
| 116 |
|
| 117 |
function markActiveRow(taskId) { |
| 118 |
clearActiveRow(); |
| 119 |
document.querySelectorAll(`.task-row[data-id="${CSS.escape(taskId)}"]`).forEach(el => { |
| 120 |
el.classList.add('task-row--active'); |
| 121 |
}); |
| 122 |
} |
| 123 |
|
| 124 |
function clearActiveRow() { |
| 125 |
document.querySelectorAll('.task-row--active').forEach(el => { |
| 126 |
el.classList.remove('task-row--active'); |
| 127 |
}); |
| 128 |
} |
| 129 |
|
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
function render(data) { |
| 134 |
const t = data.task; |
| 135 |
const content = document.getElementById('task-drawer-content'); |
| 136 |
const title = document.getElementById('task-drawer-title'); |
| 137 |
const actions = document.getElementById('task-drawer-actions'); |
| 138 |
if (!content) { |
| 139 |
renderLegacy(data); |
| 140 |
return; |
| 141 |
} |
| 142 |
title.textContent = t.description; |
| 143 |
content.innerHTML = buildOverviewHtml(data); |
| 144 |
if (actions) actions.innerHTML = renderActions(t); |
| 145 |
} |
| 146 |
|
| 147 |
|
| 148 |
function renderLegacy(data) { |
| 149 |
const t = data.task; |
| 150 |
const content = document.getElementById('task-overview-content'); |
| 151 |
const title = document.getElementById('task-overview-title'); |
| 152 |
if (title) title.textContent = t.description; |
| 153 |
content.innerHTML = buildOverviewHtml(data); |
| 154 |
const actions = document.getElementById('task-overview-actions'); |
| 155 |
if (actions) actions.innerHTML = renderActions(t); |
| 156 |
} |
| 157 |
|
| 158 |
|
| 159 |
function buildOverviewHtml(data) { |
| 160 |
const t = data.task; |
| 161 |
let html = ''; |
| 162 |
if (data.recurrenceChain.length > 0 && data.streak) { |
| 163 |
html += renderHabitSection(data); |
| 164 |
} |
| 165 |
html += renderMetadata(t); |
| 166 |
if (t.subtasks.length > 0 || t.status !== 'Completed') { |
| 167 |
html += renderSubtasks(t); |
| 168 |
} |
| 169 |
html += renderTimeTracking(t, data.timeSessions); |
| 170 |
html += renderAnnotations(t); |
| 171 |
return html; |
| 172 |
} |
| 173 |
|
| 174 |
|
| 175 |
|
| 176 |
function renderHabitSection(data) { |
| 177 |
const s = data.streak; |
| 178 |
let html = '<div class="task-overview-section">'; |
| 179 |
html += '<h3 class="task-overview-section-title">Completion History</h3>'; |
| 180 |
|
| 181 |
|
| 182 |
html += '<div class="task-overview-stats">'; |
| 183 |
html += renderStat('Current Streak', s.currentStreak + 'd'); |
| 184 |
html += renderStat('Best Streak', s.bestStreak + 'd'); |
| 185 |
html += renderStat('Completion Rate', Math.round(s.completionRate30d) + '%'); |
| 186 |
html += renderStat('Total Completed', s.totalCompleted + '/' + s.totalInstances); |
| 187 |
html += '</div>'; |
| 188 |
|
| 189 |
|
| 190 |
html += '<div class="task-overview-heatmap-nav">'; |
| 191 |
html += `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.taskOverview.prevMonth()">◀</button>`; |
| 192 |
html += `<span id="task-heatmap-month-label">${formatMonthLabel(heatmapMonth)}</span>`; |
| 193 |
html += `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.taskOverview.nextMonth()">▶</button>`; |
| 194 |
html += '</div>'; |
| 195 |
html += `<div id="task-heatmap-container">${renderHeatmap(data.recurrenceChain)}</div>`; |
| 196 |
|
| 197 |
|
| 198 |
const completed = data.recurrenceChain |
| 199 |
.filter(i => i.completedAt) |
| 200 |
.slice(0, 10); |
| 201 |
if (completed.length > 0) { |
| 202 |
html += '<div class="task-overview-completion-list">'; |
| 203 |
for (const inst of completed) { |
| 204 |
const date = new Date(inst.completedAt); |
| 205 |
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); |
| 206 |
const time = inst.actualMinutes > 0 ? ` (${inst.actualMinutes}m tracked)` : ''; |
| 207 |
html += `<div class="task-overview-completion-item">${esc(dateStr)} — completed${esc(time)}</div>`; |
| 208 |
} |
| 209 |
html += '</div>'; |
| 210 |
} |
| 211 |
|
| 212 |
html += '</div>'; |
| 213 |
return html; |
| 214 |
} |
| 215 |
|
| 216 |
function renderStat(label, value) { |
| 217 |
return `<div class="task-overview-stat"><div class="task-overview-stat-value">${esc(String(value))}</div><div class="task-overview-stat-label">${esc(label)}</div></div>`; |
| 218 |
} |
| 219 |
|
| 220 |
|
| 221 |
|
| 222 |
function renderHeatmap(chain) { |
| 223 |
const year = heatmapMonth.getFullYear(); |
| 224 |
const month = heatmapMonth.getMonth(); |
| 225 |
const daysInMonth = new Date(year, month + 1, 0).getDate(); |
| 226 |
const firstDay = new Date(year, month, 1); |
| 227 |
|
| 228 |
const firstDayOffset = (firstDay.getDay() + 6) % 7; |
| 229 |
|
| 230 |
|
| 231 |
const completionMap = {}; |
| 232 |
for (const inst of chain) { |
| 233 |
if (inst.completedAt) { |
| 234 |
const d = new Date(inst.completedAt); |
| 235 |
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; |
| 236 |
completionMap[key] = (completionMap[key] || 0) + 1; |
| 237 |
} |
| 238 |
} |
| 239 |
|
| 240 |
const today = new Date(); |
| 241 |
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; |
| 242 |
|
| 243 |
const dayHeaders = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; |
| 244 |
let html = '<div class="month-heatmap">'; |
| 245 |
html += '<div class="month-heatmap-header">'; |
| 246 |
for (const d of dayHeaders) { |
| 247 |
html += `<div class="month-heatmap-day-header">${d}</div>`; |
| 248 |
} |
| 249 |
html += '</div><div class="month-heatmap-grid">'; |
| 250 |
|
| 251 |
for (let i = 0; i < firstDayOffset; i++) { |
| 252 |
html += '<div class="month-heatmap-cell empty"></div>'; |
| 253 |
} |
| 254 |
|
| 255 |
for (let day = 1; day <= daysInMonth; day++) { |
| 256 |
const dateKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; |
| 257 |
const count = completionMap[dateKey] || 0; |
| 258 |
const intensity = count >= 3 ? 3 : count; |
| 259 |
const isToday = dateKey === todayStr; |
| 260 |
const isPast = new Date(year, month, day) < new Date(today.getFullYear(), today.getMonth(), today.getDate()); |
| 261 |
|
| 262 |
const classes = ['month-heatmap-cell']; |
| 263 |
if (isToday) classes.push('today'); |
| 264 |
if (isPast) classes.push('past'); |
| 265 |
classes.push(`intensity-${intensity}`); |
| 266 |
|
| 267 |
html += `<div class="${classes.join(' ')}" title="${count} completion${count !== 1 ? 's' : ''}" tabindex="0">`; |
| 268 |
html += `<span class="month-heatmap-day-number">${day}</span>`; |
| 269 |
if (count > 0) { |
| 270 |
html += `<div class="month-heatmap-dots"><span class="month-dot completed">${count}</span></div>`; |
| 271 |
} |
| 272 |
html += '</div>'; |
| 273 |
} |
| 274 |
|
| 275 |
const totalCells = firstDayOffset + daysInMonth; |
| 276 |
const remainder = totalCells % 7; |
| 277 |
if (remainder > 0) { |
| 278 |
for (let i = 0; i < 7 - remainder; i++) { |
| 279 |
html += '<div class="month-heatmap-cell empty"></div>'; |
| 280 |
} |
| 281 |
} |
| 282 |
|
| 283 |
html += '</div></div>'; |
| 284 |
return html; |
| 285 |
} |
| 286 |
|
| 287 |
function prevMonth() { |
| 288 |
heatmapMonth.setMonth(heatmapMonth.getMonth() - 1); |
| 289 |
refreshHeatmap(); |
| 290 |
} |
| 291 |
|
| 292 |
function nextMonth() { |
| 293 |
heatmapMonth.setMonth(heatmapMonth.getMonth() + 1); |
| 294 |
refreshHeatmap(); |
| 295 |
} |
| 296 |
|
| 297 |
async function refreshHeatmap() { |
| 298 |
const label = document.getElementById('task-heatmap-month-label'); |
| 299 |
if (label) label.textContent = formatMonthLabel(heatmapMonth); |
| 300 |
|
| 301 |
if (!currentTaskId) return; |
| 302 |
try { |
| 303 |
const data = await GoingsOn.api.tasks.getOverview(currentTaskId); |
| 304 |
const container = document.getElementById('task-heatmap-container'); |
| 305 |
if (container) container.innerHTML = renderHeatmap(data.recurrenceChain); |
| 306 |
} catch (err) { |
| 307 |
console.error('Failed to refresh heatmap:', err); |
| 308 |
} |
| 309 |
} |
| 310 |
|
| 311 |
function formatMonthLabel(date) { |
| 312 |
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); |
| 313 |
} |
| 314 |
|
| 315 |
|
| 316 |
|
| 317 |
function renderMetadata(t) { |
| 318 |
let html = '<div class="task-overview-section">'; |
| 319 |
html += '<div class="task-overview-meta">'; |
| 320 |
|
| 321 |
const STATUS_COLOR = { completed: 'green', started: 'blue', pending: 'muted' }; |
| 322 |
const PRIORITY_COLOR = { h: 'red', m: 'yellow', l: 'muted' }; |
| 323 |
const chip = (color, text) => `<span class="badge badge--xs badge--filled" data-color="${color}">${esc(text)}</span>`; |
| 324 |
const badges = []; |
| 325 |
badges.push(chip(STATUS_COLOR[t.status.toLowerCase()] || 'muted', t.status)); |
| 326 |
badges.push(chip(PRIORITY_COLOR[t.priority.toLowerCase()] || 'muted', t.priority)); |
| 327 |
if (t.isFocus) badges.push(chip('blue', 'Focus')); |
| 328 |
if (t.isOverdue) badges.push(chip('red', 'Overdue')); |
| 329 |
if (t.isSnoozed) badges.push(chip('yellow', 'Snoozed')); |
| 330 |
html += `<div class="task-overview-badges">${badges.join(' ')}</div>`; |
| 331 |
|
| 332 |
if (t.descriptionHtml) { |
| 333 |
html += `<div class="markdown-content">${t.descriptionHtml}</div>`; |
| 334 |
} |
| 335 |
|
| 336 |
const details = []; |
| 337 |
if (t.projectName) details.push(`<strong>Project:</strong> ${esc(t.projectName)}`); |
| 338 |
if (t.dueFormatted) details.push(`<strong>Due:</strong> ${esc(t.dueFormatted)}`); |
| 339 |
if (t.recurrence && t.recurrence !== 'None') details.push(`<strong>Recurrence:</strong> ${esc(t.recurrence)}`); |
| 340 |
if (t.contactName) details.push(`<strong>Contact:</strong> ${esc(t.contactName)}`); |
| 341 |
if (t.tags && t.tags.length > 0) { |
| 342 |
details.push(`<strong>Tags:</strong> ${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join(' ')}`); |
| 343 |
} |
| 344 |
|
| 345 |
if (details.length > 0) { |
| 346 |
html += `<div class="task-overview-details">${details.map(d => `<div>${d}</div>`).join('')}</div>`; |
| 347 |
} |
| 348 |
|
| 349 |
html += '</div></div>'; |
| 350 |
return html; |
| 351 |
} |
| 352 |
|
| 353 |
|
| 354 |
|
| 355 |
function renderSubtasks(t) { |
| 356 |
const completed = t.subtasks.filter(s => s.isCompleted).length; |
| 357 |
const total = t.subtasks.length; |
| 358 |
|
| 359 |
let html = '<div class="task-overview-section">'; |
| 360 |
html += `<h3 class="task-overview-section-title">Subtasks <span class="task-overview-count">${completed}/${total}</span></h3>`; |
| 361 |
|
| 362 |
if (total > 0) { |
| 363 |
const pct = Math.round((completed / total) * 100); |
| 364 |
html += `<div class="progress-bar"><div class="progress-fill" style="width: ${pct}%"></div></div>`; |
| 365 |
} |
| 366 |
|
| 367 |
html += '<div class="task-overview-subtask-list">'; |
| 368 |
for (const s of t.subtasks) { |
| 369 |
const checked = s.isCompleted ? 'checked' : ''; |
| 370 |
html += `<div class="task-overview-subtask">`; |
| 371 |
html += `<input type="checkbox" class="bulk-checkbox" ${checked} onchange="GoingsOn.taskOverview.toggleSubtask('${escAttr(s.id)}')" ${s.linkedTaskId ? 'disabled' : ''}>`; |
| 372 |
html += `<span class="${s.isCompleted ? 'completed-text' : ''}">${esc(s.text)}</span>`; |
| 373 |
if (s.linkedTaskId) html += ' <span class="badge">Linked</span>'; |
| 374 |
html += '</div>'; |
| 375 |
} |
| 376 |
html += '</div>'; |
| 377 |
|
| 378 |
|
| 379 |
html += `<div class="task-overview-add-form"> |
| 380 |
<input type="text" class="form-input" id="overview-new-subtask" placeholder="Add subtask..." onkeydown="if(event.key==='Enter') GoingsOn.taskOverview.addSubtask()"> |
| 381 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.taskOverview.addSubtask()">Add</button> |
| 382 |
</div>`; |
| 383 |
|
| 384 |
html += '</div>'; |
| 385 |
return html; |
| 386 |
} |
| 387 |
|
| 388 |
async function toggleSubtask(subtaskId) { |
| 389 |
try { |
| 390 |
await GoingsOn.api.subtasks.toggle(currentTaskId, subtaskId); |
| 391 |
GoingsOn.cache.invalidate('tasks'); |
| 392 |
if (currentTaskId) open(currentTaskId); |
| 393 |
} catch (err) { |
| 394 |
GoingsOn.ui.showToast('Failed to toggle subtask', 'error'); |
| 395 |
} |
| 396 |
} |
| 397 |
|
| 398 |
async function addSubtask() { |
| 399 |
const input = document.getElementById('overview-new-subtask'); |
| 400 |
const text = input?.value?.trim(); |
| 401 |
if (!text) return; |
| 402 |
|
| 403 |
try { |
| 404 |
await GoingsOn.api.subtasks.add(currentTaskId, text); |
| 405 |
input.value = ''; |
| 406 |
GoingsOn.cache.invalidate('tasks'); |
| 407 |
if (currentTaskId) open(currentTaskId); |
| 408 |
} catch (err) { |
| 409 |
GoingsOn.ui.showToast('Failed to add subtask', 'error'); |
| 410 |
} |
| 411 |
} |
| 412 |
|
| 413 |
|
| 414 |
|
| 415 |
function renderTimeTracking(t, sessions) { |
| 416 |
let html = '<div class="task-overview-section">'; |
| 417 |
const est = t.estimatedMinutes ? `${t.estimatedMinutes}m est` : ''; |
| 418 |
const actual = `${t.actualMinutes}m tracked`; |
| 419 |
const label = est ? `${actual} / ${est}` : actual; |
| 420 |
html += `<h3 class="task-overview-section-title">Time Tracking <span class="task-overview-count">${esc(label)}</span></h3>`; |
| 421 |
|
| 422 |
if (t.estimatedMinutes && t.estimatedMinutes > 0) { |
| 423 |
const pct = Math.min(Math.round((t.actualMinutes / t.estimatedMinutes) * 100), 100); |
| 424 |
const overClass = t.isOverEstimate ? ' over-estimate' : ''; |
| 425 |
html += `<div class="progress-bar${overClass}"><div class="progress-fill" style="width: ${pct}%"></div></div>`; |
| 426 |
} |
| 427 |
|
| 428 |
if (sessions.length > 0) { |
| 429 |
html += '<div class="task-overview-sessions">'; |
| 430 |
for (const s of sessions) { |
| 431 |
const start = new Date(s.startedAt); |
| 432 |
const dateStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); |
| 433 |
const timeStr = start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); |
| 434 |
const duration = s.durationMinutes != null ? `${s.durationMinutes}m` : 'active'; |
| 435 |
html += `<div class="task-overview-session">${esc(dateStr)} ${esc(timeStr)} — ${esc(duration)}</div>`; |
| 436 |
} |
| 437 |
html += '</div>'; |
| 438 |
} |
| 439 |
|
| 440 |
html += '</div>'; |
| 441 |
return html; |
| 442 |
} |
| 443 |
|
| 444 |
|
| 445 |
|
| 446 |
function renderAnnotations(t) { |
| 447 |
let html = '<div class="task-overview-section">'; |
| 448 |
html += `<h3 class="task-overview-section-title">Notes <span class="task-overview-count">${t.annotations.length}</span></h3>`; |
| 449 |
|
| 450 |
if (t.annotations.length > 0) { |
| 451 |
html += '<div class="task-overview-annotations">'; |
| 452 |
for (const a of t.annotations) { |
| 453 |
const date = new Date(a.timestamp); |
| 454 |
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); |
| 455 |
html += `<div class="task-overview-annotation">`; |
| 456 |
html += `<div class="task-overview-annotation-date">${esc(dateStr)}</div>`; |
| 457 |
html += `<div class="task-overview-annotation-text">${esc(a.note)}</div>`; |
| 458 |
html += '</div>'; |
| 459 |
} |
| 460 |
html += '</div>'; |
| 461 |
} |
| 462 |
|
| 463 |
|
| 464 |
html += `<div class="task-overview-add-form"> |
| 465 |
<input type="text" class="form-input" id="overview-new-note" placeholder="Add note..." onkeydown="if(event.key==='Enter') GoingsOn.taskOverview.addNote()"> |
| 466 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.taskOverview.addNote()">Add</button> |
| 467 |
</div>`; |
| 468 |
|
| 469 |
html += '</div>'; |
| 470 |
return html; |
| 471 |
} |
| 472 |
|
| 473 |
async function addNote() { |
| 474 |
const input = document.getElementById('overview-new-note'); |
| 475 |
const text = input?.value?.trim(); |
| 476 |
if (!text) return; |
| 477 |
|
| 478 |
try { |
| 479 |
await GoingsOn.api.annotations.add(currentTaskId, text); |
| 480 |
input.value = ''; |
| 481 |
GoingsOn.cache.invalidate('tasks'); |
| 482 |
if (currentTaskId) open(currentTaskId); |
| 483 |
} catch (err) { |
| 484 |
GoingsOn.ui.showToast('Failed to add note', 'error'); |
| 485 |
} |
| 486 |
} |
| 487 |
|
| 488 |
|
| 489 |
|
| 490 |
function renderActions(t) { |
| 491 |
let html = ''; |
| 492 |
if (t.status !== 'Completed') { |
| 493 |
html += `<button class="btn btn-primary" onclick="GoingsOn.taskOverview.completeTask()">Complete</button> `; |
| 494 |
} |
| 495 |
html += `<button class="btn btn-secondary" onclick="GoingsOn.tasks.openEdit('${escAttr(t.id)}')">Edit</button> `; |
| 496 |
html += `<button class="btn btn-secondary text-accent-red" onclick="GoingsOn.taskOverview.deleteTask()">Delete</button>`; |
| 497 |
return html; |
| 498 |
} |
| 499 |
|
| 500 |
async function completeTask() { |
| 501 |
if (!currentTaskId) return; |
| 502 |
try { |
| 503 |
await GoingsOn.api.tasks.complete(currentTaskId); |
| 504 |
GoingsOn.cache.invalidate('tasks'); |
| 505 |
GoingsOn.ui.showToast('Task completed!', 'success'); |
| 506 |
open(currentTaskId); |
| 507 |
} catch (err) { |
| 508 |
GoingsOn.ui.showToast('Failed to complete task', 'error'); |
| 509 |
} |
| 510 |
} |
| 511 |
|
| 512 |
async function deleteTask() { |
| 513 |
if (!currentTaskId) return; |
| 514 |
if (!await GoingsOn.ui.confirmDelete('task')) return; |
| 515 |
try { |
| 516 |
await GoingsOn.api.tasks.delete(currentTaskId); |
| 517 |
GoingsOn.cache.invalidate('tasks'); |
| 518 |
GoingsOn.ui.showToast('Task deleted', 'success'); |
| 519 |
close(); |
| 520 |
} catch (err) { |
| 521 |
GoingsOn.ui.showToast('Failed to delete task', 'error'); |
| 522 |
} |
| 523 |
} |
| 524 |
|
| 525 |
|
| 526 |
|
| 527 |
GoingsOn.taskOverview = { |
| 528 |
open, |
| 529 |
close, |
| 530 |
prevMonth, |
| 531 |
nextMonth, |
| 532 |
toggleSubtask, |
| 533 |
addSubtask, |
| 534 |
addNote, |
| 535 |
completeTask, |
| 536 |
deleteTask, |
| 537 |
}; |
| 538 |
|
| 539 |
})(); |
| 540 |
|