/** * GoingsOn - Monthly Review Module * Orchestrator for the Month view: heat-map calendar, stats, goals, reflections. */ (function() { 'use strict'; let currentMonth = null; // YYYY-MM string // ============ Navigation ============ function getCurrentMonthStr() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; } function previousMonth() { if (!currentMonth) return; const [y, m] = currentMonth.split('-').map(Number); const prev = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`; currentMonth = prev; load(); } function nextMonth() { if (!currentMonth) return; const [y, m] = currentMonth.split('-').map(Number); const next = m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, '0')}`; currentMonth = next; load(); } function goToCurrentMonth() { currentMonth = getCurrentMonthStr(); load(); } // ============ Data Loading ============ async function load() { if (!currentMonth) { currentMonth = getCurrentMonthStr(); } try { const data = await GoingsOn.api.monthlyReview.get(currentMonth); GoingsOn.state.set('monthlyReview', data); render(data); } catch (err) { console.error('Failed to load monthly review:', err); const container = document.getElementById('monthly-review-content'); if (container) { container.innerHTML = `
Failed to load monthly review.
`; } GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load monthly review'), 'error', { action: { label: 'Retry', fn: load }, duration: 8000, }); } } // ============ Rendering ============ function render(r) { // Update month display in header const displayEl = document.getElementById('monthly-review-month-display'); if (displayEl) displayEl.textContent = r.monthDisplay; const container = document.getElementById('monthly-review-content'); if (!container) return; const R = GoingsOn.monthlyReviewRender; let html = '

Set goals, watch your patterns, then close out the month with Finish & Review.

'; html += R.renderHeatMap(r); html += R.renderGoals(r); html += '
'; html += R.renderStats(r); html += R.renderAccomplished(r); html += R.renderProjectPulse(r); html += R.renderPatterns(r); html += '
'; const hasReflection = r.reflection && (r.reflection.highlightText || r.reflection.changeText); const periodState = currentPeriodState(); if (periodState === 'current') { html += `
`; } else if (periodState === 'past') { const label = hasReflection ? 'View Past Review' : 'Review Past Month'; html += `
`; } container.innerHTML = html; GoingsOn.planReviewToggle.setStatusBadge('month-review-status-badge', periodState, hasReflection); const settings = GoingsOn.planReviewToggle.getSettings(); const dayOfMonth = new Date().getDate(); if (periodState === 'current' && settings.reviewNudges && dayOfMonth <= 7 && !hasReflection) { GoingsOn.planReviewToggle.updateDot('month', true); } else { GoingsOn.planReviewToggle.updateDot('month', false); } } /** * Returns 'past', 'current', or 'future' for the currently viewed month. */ function currentPeriodState() { const now = new Date(); const nowStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; if (!currentMonth || currentMonth === nowStr) return 'current'; return currentMonth < nowStr ? 'past' : 'future'; } /** * Open the end-of-month reflection modal: prompts + complete action. */ function openFinishReviewModal() { const r = GoingsOn.state.monthlyReview; if (!r) return; const R = GoingsOn.monthlyReviewRender; const isPast = currentPeriodState() === 'past'; const banner = isPast ? `
You are reviewing a past month (${GoingsOn.utils.escapeHtml(r.monthDisplay)}), not the current one.
` : ''; const content = `
${banner} ${R.renderReflection(r)}
`; const title = isPast ? `Reviewing Past: ${r.monthDisplay}` : `Wrap Up: ${r.monthDisplay}`; GoingsOn.ui.openModal(title, content, { large: true }); const prompts = [ { key: 'highlight' }, { key: 'change' }, ]; GoingsOn.planReviewToggle.wireReflectionAutosave({ idPrefix: 'monthly', prompts, onChange: (values) => { GoingsOn.api.monthlyReview .saveReflection(r.month, values.highlight, values.change) .catch(err => console.error('Failed to save reflection:', err)); }, debounceMs: 1000, }); GoingsOn.planReviewToggle.autoGrowReflection({ idPrefix: 'monthly', prompts }); } // ============ Goals ============ /** * Open a form modal to add a monthly goal. * @param {string} month - YYYY-MM month string * @param {number} position - Goal slot position (1-3) */ function addGoal(month, position) { GoingsOn.ui.openFormModal({ title: 'Add Goal', entityType: 'goal', isEdit: false, fields: [ { name: 'text', type: 'text', label: 'Goal', required: true, placeholder: 'What do you want to achieve this month?' }, ], onSubmit: async (data) => { await GoingsOn.ui.apiCall( GoingsOn.api.monthlyReview.upsertGoal(month, data.text.trim(), position), { successMessage: 'Goal added', reload: load } ); }, }); } /** * Cycle a goal's status: active -> done -> abandoned -> active. * @param {string} id - Goal ID */ async function cycleGoalStatus(id) { const data = GoingsOn.state.monthlyReview; if (!data) return; const goal = data.goals.find(g => g.id === id); if (!goal) return; const next = { active: 'done', done: 'abandoned', abandoned: 'active' }; const newStatus = next[goal.status] || 'active'; await GoingsOn.ui.apiCall( GoingsOn.api.monthlyReview.updateGoalStatus(id, newStatus), { reload: load } ); } /** * Delete a monthly goal after confirmation. * @param {string} id - Goal ID */ async function deleteGoal(id) { const confirmed = await GoingsOn.ui.confirmDelete('Delete this goal?'); if (!confirmed) return; await GoingsOn.ui.apiCall( GoingsOn.api.monthlyReview.deleteGoal(id), { successMessage: 'Goal deleted', reload: load } ); } // ============ Day Navigation ============ /** * Navigate to the day plan view for a specific date. * @param {string} dateStr - YYYY-MM-DD date string */ function navigateToDay(dateStr) { GoingsOn.state.set('dayPlanDate', new Date(dateStr + 'T12:00:00')); GoingsOn.navigation.switchView('day-plan'); } /** * Show a day summary popup (touch) or navigate to the day (desktop). * @param {string} dateStr - YYYY-MM-DD date string */ async function showDaySummary(dateStr) { // On non-touch devices, navigate directly if (!GoingsOn.touch?.isTouchDevice) { navigateToDay(dateStr); return; } try { const day = await GoingsOn.api.dayPlanning.getDay(dateStr); const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; const dateDisplay = new Date(dateStr + 'T12:00:00') .toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); // Build stat chips const scheduled = day.timelineItems ? day.timelineItems.length : 0; const unscheduled = day.unscheduledTasks ? day.unscheduledTasks.length : 0; let html = `
`; html += `
${esc(dateDisplay)}
`; html += `
`; html += `${scheduled} scheduled`; html += `${unscheduled} unscheduled`; html += `
`; // Top timeline items (max 5) const items = day.timelineItems || []; if (items.length > 0) { html += ``; } else { html += `

No scheduled items

`; } html += ``; html += `
`; GoingsOn.ui.openModal(dateDisplay, html); } catch { // Fallback on error navigateToDay(dateStr); } } // ============ Complete Review ============ /** * Explicitly save the monthly reflection and mark the review as complete. */ async function complete() { const highlightEl = document.getElementById('monthly-highlight'); const changeEl = document.getElementById('monthly-change'); const highlight = highlightEl?.value?.trim() || ''; const change = changeEl?.value?.trim() || ''; if (!highlight && !change) { GoingsOn.ui.showToast('Write at least one reflection before completing', 'error'); return; } try { await GoingsOn.api.monthlyReview.saveReflection(currentMonth, highlight, change); GoingsOn.ui.closeModal(); GoingsOn.ui.showToast('Monthly review completed!', 'success'); await load(); } catch (err) { GoingsOn.ui.showToast('Failed to complete review', 'error'); } } // ============ Exports ============ GoingsOn.monthlyReview = { load, previousMonth, nextMonth, goToCurrentMonth, addGoal, cycleGoalStatus, deleteGoal, navigateToDay, showDaySummary, complete, openFinishReviewModal, }; })();