Skip to main content

max / goingson

11.5 KB · 343 lines History Blame Raw
1 /**
2 * GoingsOn - Monthly Review Module
3 * Orchestrator for the Month view: heat-map calendar, stats, goals, reflections.
4 */
5
6 (function() {
7 'use strict';
8
9 let currentMonth = null; // YYYY-MM string
10
11 // ============ Navigation ============
12
13 function getCurrentMonthStr() {
14 const now = new Date();
15 return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
16 }
17
18 function previousMonth() {
19 if (!currentMonth) return;
20 const [y, m] = currentMonth.split('-').map(Number);
21 const prev = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`;
22 currentMonth = prev;
23 load();
24 }
25
26 function nextMonth() {
27 if (!currentMonth) return;
28 const [y, m] = currentMonth.split('-').map(Number);
29 const next = m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, '0')}`;
30 currentMonth = next;
31 load();
32 }
33
34 function goToCurrentMonth() {
35 currentMonth = getCurrentMonthStr();
36 load();
37 }
38
39 // ============ Data Loading ============
40
41 async function load() {
42 if (!currentMonth) {
43 currentMonth = getCurrentMonthStr();
44 }
45
46 try {
47 const data = await GoingsOn.api.monthlyReview.get(currentMonth);
48 GoingsOn.state.set('monthlyReview', data);
49 render(data);
50 } catch (err) {
51 console.error('Failed to load monthly review:', err);
52 const container = document.getElementById('monthly-review-content');
53 if (container) {
54 container.innerHTML = `<div class="empty-state">Failed to load monthly review.</div>`;
55 }
56 GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load monthly review'), 'error', {
57 action: { label: 'Retry', fn: load },
58 duration: 8000,
59 });
60 }
61 }
62
63 // ============ Rendering ============
64
65 function render(r) {
66 // Update month display in header
67 const displayEl = document.getElementById('monthly-review-month-display');
68 if (displayEl) displayEl.textContent = r.monthDisplay;
69
70 const container = document.getElementById('monthly-review-content');
71 if (!container) return;
72
73 const R = GoingsOn.monthlyReviewRender;
74
75 let html = '<p class="review-intro">Set goals, watch your patterns, then close out the month with Finish &amp; Review.</p>';
76 html += R.renderHeatMap(r);
77 html += R.renderGoals(r);
78 html += '<div class="monthly-review-cards">';
79 html += R.renderStats(r);
80 html += R.renderAccomplished(r);
81 html += R.renderProjectPulse(r);
82 html += R.renderPatterns(r);
83 html += '</div>';
84
85 const hasReflection = r.reflection && (r.reflection.highlightText || r.reflection.changeText);
86 const periodState = currentPeriodState();
87 if (periodState === 'current') {
88 html += `
89 <div class="finish-review-bar">
90 <button class="btn btn-primary finish-review-btn" id="month-finish-review-btn" onclick="GoingsOn.monthlyReview.openFinishReviewModal()">
91 ${hasReflection ? 'Update Review' : 'Finish & Review'}
92 </button>
93 </div>
94 `;
95 } else if (periodState === 'past') {
96 const label = hasReflection ? 'View Past Review' : 'Review Past Month';
97 html += `
98 <div class="finish-review-bar">
99 <button class="btn btn-secondary finish-review-btn" id="month-finish-review-btn" onclick="GoingsOn.monthlyReview.openFinishReviewModal()">
100 ${label}
101 </button>
102 </div>
103 `;
104 }
105
106 container.innerHTML = html;
107
108 GoingsOn.planReviewToggle.setStatusBadge('month-review-status-badge', periodState, hasReflection);
109
110 const settings = GoingsOn.planReviewToggle.getSettings();
111 const dayOfMonth = new Date().getDate();
112 if (periodState === 'current' && settings.reviewNudges && dayOfMonth <= 7 && !hasReflection) {
113 GoingsOn.planReviewToggle.updateDot('month', true);
114 } else {
115 GoingsOn.planReviewToggle.updateDot('month', false);
116 }
117 }
118
119 /**
120 * Returns 'past', 'current', or 'future' for the currently viewed month.
121 */
122 function currentPeriodState() {
123 const now = new Date();
124 const nowStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
125 if (!currentMonth || currentMonth === nowStr) return 'current';
126 return currentMonth < nowStr ? 'past' : 'future';
127 }
128
129 /**
130 * Open the end-of-month reflection modal: prompts + complete action.
131 */
132 function openFinishReviewModal() {
133 const r = GoingsOn.state.monthlyReview;
134 if (!r) return;
135 const R = GoingsOn.monthlyReviewRender;
136 const isPast = currentPeriodState() === 'past';
137
138 const banner = isPast
139 ? `<div class="past-review-banner">You are reviewing a past month (${GoingsOn.utils.escapeHtml(r.monthDisplay)}), not the current one.</div>`
140 : '';
141
142 const content = `
143 <div class="finish-review-modal-content">
144 ${banner}
145 ${R.renderReflection(r)}
146 <div class="review-actions-grid">
147 <button class="btn btn-primary" onclick="GoingsOn.monthlyReview.complete()">
148 ${isPast ? 'Save Review' : 'Complete Review'}
149 </button>
150 </div>
151 </div>
152 `;
153
154 const title = isPast ? `Reviewing Past: ${r.monthDisplay}` : `Wrap Up: ${r.monthDisplay}`;
155 GoingsOn.ui.openModal(title, content, { large: true });
156
157 const prompts = [
158 { key: 'highlight' },
159 { key: 'change' },
160 ];
161 GoingsOn.planReviewToggle.wireReflectionAutosave({
162 idPrefix: 'monthly',
163 prompts,
164 onChange: (values) => {
165 GoingsOn.api.monthlyReview
166 .saveReflection(r.month, values.highlight, values.change)
167 .catch(err => console.error('Failed to save reflection:', err));
168 },
169 debounceMs: 1000,
170 });
171 GoingsOn.planReviewToggle.autoGrowReflection({ idPrefix: 'monthly', prompts });
172 }
173
174 // ============ Goals ============
175
176 /**
177 * Open a form modal to add a monthly goal.
178 * @param {string} month - YYYY-MM month string
179 * @param {number} position - Goal slot position (1-3)
180 */
181 function addGoal(month, position) {
182 GoingsOn.ui.openFormModal({
183 title: 'Add Goal',
184 entityType: 'goal',
185 isEdit: false,
186 fields: [
187 { name: 'text', type: 'text', label: 'Goal', required: true, placeholder: 'What do you want to achieve this month?' },
188 ],
189 onSubmit: async (data) => {
190 await GoingsOn.ui.apiCall(
191 GoingsOn.api.monthlyReview.upsertGoal(month, data.text.trim(), position),
192 { successMessage: 'Goal added', reload: load }
193 );
194 },
195 });
196 }
197
198 /**
199 * Cycle a goal's status: active -> done -> abandoned -> active.
200 * @param {string} id - Goal ID
201 */
202 async function cycleGoalStatus(id) {
203 const data = GoingsOn.state.monthlyReview;
204 if (!data) return;
205
206 const goal = data.goals.find(g => g.id === id);
207 if (!goal) return;
208
209 const next = { active: 'done', done: 'abandoned', abandoned: 'active' };
210 const newStatus = next[goal.status] || 'active';
211
212 await GoingsOn.ui.apiCall(
213 GoingsOn.api.monthlyReview.updateGoalStatus(id, newStatus),
214 { reload: load }
215 );
216 }
217
218 /**
219 * Delete a monthly goal after confirmation.
220 * @param {string} id - Goal ID
221 */
222 async function deleteGoal(id) {
223 const confirmed = await GoingsOn.ui.confirmDelete('Delete this goal?');
224 if (!confirmed) return;
225
226 await GoingsOn.ui.apiCall(
227 GoingsOn.api.monthlyReview.deleteGoal(id),
228 { successMessage: 'Goal deleted', reload: load }
229 );
230 }
231
232 // ============ Day Navigation ============
233
234 /**
235 * Navigate to the day plan view for a specific date.
236 * @param {string} dateStr - YYYY-MM-DD date string
237 */
238 function navigateToDay(dateStr) {
239 GoingsOn.state.set('dayPlanDate', new Date(dateStr + 'T12:00:00'));
240 GoingsOn.navigation.switchView('day-plan');
241 }
242
243 /**
244 * Show a day summary popup (touch) or navigate to the day (desktop).
245 * @param {string} dateStr - YYYY-MM-DD date string
246 */
247 async function showDaySummary(dateStr) {
248 // On non-touch devices, navigate directly
249 if (!GoingsOn.touch?.isTouchDevice) {
250 navigateToDay(dateStr);
251 return;
252 }
253
254 try {
255 const day = await GoingsOn.api.dayPlanning.getDay(dateStr);
256 const esc = GoingsOn.utils.escapeHtml;
257 const escAttr = GoingsOn.utils.escapeAttr;
258
259 const dateDisplay = new Date(dateStr + 'T12:00:00')
260 .toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
261
262 // Build stat chips
263 const scheduled = day.timelineItems ? day.timelineItems.length : 0;
264 const unscheduled = day.unscheduledTasks ? day.unscheduledTasks.length : 0;
265
266 let html = `<div class="day-summary-sheet">`;
267 html += `<div class="day-summary-date">${esc(dateDisplay)}</div>`;
268 html += `<div class="day-summary-stats">`;
269 html += `<span class="day-summary-chip">${scheduled} scheduled</span>`;
270 html += `<span class="day-summary-chip">${unscheduled} unscheduled</span>`;
271 html += `</div>`;
272
273 // Top timeline items (max 5)
274 const items = day.timelineItems || [];
275 if (items.length > 0) {
276 html += `<ul class="day-summary-list">`;
277 for (let i = 0; i < Math.min(items.length, 5); i++) {
278 const item = items[i];
279 const time = new Date(item.startTime).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
280 html += `<li class="day-summary-item"><span class="day-summary-time">${esc(time)}</span> ${esc(item.title)}</li>`;
281 }
282 if (items.length > 5) {
283 html += `<li class="day-summary-item day-summary-more">+${items.length - 5} more</li>`;
284 }
285 html += `</ul>`;
286 } else {
287 html += `<p class="day-summary-empty">No scheduled items</p>`;
288 }
289
290 html += `<button class="btn btn-primary day-summary-go-btn" onclick="GoingsOn.monthlyReview.navigateToDay('${escAttr(dateStr)}'); GoingsOn.ui.closeModal();">Go to Day</button>`;
291 html += `</div>`;
292
293 GoingsOn.ui.openModal(dateDisplay, html);
294 } catch {
295 // Fallback on error
296 navigateToDay(dateStr);
297 }
298 }
299
300 // ============ Complete Review ============
301
302 /**
303 * Explicitly save the monthly reflection and mark the review as complete.
304 */
305 async function complete() {
306 const highlightEl = document.getElementById('monthly-highlight');
307 const changeEl = document.getElementById('monthly-change');
308 const highlight = highlightEl?.value?.trim() || '';
309 const change = changeEl?.value?.trim() || '';
310
311 if (!highlight && !change) {
312 GoingsOn.ui.showToast('Write at least one reflection before completing', 'error');
313 return;
314 }
315
316 try {
317 await GoingsOn.api.monthlyReview.saveReflection(currentMonth, highlight, change);
318 GoingsOn.ui.closeModal();
319 GoingsOn.ui.showToast('Monthly review completed!', 'success');
320 await load();
321 } catch (err) {
322 GoingsOn.ui.showToast('Failed to complete review', 'error');
323 }
324 }
325
326 // ============ Exports ============
327
328 GoingsOn.monthlyReview = {
329 load,
330 previousMonth,
331 nextMonth,
332 goToCurrentMonth,
333 addGoal,
334 cycleGoalStatus,
335 deleteGoal,
336 navigateToDay,
337 showDaySummary,
338 complete,
339 openFinishReviewModal,
340 };
341
342 })();
343