/** * GoingsOn - Weekly Review Module (V2) * * A guided weekly review workflow with grid-based layout. * All data computation is done in Rust; this module only handles rendering. * Section renderers are in weekly-review-render.js. */ (function() { 'use strict'; // ============ Navigation ============ // `null` means "current week" — let the backend resolve it. Set to a Monday // YYYY-MM-DD string when viewing past/future weeks. let currentWeekStart = null; function shiftWeek(deltaDays) { const base = currentWeekStart ? new Date(currentWeekStart + 'T12:00:00') : new Date(); // Snap to Monday of the resulting week const result = new Date(base); result.setDate(result.getDate() + deltaDays); const dayIdx = (result.getDay() + 6) % 7; // 0 = Monday result.setDate(result.getDate() - dayIdx); const y = result.getFullYear(); const m = String(result.getMonth() + 1).padStart(2, '0'); const d = String(result.getDate()).padStart(2, '0'); currentWeekStart = `${y}-${m}-${d}`; load(); } function previousWeek() { shiftWeek(-7); } function nextWeek() { shiftWeek(7); } function goToCurrentWeek() { currentWeekStart = null; load(); } /** * Returns 'past', 'current', or 'future' for the currently viewed week. * Compares against today's Monday. */ function currentPeriodState() { if (currentWeekStart === null) return 'current'; const today = new Date(); const dayIdx = (today.getDay() + 6) % 7; const monday = new Date(today); monday.setDate(today.getDate() - dayIdx); const y = monday.getFullYear(); const m = String(monday.getMonth() + 1).padStart(2, '0'); const d = String(monday.getDate()).padStart(2, '0'); const todayMonday = `${y}-${m}-${d}`; if (currentWeekStart === todayMonday) return 'current'; return currentWeekStart < todayMonday ? 'past' : 'future'; } // ============ Auto-Save ============ const DRAFT_STORAGE_KEY = 'weekly-review-draft'; function getDraft() { try { return JSON.parse(localStorage.getItem(DRAFT_STORAGE_KEY) || '{}'); } catch { return {}; } } const REFLECTION_PROMPTS = [ { key: 'went-well' }, { key: 'improve' }, ]; function setupAutoSave() { GoingsOn.planReviewToggle.wireReflectionAutosave({ idPrefix: 'weekly', prompts: REFLECTION_PROMPTS, onChange: (values) => { const draft = { wentWell: values['went-well'], improve: values['improve'], savedAt: Date.now(), weekStart: GoingsOn.state.weeklyReview?.weekStart || null, }; localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft)); }, }); } function clearDraft() { localStorage.removeItem(DRAFT_STORAGE_KEY); } // ============ Load Functions ============ async function load() { try { GoingsOn.state.set('weeklyReview', await GoingsOn.api.weeklyReview.get(currentWeekStart)); render(); } catch (err) { console.error('Failed to load weekly review:', err); showError('Failed to load weekly review data'); GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load weekly review'), 'error', { action: { label: 'Retry', fn: load }, duration: 8000, }); } } // ============ Render Functions ============ function render() { const container = document.getElementById('weekly-review-content'); if (!container || !GoingsOn.state.weeklyReview) return; const r = GoingsOn.state.weeklyReview; const esc = GoingsOn.utils.escapeHtml; const wr = GoingsOn.weeklyReviewRender; container.innerHTML = `
${esc(r.weekDisplay)}
${!r.isCompleted ? '

Plan your week, then close it out with Finish & Review.

' : ''}
${wr.renderWeekTimeline(r)} ${wr.renderVacationToggles(r)} ${wr.renderFocusSection(r)} ${wr.renderAccomplished(r)} ${wr.renderNeedsAttention(r)} ${wr.renderDueThisWeek(r)} ${wr.renderProjectsHealth(r)}
${renderFinishReviewBar(r)} `; const periodState = currentPeriodState(); GoingsOn.planReviewToggle.setStatusBadge('week-review-status-badge', periodState, r.isCompleted); const settings = GoingsOn.planReviewToggle.getSettings(); const isMonday = new Date().getDay() === 1; const isCurrentWeek = periodState === 'current'; if (isCurrentWeek && settings.reviewNudges && isMonday && !r.isCompleted) { GoingsOn.planReviewToggle.updateDot('week', true); } else { GoingsOn.planReviewToggle.updateDot('week', false); } } function renderFinishReviewBar(r) { const state = currentPeriodState(); if (state === 'future') return ''; if (state === 'current') { return `
`; } return `
`; } /** * Open the end-of-week reflection modal: timeline events recap + reflection prompts. */ function openFinishReviewModal() { const r = GoingsOn.state.weeklyReview; if (!r) return; const wr = GoingsOn.weeklyReviewRender; const isPast = currentPeriodState() === 'past'; const esc = GoingsOn.utils.escapeHtml; const banner = isPast ? `
You are reviewing a past week (${esc(r.weekDisplay)}), not the current one.
` : ''; const content = `
${banner} ${wr.renderTimelineEvents(r)} ${wr.renderReflection(r, getDraft)}
`; const title = isPast ? `Reviewing Past: ${r.weekDisplay}` : `Wrap Up: ${r.weekDisplay}`; GoingsOn.ui.openModal(title, content, { large: true }); setupAutoSave(); GoingsOn.planReviewToggle.autoGrowReflection({ idPrefix: 'weekly', prompts: REFLECTION_PROMPTS, }); } // ============ Helpers ============ function showError(message) { const container = document.getElementById('weekly-review-content'); if (container) { GoingsOn.utils.showError(container, message); } } // ============ Vacation Toggles ============ /** * Toggle a day as vacation/non-vacation in the weekly review. * @param {number} dayIndex - Day of week index (0 = Monday, 6 = Sunday) */ async function toggleVacationDay(dayIndex) { if (!GoingsOn.state.weeklyReview) return; const current = GoingsOn.state.weeklyReview.vacationDays || []; let updated; if (current.includes(dayIndex)) { updated = current.filter(d => d !== dayIndex); } else { updated = [...current, dayIndex].sort(); } try { await GoingsOn.api.weeklyReview.setVacationDays(updated, currentWeekStart); await load(); } catch (err) { console.error('Failed to set vacation days:', err); GoingsOn.ui.showToast('Failed to update vacation days', 'error'); } } // ============ Keyboard Navigation ============ /** * Handle keyboard navigation within focus slots. * @param {KeyboardEvent} event * @param {string|null} taskId - Task ID in this slot, or null if empty * @param {number} slotIndex - Index of the focus slot (0-2) */ function handleSlotKeydown(event, taskId, slotIndex) { const slots = document.querySelectorAll('.focus-slot'); switch (event.key) { case 'ArrowRight': case 'ArrowDown': event.preventDefault(); const nextSlot = slots[Math.min(slotIndex + 1, slots.length - 1)]; if (nextSlot) nextSlot.focus(); break; case 'ArrowLeft': case 'ArrowUp': event.preventDefault(); const prevSlot = slots[Math.max(slotIndex - 1, 0)]; if (prevSlot) prevSlot.focus(); break; case 'Delete': case 'Backspace': event.preventDefault(); if (taskId) { toggleFocus(taskId, false); } break; case 'Enter': case ' ': event.preventDefault(); if (taskId) { // If slot has a task, remove it toggleFocus(taskId, false); } else { // If slot is empty, focus the first suggested task button const firstSuggestion = document.querySelector('.focus-section .btn.btn-secondary'); if (firstSuggestion) { firstSuggestion.focus(); } } break; } } // ============ Actions ============ /** * Set or unset a task as a weekly focus priority. * @param {string} taskId - Task ID * @param {boolean} isFocus - true to add focus, false to remove */ async function toggleFocus(taskId, isFocus) { try { await GoingsOn.api.weeklyReview.setFocus(taskId, isFocus); await load(); // Reload to get updated data } catch (err) { console.error('Failed to toggle focus:', err); GoingsOn.ui.showToast('Failed to update focus', 'error'); } } /** * Remove focus from all tasks after confirmation. */ async function clearAllFocus() { const confirmed = await GoingsOn.ui.confirmDelete('Clear focus from all tasks?'); if (!confirmed) return; try { await GoingsOn.api.weeklyReview.clearAllFocus(); await load(); GoingsOn.ui.showToast('Focus cleared', 'success'); } catch (err) { console.error('Failed to clear focus:', err); GoingsOn.ui.showToast('Failed to clear focus', 'error'); } } /** * Complete the weekly review, saving reflection notes to the backend. */ async function complete() { // Build structured notes from reflection prompts const wentWellInput = document.getElementById('weekly-went-well'); const improveInput = document.getElementById('weekly-improve'); let notes = ''; if (wentWellInput && wentWellInput.value.trim()) { notes += 'What went well:\n' + wentWellInput.value.trim() + '\n\n'; } if (improveInput && improveInput.value.trim()) { notes += 'What could be improved:\n' + improveInput.value.trim(); } notes = notes.trim(); try { await GoingsOn.api.weeklyReview.complete(notes, currentWeekStart); clearDraft(); GoingsOn.ui.closeModal(); await load(); GoingsOn.ui.showToast('Weekly review completed!', 'success'); updateBadge(false); } catch (err) { console.error('Failed to complete review:', err); GoingsOn.ui.showToast('Failed to complete review', 'error'); } } // ============ Badge / Nudge ============ /** * Show or hide the review-pending badge on the Time tab. * @param {boolean} showBadge - true to show, false to hide */ function updateBadge(showBadge) { // Badge goes on the Time tab in the top nav const tab = document.querySelector('.tab-navigation [data-view="time"]'); if (tab) { const existingBadge = tab.querySelector('.tab-badge'); if (showBadge && !existingBadge) { const badge = document.createElement('span'); badge.className = 'tab-badge'; badge.setAttribute('aria-label', 'Review pending'); tab.appendChild(badge); } else if (!showBadge && existingBadge) { existingBadge.remove(); } } // Also update the mobile tab bar const mobileTab = document.querySelector('.mobile-tab-bar [data-view="time"]'); if (mobileTab) { const existing = mobileTab.querySelector('.tab-badge'); if (showBadge && !existing) { const badge = document.createElement('span'); badge.className = 'tab-badge'; badge.setAttribute('aria-label', 'Review pending'); mobileTab.appendChild(badge); } else if (!showBadge && existing) { existing.remove(); } } } /** * Check if the user should be nudged to do their weekly review. */ async function checkNudge() { try { const showNudge = await GoingsOn.api.weeklyReview.checkNudge(); updateBadge(showNudge); if (showNudge) { GoingsOn.ui.showToast('Time for your weekly review!', 'info'); } } catch (err) { console.error('Failed to check weekly review nudge:', err); } } // ============ Populate Namespace ============ GoingsOn.weeklyReview = { load, render, previousWeek, nextWeek, goToCurrentWeek, openFinishReviewModal, toggleFocus, clearAllFocus, toggleVacationDay, complete, checkNudge, updateBadge, handleSlotKeydown, }; })();