/**
* GoingsOn - Monthly Review Render Module
* Pure rendering functions for the Month view.
*/
(function() {
'use strict';
const esc = GoingsOn.utils.escapeHtml;
const escAttr = GoingsOn.utils.escapeAttr;
/**
* Truncate a string with an ellipsis.
* @param {string} str - String to truncate
* @param {number} len - Maximum length
* @returns {string} Truncated string
*/
function truncate(str, len) {
if (!str) return '';
return str.length > len ? str.substring(0, len) + '...' : str;
}
// ============ Heat Map Calendar ============
/**
* Render the month heat-map calendar grid.
* @param {Object} r - Monthly review data with days, firstDayOffset
* @returns {string} HTML string
*/
function renderHeatMap(r) {
const dayHeaders = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
let html = '
';
html += '
';
for (const d of dayHeaders) {
html += `
${d}
`;
}
html += '
';
html += '
';
// Empty cells before the 1st
for (let i = 0; i < r.firstDayOffset; i++) {
html += '';
}
for (const day of r.days) {
const classes = ['month-heatmap-cell'];
if (day.isToday) classes.push('today');
if (day.isPast) classes.push('past');
if (day.isVacation) classes.push('vacation');
classes.push(`intensity-${day.intensity}`);
html += `
`;
html += `${day.dayNumber}`;
if (day.completedCount > 0 || day.eventCount > 0) {
html += '
';
if (day.completedCount > 0) html += `${day.completedCount}`;
if (day.eventCount > 0) html += `${day.eventCount}`;
html += '
';
}
html += '
';
}
// Empty cells after the last day
const totalCells = r.firstDayOffset + r.days.length;
const remainder = totalCells % 7;
if (remainder > 0) {
for (let i = 0; i < 7 - remainder; i++) {
html += '';
}
}
html += '
';
return html;
}
// ============ Accomplished ============
/**
* Render the Accomplished card with a sample of completed tasks for the month.
* @param {Object} r - Monthly review data
* @returns {string} HTML string
*/
function renderAccomplished(r) {
const count = r.tasksCompletedCount || 0;
const top = r.tasksCompletedTop || [];
if (count === 0) return '';
const items = top.map(t => `
`;
}
// ============ Month in Numbers ============
/**
* Render the "Month in Numbers" stats card.
* @param {Object} r - Monthly review data
* @returns {string} HTML string
*/
function renderStats(r) {
let html = '
';
html += '
Month in Numbers
';
html += '
';
html += renderStatItem('Tasks Completed', r.tasksCompletedCount, 'completed');
html += renderStatItem('Tasks Created', r.tasksCreatedCount, 'created');
html += renderStatItem('Events', r.eventsCount, 'events');
html += renderStatItem('Best Streak', r.completionStreak + 'd', 'streak');
html += '
';
if (r.busiestDay || r.quietestDay) {
html += '
';
if (r.busiestDay) html += `Busiest: ${formatDateShort(r.busiestDay)}`;
if (r.quietestDay) html += `Quietest: ${formatDateShort(r.quietestDay)}`;
html += '
`;
}
function formatDateShort(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr + 'T12:00:00');
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
// ============ Project Health ============
/**
* Render the Project Health card showing per-project activity direction.
* @param {Object} r - Monthly review data with projectPulse array
* @returns {string} HTML string
*/
function renderProjectPulse(r) {
if (!r.projectPulse || r.projectPulse.length === 0) return '';
let html = '
`;
html += `${esc(truncate(p.name, 24))}`;
html += `+${p.completed} done / +${p.created} new`;
html += `${arrow}`;
html += '
';
}
html += '
';
return html;
}
// ============ Monthly Goals ============
/**
* Render the Monthly Goals card with 3 goal slots.
* @param {Object} r - Monthly review data with goals array
* @returns {string} HTML string
*/
function renderGoals(r) {
let html = '
';
html += '
';
html += 'Monthly Goals';
html += 'Set up to 3 goals';
html += '
';
html += '
';
for (let pos = 1; pos <= 3; pos++) {
const goal = r.goals.find(g => g.position === pos);
if (goal) {
html += renderGoalItem(goal, pos);
} else {
html += renderEmptyGoalSlot(r.month, pos);
}
}
html += '
';
return html;
}
function renderGoalItem(goal, position) {
const statusIcons = { active: '○', done: '✓', abandoned: '✗' };
const icon = statusIcons[goal.status] || '○';
let html = `
`;
html += `Goal #${position}`;
html += `
`;
html += ``;
html += `${esc(goal.text)}`;
html += ``;
html += `
`;
html += '
';
return html;
}
function renderEmptyGoalSlot(month, position) {
return `
Goal #${position}+ Add goal
`;
}
// ============ Reflection ============
/**
* Render the reflection card using the shared helper.
* @param {Object} r - Monthly review data with reflection object
* @returns {string} HTML string
*/
function renderReflection(r) {
return GoingsOn.planReviewToggle.renderReflection({
idPrefix: 'monthly',
prompts: [
{ key: 'highlight', label: 'What was the highlight of this month?', placeholder: "Something you're proud of...", value: r.reflection?.highlightText || '' },
{ key: 'change', label: 'What would you change?', placeholder: 'Something to improve next month...', value: r.reflection?.changeText || '' },
],
});
}
// ============ Patterns ============
/**
* Render the Patterns card with observed behavioral patterns.
* @param {Object} r - Monthly review data with patterns array
* @returns {string} HTML string
*/
function renderPatterns(r) {
if (!r.patterns || r.patterns.length === 0) return '';
let html = '