Skip to main content

max / goingson

4.2 KB · 129 lines History Blame Raw
1 /**
2 * @fileoverview Time tracking summary panel for the Day view.
3 *
4 * Collapsible section showing:
5 * - Today's total tracked time
6 * - This week's per-project breakdown (CSS bar chart, no library)
7 */
8 (function() {
9 'use strict';
10
11 const esc = (s) => GoingsOn.utils.escapeHtml(s);
12
13 /**
14 * Format a minute count as a human-readable duration.
15 * @param {number} mins - Minutes to format
16 * @returns {string} Formatted string (e.g., "2h 15m", "45m", "3h")
17 */
18 function formatMinutes(mins) {
19 if (mins < 60) return `${mins}m`;
20 const h = Math.floor(mins / 60);
21 const m = mins % 60;
22 return m > 0 ? `${h}h ${m}m` : `${h}h`;
23 }
24
25 // ============ Render ============
26
27 /**
28 * Render the time tracking summary panel (today's total + weekly per-project bars).
29 * @param {HTMLElement} container - DOM element to render into
30 */
31 async function render(container) {
32 if (!container) return;
33
34 const now = new Date();
35 const todayStr = now.toISOString().slice(0, 10);
36
37 // Week start (Monday)
38 const day = now.getDay();
39 const diff = day === 0 ? 6 : day - 1;
40 const weekStart = new Date(now);
41 weekStart.setDate(now.getDate() - diff);
42 weekStart.setHours(0, 0, 0, 0);
43
44 const weekEnd = new Date(weekStart);
45 weekEnd.setDate(weekStart.getDate() + 7);
46
47 let summaries = [];
48 try {
49 summaries = await GoingsOn.api.timeTracking.getSummary(
50 weekStart.toISOString(),
51 weekEnd.toISOString()
52 );
53 } catch (err) {
54 console.error('Failed to load time summary:', err);
55 return;
56 }
57
58 // Calculate today's total
59 const todayTotal = summaries
60 .filter(s => s.date === todayStr)
61 .reduce((sum, s) => sum + s.totalMinutes, 0);
62
63 // Aggregate by project for the week
64 const projectMap = new Map();
65 for (const s of summaries) {
66 const key = s.projectId || '__none__';
67 const existing = projectMap.get(key) || { name: s.projectName || 'No Project', minutes: 0, sessions: 0 };
68 existing.minutes += s.totalMinutes;
69 existing.sessions += s.sessionCount;
70 projectMap.set(key, existing);
71 }
72
73 const projects = Array.from(projectMap.values()).sort((a, b) => b.minutes - a.minutes);
74 const maxMinutes = projects.length > 0 ? projects[0].minutes : 1;
75
76 let html = `
77 <div class="time-summary-section">
78 <button class="time-summary-toggle" aria-expanded="true">
79 <span class="time-summary-toggle-icon">&#9660;</span>
80 Time Tracked
81 </button>
82 <div class="time-summary-body">
83 <div class="time-summary-today">
84 <span class="time-summary-today-label">Today</span>
85 <span class="time-summary-today-value">${esc(formatMinutes(todayTotal))}</span>
86 </div>`;
87
88 if (projects.length > 0) {
89 html += `<div class="time-summary-week-header">This Week</div>`;
90 for (const p of projects) {
91 const pct = Math.round((p.minutes / maxMinutes) * 100);
92 html += `
93 <div class="time-summary-project">
94 <div class="time-summary-project-info">
95 <span class="time-summary-project-name">${esc(p.name)}</span>
96 <span class="time-summary-project-time">${esc(formatMinutes(p.minutes))}</span>
97 </div>
98 <div class="time-summary-bar">
99 <div class="time-summary-bar-fill" style="width: ${pct}%"></div>
100 </div>
101 </div>`;
102 }
103 }
104
105 html += `</div></div>`;
106 container.innerHTML = html;
107
108 // Toggle collapse
109 const toggle = container.querySelector('.time-summary-toggle');
110 const body = container.querySelector('.time-summary-body');
111 if (toggle && body) {
112 toggle.addEventListener('click', () => {
113 const expanded = toggle.getAttribute('aria-expanded') === 'true';
114 toggle.setAttribute('aria-expanded', String(!expanded));
115 body.classList.toggle('collapsed', expanded);
116 toggle.querySelector('.time-summary-toggle-icon').textContent = expanded ? '\u25B6' : '\u25BC';
117 });
118 }
119 }
120
121 // ============ Namespace ============
122
123 GoingsOn.timeSummary = {
124 render,
125 formatMinutes,
126 };
127
128 })();
129