Skip to main content

max / goingson

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