/** * GoingsOn - Projects Render Module * Project dashboard rendering: tasks, events, emails, milestones */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; /** * Empty state for a dashboard column. Same canonical primitive as the rest * of the app, with the compact `--dashboard` modifier and an optional * action button. * @param {string} message * @param {string} iconKey - EMPTY_STATE_ICONS key * @param {{label: string, onClick: string}} [action] */ function renderDashboardEmpty(message, iconKey, action) { let html = `
${GoingsOn.ui.emptyStateIcon(iconKey)}` + `

${esc(message)}

`; if (action && action.label && action.onClick) { html += ``; } return html + `
`; } // ============ Dashboard Rendering ============ /** * Render the project dashboard: tasks, events, emails, milestones, and attachments. * @param {string} projectId - Project ID to render dashboard for */ async function renderDashboard(projectId) { const tasksEl = document.getElementById('project-tasks-list'); const eventsEl = document.getElementById('project-events-list'); const emailsEl = document.getElementById('project-emails-list'); const attachmentsEl = document.getElementById('project-attachments-list'); // Wire up attach button const attachBtn = document.getElementById('project-attach-btn'); if (attachBtn) { attachBtn.onclick = () => GoingsOn.attachments.pickAndAttach(null, projectId); } // Load all data in parallel try { const [tasks, events, emails, milestones, attachments] = await Promise.all([ GoingsOn.api.tasks.listByProject(projectId), GoingsOn.api.events.listByProject(projectId), GoingsOn.api.emails.listByProject(projectId), GoingsOn.api.milestones.list(projectId), GoingsOn.api.attachments.list(null, projectId), ]); // Render milestones renderMilestones(milestones, projectId); // Render tasks (pre-sorted by urgency DESC from backend) if (tasks.length === 0) { tasksEl.innerHTML = renderDashboardEmpty('No tasks linked yet.', 'tasks'); } else if (tasks.every(t => t.status === 'Completed')) { tasksEl.innerHTML = '

All tasks complete.

'; } else { tasksEl.innerHTML = tasks.map(t => { const progress = t.subtaskProgress ?? 0; return `
${esc(t.description)} ${GoingsOn.tasks.renderTaskBadges(t)}
${t.subtaskCount > 0 ? `
` : ''}
${t.priority} ${t.dueFormatted ? `• ${t.dueFormatted}` : ''}
`}).join(''); } // Render events (pre-sorted by start_time ASC from backend) if (events.length === 0) { eventsEl.innerHTML = renderDashboardEmpty('No events linked yet.', 'events'); } else { eventsEl.innerHTML = events.map(e => `
${esc(e.title)}
${e.dateFormatted} • ${e.timeFormatted}
`).join(''); } // Render emails if (emails.length === 0) { emailsEl.innerHTML = renderDashboardEmpty('No emails linked yet.', 'emails'); } else { emailsEl.innerHTML = emails.map(e => `
${esc(e.subject)}
${esc(e.from)} • ${e.receivedFormatted}
`).join(''); } // Render attachments if (attachmentsEl) { if (attachments.length === 0) { attachmentsEl.innerHTML = renderDashboardEmpty( 'No attachments yet.', 'attachments', { label: 'Attach File', onClick: `GoingsOn.attachments.pickAndAttach(null, '${escAttr(projectId)}')` }, ); } else { attachmentsEl.innerHTML = attachments.map(a => `
${esc(a.filename)}
${esc(a.fileSizeFormatted)}
`).join(''); } } } catch (err) { tasksEl.innerHTML = `
Error: ${esc(err.message)}
`; } } // ============ Milestones Rendering ============ // Track completed milestones collapse state (ephemeral UI state) let showCompletedMilestones = false; function toggleCompletedMilestones() { showCompletedMilestones = !showCompletedMilestones; const projectId = GoingsOn.state.currentProjectId; if (projectId) { GoingsOn.projects.loadDashboard(projectId); } } /** * Render the milestones section of the project dashboard. * @param {Array} milestones - Milestone objects from the backend * @param {string} projectId - Parent project ID */ function renderMilestones(milestones, projectId) { const section = document.getElementById('project-milestones-section'); if (!section) return; if (!milestones || milestones.length === 0) { section.innerHTML = `

Milestones

No milestones yet

`; return; } const openMilestones = milestones.filter(m => m.status !== 'completed'); const completedMilestones = milestones.filter(m => m.status === 'completed'); let html = `

Milestones

${openMilestones.map((m, idx) => `
${esc(m.name)} ${m.targetDate ? `${esc(m.targetDate)}` : ''}
${m.completedCount}/${m.taskCount}
${idx > 0 ? `` : ''} ${idx < openMilestones.length - 1 ? `` : ''}
`).join('')}
`; // Completed milestones section (collapsed by default) if (completedMilestones.length > 0) { html += `
${completedMilestones.map(m => `
${esc(m.name)} Complete
`).join('')}
`; } section.innerHTML = html; } // ============ Populate Namespace ============ GoingsOn.projectsRender = { renderDashboard, renderMilestones, toggleCompletedMilestones, }; })();