Skip to main content

max / goingson

11.3 KB · 221 lines History Blame Raw
1 /**
2 * GoingsOn - Projects Render Module
3 * Project dashboard rendering: tasks, events, emails, milestones
4 */
5
6 (function() {
7 'use strict';
8 const esc = GoingsOn.utils.escapeHtml;
9 const escAttr = GoingsOn.utils.escapeAttr;
10
11 // ============ Dashboard Rendering ============
12
13 /**
14 * Render the project dashboard: tasks, events, emails, milestones, and attachments.
15 * @param {string} projectId - Project ID to render dashboard for
16 */
17 async function renderDashboard(projectId) {
18 const tasksEl = document.getElementById('project-tasks-list');
19 const eventsEl = document.getElementById('project-events-list');
20 const emailsEl = document.getElementById('project-emails-list');
21 const attachmentsEl = document.getElementById('project-attachments-list');
22
23 // Wire up attach button
24 const attachBtn = document.getElementById('project-attach-btn');
25 if (attachBtn) {
26 attachBtn.onclick = () => GoingsOn.attachments.pickAndAttach(null, projectId);
27 }
28
29 // Load all data in parallel
30 try {
31 const [tasks, events, emails, milestones, attachments] = await Promise.all([
32 GoingsOn.api.tasks.listByProject(projectId),
33 GoingsOn.api.events.listByProject(projectId),
34 GoingsOn.api.emails.listByProject(projectId),
35 GoingsOn.api.milestones.list(projectId),
36 GoingsOn.api.attachments.list(null, projectId),
37 ]);
38
39 // Render milestones
40 renderMilestones(milestones, projectId);
41
42 // Render tasks (pre-sorted by urgency DESC from backend)
43 if (tasks.length === 0) {
44 tasksEl.innerHTML = '<div class="empty-state empty-state--dashboard">No tasks linked to this project</div>';
45 } else if (tasks.every(t => t.status === 'Completed')) {
46 tasksEl.innerHTML = '<div class="empty-state empty-state--compact"><p class="empty-state-text">All tasks complete.</p></div>';
47 } else {
48 tasksEl.innerHTML = tasks.map(t => {
49 const progress = t.subtaskProgress ?? 0;
50
51 return `
52 <div class="card card--list-item" onclick="GoingsOn.tasks.openSubtasks('${escAttr(t.id)}')"
53 tabindex="0" role="button" aria-label="Open task ${esc(t.description)}">
54 <div class="dashboard-item-title">
55 ${esc(t.description)}
56 ${GoingsOn.tasks.renderTaskBadges(t)}
57 </div>
58 ${t.subtaskCount > 0 ? `
59 <div class="progress-bar-container" style="margin: 0.5rem 0;" title="${t.subtaskCompleted}/${t.subtaskCount} subtasks"
60 role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100">
61 <div class="progress-bar" style="width: ${progress}%"></div>
62 </div>
63 ` : ''}
64 <div class="dashboard-item-meta">
65 <span class="priority-${t.priority.toLowerCase()}" aria-label="Priority ${t.priority}">${t.priority}</span>
66 ${t.dueFormatted ? `&bull; ${t.dueFormatted}` : ''}
67 </div>
68 </div>
69 `}).join('');
70 }
71
72 // Render events (pre-sorted by start_time ASC from backend)
73 if (events.length === 0) {
74 eventsEl.innerHTML = '<div class="empty-state empty-state--dashboard">No events linked to this project</div>';
75 } else {
76 eventsEl.innerHTML = events.map(e => `
77 <div class="card card--list-item" onclick="GoingsOn.events.open('${escAttr(e.id)}')"
78 tabindex="0" role="button" aria-label="Open event ${esc(e.title)}">
79 <div class="dashboard-item-title">${esc(e.title)}</div>
80 <div class="dashboard-item-meta">
81 ${e.dateFormatted} &bull; ${e.timeFormatted}
82 </div>
83 </div>
84 `).join('');
85 }
86
87 // Render emails
88 if (emails.length === 0) {
89 emailsEl.innerHTML = '<div class="empty-state empty-state--dashboard">No emails linked to this project</div>';
90 } else {
91 emailsEl.innerHTML = emails.map(e => `
92 <div class="card card--list-item ${e.is_read ? '' : 'unread'}" onclick="GoingsOn.emails.open('${escAttr(e.id)}')"
93 tabindex="0" role="button" aria-label="Open email ${esc(e.subject)}">
94 <div class="dashboard-item-title">${esc(e.subject)}</div>
95 <div class="dashboard-item-meta">
96 ${esc(e.from)} &bull; ${e.receivedFormatted}
97 </div>
98 </div>
99 `).join('');
100 }
101
102 // Render attachments
103 if (attachmentsEl) {
104 if (attachments.length === 0) {
105 attachmentsEl.innerHTML = '<div class="empty-state empty-state--dashboard">No attachments</div>';
106 } else {
107 attachmentsEl.innerHTML = attachments.map(a => `
108 <div class="card card--list-item" onclick="GoingsOn.attachments.openPanel(null, '${escAttr(projectId)}')"
109 tabindex="0" role="button" aria-label="View attachment ${esc(a.filename)}">
110 <div class="dashboard-item-title">${esc(a.filename)}</div>
111 <div class="dashboard-item-meta">${esc(a.fileSizeFormatted)}</div>
112 </div>
113 `).join('');
114 }
115 }
116 } catch (err) {
117 tasksEl.innerHTML = `<div class="empty-state empty-state--dashboard empty-state--error">Error: ${esc(err.message)}</div>`;
118 }
119 }
120
121 // ============ Milestones Rendering ============
122
123 // Track completed milestones collapse state (ephemeral UI state)
124 let showCompletedMilestones = false;
125
126 function toggleCompletedMilestones() {
127 showCompletedMilestones = !showCompletedMilestones;
128 const projectId = GoingsOn.state.currentProjectId;
129 if (projectId) {
130 GoingsOn.projects.loadDashboard(projectId);
131 }
132 }
133
134 /**
135 * Render the milestones section of the project dashboard.
136 * @param {Array<Object>} milestones - Milestone objects from the backend
137 * @param {string} projectId - Parent project ID
138 */
139 function renderMilestones(milestones, projectId) {
140 const section = document.getElementById('project-milestones-section');
141 if (!section) return;
142
143 if (!milestones || milestones.length === 0) {
144 section.innerHTML = `
145 <div class="milestones-header">
146 <h3>Milestones</h3>
147 <button class="btn btn-sm btn-primary" onclick="GoingsOn.projects.openNewMilestone()">+ Add</button>
148 </div>
149 <p class="milestones-empty">No milestones yet</p>
150 `;
151 return;
152 }
153
154 const openMilestones = milestones.filter(m => m.status !== 'completed');
155 const completedMilestones = milestones.filter(m => m.status === 'completed');
156
157 let html = `
158 <div class="milestones-header">
159 <h3>Milestones</h3>
160 <button class="btn btn-sm btn-primary" onclick="GoingsOn.projects.openNewMilestone()">+ Add</button>
161 </div>
162 <div class="milestones-list">
163 ${openMilestones.map((m, idx) => `
164 <div class="milestone-card">
165 <div class="milestone-info">
166 <span class="milestone-name">${esc(m.name)}</span>
167 ${m.targetDate ? `<span class="milestone-date">${esc(m.targetDate)}</span>` : ''}
168 </div>
169 <div class="milestone-progress">
170 <div class="progress-bar-container" title="${m.completedCount}/${m.taskCount} tasks">
171 <div class="progress-bar" style="width: ${m.progress}%"></div>
172 </div>
173 <span class="milestone-progress-text">${m.completedCount}/${m.taskCount}</span>
174 </div>
175 <div class="milestone-actions">
176 ${idx > 0 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escAttr(m.id)}', -1)" title="Move up">\u25B2</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25B2</span>'}
177 ${idx < openMilestones.length - 1 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escAttr(m.id)}', 1)" title="Move down">\u25BC</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25BC</span>'}
178 <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escAttr(m.id)}')">Edit</button>
179 <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escAttr(m.id)}')">Delete</button>
180 </div>
181 </div>
182 `).join('')}
183 </div>`;
184
185 // Completed milestones section (collapsed by default)
186 if (completedMilestones.length > 0) {
187 html += `
188 <div class="milestones-completed-section">
189 <button class="btn btn-link milestones-completed-toggle" onclick="GoingsOn.projects.toggleCompletedMilestones()">
190 ${showCompletedMilestones ? 'Hide' : 'Show'} ${completedMilestones.length} completed
191 </button>
192 <div class="milestones-completed-list" style="display: ${showCompletedMilestones ? 'block' : 'none'};">
193 ${completedMilestones.map(m => `
194 <div class="milestone-card completed milestone-card-summary">
195 <div class="milestone-info">
196 <span class="milestone-name">${esc(m.name)}</span>
197 <span class="milestone-complete-badge">Complete</span>
198 </div>
199 <div class="milestone-actions">
200 <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escAttr(m.id)}')">Edit</button>
201 <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escAttr(m.id)}')">Delete</button>
202 </div>
203 </div>
204 `).join('')}
205 </div>
206 </div>`;
207 }
208
209 section.innerHTML = html;
210 }
211
212 // ============ Populate Namespace ============
213
214 GoingsOn.projectsRender = {
215 renderDashboard,
216 renderMilestones,
217 toggleCompletedMilestones,
218 };
219
220 })();
221