/**
* 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 = `
${!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,
};
})();