Skip to main content

max / goingson

14.5 KB · 361 lines History Blame Raw
1 /**
2 * GoingsOn - Day Planning Schedule & Review Module
3 * Schedule task modal, daily review.
4 */
5
6 (function() {
7 'use strict';
8 const esc = GoingsOn.utils.escapeHtml;
9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 const escArg = GoingsOn.utils.escapeHandlerArg;
11
12 // ============ Schedule Task Modal ============
13
14 /**
15 * Open the schedule task modal with time slot picker and duration presets.
16 * @param {string} id - Task ID to schedule
17 */
18 function openScheduleTaskModal(id) {
19 const now = new Date();
20 const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
21
22 const timeSlots = [];
23 for (let hour = 6; hour <= 21; hour++) {
24 for (let min = 0; min < 60; min += 15) {
25 const slotTime = new Date(today.getTime() + hour * 60 * 60 * 1000 + min * 60 * 1000);
26 if (slotTime > now) {
27 timeSlots.push({
28 time: slotTime,
29 label: slotTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
30 });
31 }
32 }
33 }
34
35 const timeSlotsHtml = timeSlots.slice(0, 12).map(slot => `
36 <button class="btn btn-sm btn-secondary time-block-quick-btn" data-act="dayPlan.selectTimeSlot" data-a1="@el" data-a2="${escAttr(slot.time.toISOString())}">
37 ${slot.label}
38 </button>
39 `).join('');
40
41 const content = `
42 <div class="time-block-form">
43 <div class="form-group">
44 <label class="form-label" id="schedule-quick-label">Quick Select Time</label>
45 <div class="time-block-quick-options" role="group" aria-labelledby="schedule-quick-label">
46 ${timeSlotsHtml}
47 </div>
48 </div>
49
50 <div class="form-group">
51 <label class="form-label" for="schedule-datetime">Or Choose Custom</label>
52 <input type="datetime-local" id="schedule-datetime" class="form-input"
53 min="${now.toISOString().slice(0, 16)}"
54 value="${now.toISOString().slice(0, 16)}">
55 </div>
56
57 <div class="form-group">
58 <label class="form-label" id="schedule-duration-label">Duration</label>
59 <div class="duration-presets" role="group" aria-labelledby="schedule-duration-label">
60 <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 15]'>15m</button>
61 <button class="duration-preset selected" data-act="dayPlan.selectDuration" data-args='["@el", 30]'>30m</button>
62 <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 45]'>45m</button>
63 <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 60]'>1h</button>
64 <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 90]'>1.5h</button>
65 <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 120]'>2h</button>
66 </div>
67 <input type="hidden" id="schedule-duration" value="30">
68 </div>
69
70 <div id="schedule-conflict-warning"></div>
71
72 <div class="form-actions">
73 <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button>
74 <button type="button" class="btn btn-primary" data-act="dayPlan.scheduleTaskFromModal" data-a1="${escAttr(id)}">Schedule Task</button>
75 </div>
76 </div>
77 `;
78
79 GoingsOn.ui.openModal('Schedule Time Block', content);
80 }
81
82 /**
83 * Select a quick time slot and update the datetime input.
84 * @param {HTMLElement} btn - Clicked button element
85 * @param {string} isoTime - ISO 8601 timestamp for the slot
86 */
87 function selectTimeSlot(btn, isoTime) {
88 document.querySelectorAll('.time-block-quick-btn').forEach(b => b.classList.remove('selected'));
89 btn.classList.add('selected');
90 const datetime = new Date(isoTime);
91 document.getElementById('schedule-datetime').value = datetime.toISOString().slice(0, 16);
92 }
93
94 /**
95 * Select a duration preset and update the hidden input.
96 * @param {HTMLElement} btn - Clicked button element
97 * @param {number} minutes - Duration in minutes
98 */
99 function selectDuration(btn, minutes) {
100 document.querySelectorAll('.duration-preset').forEach(b => b.classList.remove('selected'));
101 btn.classList.add('selected');
102 document.getElementById('schedule-duration').value = minutes;
103 }
104
105 /**
106 * Submit the schedule task modal, creating the time block.
107 * @param {string} id - Task ID to schedule
108 */
109 async function scheduleTaskFromModal(id) {
110 const datetimeInput = document.getElementById('schedule-datetime');
111 const durationInput = document.getElementById('schedule-duration');
112
113 if (!datetimeInput.value) {
114 GoingsOn.ui.showToast('Please select a time', 'error');
115 return;
116 }
117
118 const startTime = new Date(datetimeInput.value).toISOString();
119 const duration = parseInt(durationInput.value) || 30;
120
121 try {
122 await GoingsOn.api.dayPlanning.scheduleTask(id, { startTime, duration });
123 GoingsOn.ui.closeModal();
124 GoingsOn.tasks.load();
125
126 const dayPlanView = document.getElementById('day-plan-view');
127 if (dayPlanView && !dayPlanView.classList.contains('hidden')) {
128 GoingsOn.dayPlan.load();
129 }
130
131 const startDisplay = new Date(datetimeInput.value).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
132 const endDisplay = new Date(new Date(datetimeInput.value).getTime() + duration * 60000).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
133 GoingsOn.ui.showToast(`Task scheduled for ${startDisplay} \u2013 ${endDisplay}`, 'success');
134 } catch (err) {
135 GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to schedule task'), 'error');
136 }
137 }
138
139 // ============ Daily Review ============
140
141 const formatDateForApi = GoingsOn.utils.formatDateForApi;
142 const formatDateDisplay = GoingsOn.utils.formatDateDisplay;
143
144 const REFLECTION_PROMPTS = [
145 { key: 'went-well' },
146 { key: 'improve' },
147 ];
148
149 function setupDailyReviewAutoSave() {
150 GoingsOn.planReviewToggle.wireReflectionAutosave({
151 idPrefix: 'daily',
152 prompts: REFLECTION_PROMPTS,
153 onChange: (values) => {
154 const dateStr = formatDateForApi(GoingsOn.state.dayPlanDate);
155 GoingsOn.api.dailyNotes.upsert({
156 noteDate: dateStr,
157 wentWell: values['went-well'],
158 couldImprove: values['improve'],
159 isReviewed: false,
160 }).catch(() => {});
161 },
162 });
163 }
164
165 /**
166 * Render the "Accomplished" list inline in the day plan sidebar,
167 * and refresh nudge state.
168 */
169 async function loadDayReviewPane() {
170 const container = document.getElementById('day-accomplished-inline');
171 if (!container) return;
172
173 const timelineItems = GoingsOn.state.dayPlanData?.timelineItems || [];
174 const completedTasks = timelineItems.filter(item => item.itemType === 'task');
175 const eventCount = timelineItems.filter(item => item.itemType === 'event').length;
176
177 const dateStr = formatDateForApi(GoingsOn.state.dayPlanDate);
178 let isReviewed = false;
179 try {
180 const saved = await GoingsOn.api.dailyNotes.get(dateStr);
181 if (saved) isReviewed = saved.isReviewed || false;
182 } catch (err) {
183 console.error('Failed to load daily note:', err);
184 }
185
186 if (completedTasks.length === 0 && eventCount === 0) {
187 container.innerHTML = '';
188 } else {
189 const statBits = [];
190 if (completedTasks.length > 0) {
191 statBits.push(`${completedTasks.length} task${completedTasks.length === 1 ? '' : 's'}`);
192 }
193 if (eventCount > 0) {
194 statBits.push(`${eventCount} event${eventCount === 1 ? '' : 's'}`);
195 }
196 const statStrip = `<div class="day-accomplished-stats">${statBits.join(' · ')}</div>`;
197
198 const completedList = completedTasks.map(t => `
199 <li class="task-item completed">
200 <span class="task-checkbox checked">&#x2713;</span>
201 <span class="task-text">${esc(t.title)}</span>
202 ${t.projectName ? `<span class="task-project">${esc(t.projectName)}</span>` : ''}
203 </li>
204 `).join('');
205
206 container.innerHTML = `
207 <div class="sidebar-header">
208 <h3>Accomplished</h3>
209 </div>
210 ${statStrip}
211 ${completedTasks.length > 0 ? `<ul class="task-list">${completedList}</ul>` : ''}
212 `;
213 }
214
215 renderFinishReviewBar(dateStr, isReviewed);
216 GoingsOn.planReviewToggle.setStatusBadge(
217 'day-review-status-badge',
218 dayPeriodState(dateStr),
219 isReviewed,
220 );
221
222 const isToday = dateStr === formatDateForApi(new Date());
223 updateDayNudges(isReviewed, isToday);
224 }
225
226 /**
227 * Returns 'past', 'current', or 'future' for the currently viewed date.
228 */
229 function dayPeriodState(dateStr) {
230 const todayStr = formatDateForApi(new Date());
231 if (dateStr === todayStr) return 'current';
232 return dateStr < todayStr ? 'past' : 'future';
233 }
234
235 function renderFinishReviewBar(dateStr, isReviewed) {
236 const bar = document.querySelector('#day-plan-view .finish-review-bar');
237 if (!bar) return;
238 const state = dayPeriodState(dateStr);
239 if (state === 'future') {
240 bar.classList.add('hidden');
241 bar.innerHTML = '';
242 return;
243 }
244 bar.classList.remove('hidden');
245 if (state === 'current') {
246 bar.innerHTML = `
247 <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" data-act="dayPlanSchedule.openFinishReviewModal">
248 Finish &amp; Review
249 </button>
250 `;
251 } else {
252 const label = isReviewed ? 'View Past Review' : 'Review Past Day';
253 bar.innerHTML = `
254 <button class="btn btn-secondary finish-review-btn" id="day-finish-review-btn" data-act="dayPlanSchedule.openFinishReviewModal">
255 ${label}
256 </button>
257 `;
258 }
259 }
260
261 function updateDayNudges(isReviewed, isToday) {
262 const settings = GoingsOn.planReviewToggle.getSettings();
263 if (isToday && settings.reviewNudges && !isReviewed && GoingsOn.planReviewToggle.isAfterWorkHours()) {
264 GoingsOn.planReviewToggle.updateDot('day', true);
265 } else {
266 GoingsOn.planReviewToggle.updateDot('day', false);
267 }
268 }
269
270 /**
271 * Open the end-of-day reflection modal.
272 */
273 async function openFinishReviewModal() {
274 const dateStr = formatDateForApi(GoingsOn.state.dayPlanDate);
275 const displayDate = formatDateDisplay(GoingsOn.state.dayPlanDate);
276
277 let wentWell = '';
278 let improve = '';
279 let isReviewed = false;
280 try {
281 const saved = await GoingsOn.api.dailyNotes.get(dateStr);
282 if (saved) {
283 wentWell = saved.wentWell || '';
284 improve = saved.couldImprove || '';
285 isReviewed = saved.isReviewed || false;
286 }
287 } catch (err) {
288 console.error('Failed to load daily note:', err);
289 }
290
291 const reflectionHtml = GoingsOn.planReviewToggle.renderReflection({
292 idPrefix: 'daily',
293 prompts: [
294 { key: 'went-well', label: 'What went well today?', placeholder: 'Got focused work done in the morning...', value: wentWell },
295 { key: 'improve', label: 'What could be improved?', placeholder: 'Got distracted after lunch...', value: improve },
296 ],
297 });
298
299 const isPast = dayPeriodState(dateStr) === 'past';
300 const banner = isPast
301 ? `<div class="past-review-banner">You are reviewing a past day (${esc(displayDate)}), not today.</div>`
302 : '';
303
304 const content = `
305 <div class="finish-review-modal-content">
306 ${banner}
307 ${reflectionHtml}
308 <div class="review-actions-grid">
309 <button type="button" class="btn btn-primary" data-act="dayPlan.saveDailyReview">
310 ${isReviewed ? 'Update Review' : 'Save Review'}
311 </button>
312 </div>
313 </div>
314 `;
315
316 const title = isPast ? `Reviewing Past: ${displayDate}` : `Wrap Up: ${displayDate}`;
317 GoingsOn.ui.openModal(title, content);
318
319 requestAnimationFrame(() => {
320 setupDailyReviewAutoSave();
321 GoingsOn.planReviewToggle.autoGrowReflection({
322 idPrefix: 'daily',
323 prompts: REFLECTION_PROMPTS,
324 });
325 });
326 }
327
328 async function saveDailyReview() {
329 const dateStr = formatDateForApi(GoingsOn.state.dayPlanDate);
330 const wentWellInput = document.getElementById('daily-went-well');
331 const improveInput = document.getElementById('daily-improve');
332
333 try {
334 await GoingsOn.api.dailyNotes.upsert({
335 noteDate: dateStr,
336 wentWell: wentWellInput?.value?.trim() || '',
337 couldImprove: improveInput?.value?.trim() || '',
338 isReviewed: true,
339 });
340 GoingsOn.ui.closeModal();
341 GoingsOn.ui.showToast('Daily review saved!', 'success');
342 updateDayNudges(true, true);
343 } catch (err) {
344 GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to save daily review'), 'error');
345 }
346 }
347
348 // ============ Populate GoingsOn.dayPlanSchedule Namespace ============
349
350 GoingsOn.dayPlanSchedule = {
351 openScheduleTaskModal,
352 selectTimeSlot,
353 selectDuration,
354 scheduleTaskFromModal,
355 openFinishReviewModal,
356 saveDailyReview,
357 loadDayReviewPane,
358 };
359
360 })();
361