/** * @fileoverview Time tracking: floating timer widget, start/stop/discard. * * On init, checks for an active timer. When active, shows a floating bar * at the bottom with task name, elapsed time (h:mm:ss), and stop/discard buttons. * Elapsed time is computed client-side from timerStartedAt. */ (function() { 'use strict'; const esc = (s) => GoingsOn.utils.escapeHtml(s); let tickInterval = null; let activeTimer = null; // { taskId, taskDescription, startedAt } // ============ Timer Widget ============ function createWidget() { if (document.getElementById('timer-widget')) return; const widget = document.createElement('div'); widget.id = 'timer-widget'; widget.className = 'timer-widget hidden'; widget.innerHTML = `
`; document.body.appendChild(widget); widget.querySelector('.timer-stop-btn').addEventListener('click', stopActive); widget.querySelector('.timer-discard-btn').addEventListener('click', discardActive); } function showWidget(taskDescription, startedAt) { const widget = document.getElementById('timer-widget'); if (!widget) return; widget.querySelector('.timer-task-name').textContent = taskDescription; widget.classList.remove('hidden'); activeTimer = { startedAt: new Date(startedAt) }; updateElapsed(); if (tickInterval) clearInterval(tickInterval); tickInterval = setInterval(updateElapsed, 1000); } function hideWidget() { const widget = document.getElementById('timer-widget'); if (widget) widget.classList.add('hidden'); if (tickInterval) { clearInterval(tickInterval); tickInterval = null; } activeTimer = null; } function updateElapsed() { if (!activeTimer) return; const widget = document.getElementById('timer-widget'); if (!widget) return; const diff = Math.floor((Date.now() - activeTimer.startedAt.getTime()) / 1000); const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); const s = diff % 60; const display = h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; widget.querySelector('.timer-elapsed').textContent = display; } // ============ Actions ============ /** * Start a time tracking timer for a task. Shows the floating widget. * @param {string} taskId - Task ID to track time for */ async function startTimer(taskId) { console.log('[timer] startTimer called', { taskId, hasApi: !!GoingsOn.api?.timeTracking?.startTimer }); try { const result = await GoingsOn.api.timeTracking.startTimer(taskId); console.log('[timer] startTimer succeeded', result); await checkActive(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); } catch (err) { console.error('[timer] startTimer failed', err); GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to start timer'), 'error'); } } async function stopActive() { if (!activeTimer) return; try { const result = await GoingsOn.api.timeTracking.getActive(); if (result) { const session = await GoingsOn.api.timeTracking.stopTimer(result.taskId); if (session) { const mins = session.durationMinutes || 0; const display = mins >= 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins}m`; GoingsOn.ui.showToast(`Tracked ${display}`, 'success'); } } hideWidget(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to stop timer'), 'error'); } } async function discardActive() { if (!activeTimer) return; try { const result = await GoingsOn.api.timeTracking.getActive(); if (result) { await GoingsOn.api.timeTracking.discardTimer(result.taskId); } hideWidget(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); GoingsOn.ui.showToast('Timer discarded', 'info'); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to discard timer'), 'error'); } } /** * Check for an active timer session and show/hide the widget accordingly. */ async function checkActive() { try { const result = await GoingsOn.api.timeTracking.getActive(); if (result) { showWidget(result.taskDescription, result.session.startedAt); } else { hideWidget(); } } catch (err) { console.error('Failed to check active timer:', err); } } // ============ Timer Subview ============ let subviewTickInterval = null; let focusWorkMinutes = 25; let focusBreakMinutes = 5; /** * Format elapsed time since a start timestamp as h:mm:ss or m:ss. * @param {string} startedAt - ISO 8601 start timestamp * @returns {string} Formatted elapsed time */ function fmtElapsed(startedAt) { const diff = Math.max(0, Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000)); const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); const s = diff % 60; return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; } function clearSubviewTick() { if (subviewTickInterval) { clearInterval(subviewTickInterval); subviewTickInterval = null; } } /** * Load and render the Timer sub-view with active session, focus split, and task list. */ async function loadTimerView() { const container = document.getElementById('timer-subview-content'); if (!container) return; console.log('[timer] loadTimerView start'); // Fetch data independently so one failure doesn't block the rest let activeResult = null; let tasks = []; try { activeResult = await GoingsOn.api.timeTracking.getActive(); console.log('[timer] getActive result:', activeResult); } catch (err) { console.error('[timer] getActive failed:', err); } try { const [pendingResp, startedResp] = await Promise.all([ GoingsOn.api.tasks.listFiltered({ status: 'Pending', showSnoozed: false, limit: 200 }), GoingsOn.api.tasks.listFiltered({ status: 'Started', showSnoozed: false, limit: 200 }), ]); console.log('[timer] listFiltered results — pending:', pendingResp?.tasks?.length, 'started:', startedResp?.tasks?.length); const pending = pendingResp?.tasks || []; const started = startedResp?.tasks || []; // Started first (more likely to be tracked), then pending const allTasks = [...started, ...pending]; // Remove the actively-timed task from the list const activeTaskId = activeResult?.taskId; tasks = activeTaskId ? allTasks.filter(t => t.id !== activeTaskId) : allTasks; } catch (err) { console.error('[timer] listFiltered failed:', err); } clearSubviewTick(); let html = ''; // ---- Active session banner ---- if (activeResult) { html += ` `; const startMs = new Date(activeResult.session.startedAt).getTime(); subviewTickInterval = setInterval(() => { const el = document.getElementById('timer-subview-elapsed'); if (!el) { clearSubviewTick(); return; } const diff = Math.max(0, Math.floor((Date.now() - startMs) / 1000)); const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); const s = diff % 60; el.textContent = h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; }, 1000); } // ---- Focus split inputs ---- html += `No pending or started tasks to track.
`; } else if (tasks.length > 0) { const hasActive = !!activeResult; html += `