/**
* 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 = '';
} 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