`;
} else if (periodState === 'past') {
const label = hasReflection ? 'View Past Review' : 'Review Past Month';
html += `
`;
}
container.innerHTML = html;
GoingsOn.planReviewToggle.setStatusBadge('month-review-status-badge', periodState, hasReflection);
const settings = GoingsOn.planReviewToggle.getSettings();
const dayOfMonth = new Date().getDate();
if (periodState === 'current' && settings.reviewNudges && dayOfMonth <= 7 && !hasReflection) {
GoingsOn.planReviewToggle.updateDot('month', true);
} else {
GoingsOn.planReviewToggle.updateDot('month', false);
}
}
/**
* Returns 'past', 'current', or 'future' for the currently viewed month.
*/
function currentPeriodState() {
const now = new Date();
const nowStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
if (!currentMonth || currentMonth === nowStr) return 'current';
return currentMonth < nowStr ? 'past' : 'future';
}
/**
* Open the end-of-month reflection modal: prompts + complete action.
*/
function openFinishReviewModal() {
const r = GoingsOn.state.monthlyReview;
if (!r) return;
const R = GoingsOn.monthlyReviewRender;
const isPast = currentPeriodState() === 'past';
const banner = isPast
? `
You are reviewing a past month (${GoingsOn.utils.escapeHtml(r.monthDisplay)}), not the current one.
`
: '';
const content = `
${banner}
${R.renderReflection(r)}
`;
const title = isPast ? `Reviewing Past: ${r.monthDisplay}` : `Wrap Up: ${r.monthDisplay}`;
GoingsOn.ui.openModal(title, content, { large: true });
const prompts = [
{ key: 'highlight' },
{ key: 'change' },
];
GoingsOn.planReviewToggle.wireReflectionAutosave({
idPrefix: 'monthly',
prompts,
onChange: (values) => {
GoingsOn.api.monthlyReview
.saveReflection(r.month, values.highlight, values.change)
.catch(err => console.error('Failed to save reflection:', err));
},
debounceMs: 1000,
});
GoingsOn.planReviewToggle.autoGrowReflection({ idPrefix: 'monthly', prompts });
}
// ============ Goals ============
/**
* Open a form modal to add a monthly goal.
* @param {string} month - YYYY-MM month string
* @param {number} position - Goal slot position (1-3)
*/
function addGoal(month, position) {
GoingsOn.ui.openFormModal({
title: 'Add Goal',
entityType: 'goal',
isEdit: false,
fields: [
{ name: 'text', type: 'text', label: 'Goal', required: true, placeholder: 'What do you want to achieve this month?' },
],
onSubmit: async (data) => {
await GoingsOn.ui.apiCall(
GoingsOn.api.monthlyReview.upsertGoal(month, data.text.trim(), position),
{ successMessage: 'Goal added', reload: load }
);
},
});
}
/**
* Cycle a goal's status: active -> done -> abandoned -> active.
* @param {string} id - Goal ID
*/
async function cycleGoalStatus(id) {
const data = GoingsOn.state.monthlyReview;
if (!data) return;
const goal = data.goals.find(g => g.id === id);
if (!goal) return;
const next = { active: 'done', done: 'abandoned', abandoned: 'active' };
const newStatus = next[goal.status] || 'active';
await GoingsOn.ui.apiCall(
GoingsOn.api.monthlyReview.updateGoalStatus(id, newStatus),
{ reload: load }
);
}
/**
* Delete a monthly goal after confirmation.
* @param {string} id - Goal ID
*/
async function deleteGoal(id) {
const confirmed = await GoingsOn.ui.confirmDelete('Delete this goal?');
if (!confirmed) return;
await GoingsOn.ui.apiCall(
GoingsOn.api.monthlyReview.deleteGoal(id),
{ successMessage: 'Goal deleted', reload: load }
);
}
// ============ Day Navigation ============
/**
* Navigate to the day plan view for a specific date.
* @param {string} dateStr - YYYY-MM-DD date string
*/
function navigateToDay(dateStr) {
GoingsOn.state.set('dayPlanDate', new Date(dateStr + 'T12:00:00'));
GoingsOn.navigation.switchView('day-plan');
}
/**
* Show a day summary popup (touch) or navigate to the day (desktop).
* @param {string} dateStr - YYYY-MM-DD date string
*/
async function showDaySummary(dateStr) {
// On non-touch devices, navigate directly
if (!GoingsOn.touch?.isTouchDevice) {
navigateToDay(dateStr);
return;
}
try {
const day = await GoingsOn.api.dayPlanning.getDay(dateStr);
const esc = GoingsOn.utils.escapeHtml;
const escAttr = GoingsOn.utils.escapeAttr;
const dateDisplay = new Date(dateStr + 'T12:00:00')
.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
// Build stat chips
const scheduled = day.timelineItems ? day.timelineItems.length : 0;
const unscheduled = day.unscheduledTasks ? day.unscheduledTasks.length : 0;
let html = `
`;
html += `
${esc(dateDisplay)}
`;
html += `
`;
html += `${scheduled} scheduled`;
html += `${unscheduled} unscheduled`;
html += `
`;
// Top timeline items (max 5)
const items = day.timelineItems || [];
if (items.length > 0) {
html += `
`;
for (let i = 0; i < Math.min(items.length, 5); i++) {
const item = items[i];
const time = new Date(item.startTime).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
html += `
${esc(time)} ${esc(item.title)}
`;
}
if (items.length > 5) {
html += `
+${items.length - 5} more
`;
}
html += `
`;
} else {
html += `
No scheduled items
`;
}
html += ``;
html += `
`;
GoingsOn.ui.openModal(dateDisplay, html);
} catch {
// Fallback on error
navigateToDay(dateStr);
}
}
// ============ Complete Review ============
/**
* Explicitly save the monthly reflection and mark the review as complete.
*/
async function complete() {
const highlightEl = document.getElementById('monthly-highlight');
const changeEl = document.getElementById('monthly-change');
const highlight = highlightEl?.value?.trim() || '';
const change = changeEl?.value?.trim() || '';
if (!highlight && !change) {
GoingsOn.ui.showToast('Write at least one reflection before completing', 'error');
return;
}
try {
await GoingsOn.api.monthlyReview.saveReflection(currentMonth, highlight, change);
GoingsOn.ui.closeModal();
GoingsOn.ui.showToast('Monthly review completed!', 'success');
await load();
} catch (err) {
GoingsOn.ui.showToast('Failed to complete review', 'error');
}
}
// ============ Exports ============
GoingsOn.monthlyReview = {
load,
previousMonth,
nextMonth,
goToCurrentMonth,
addGoal,
cycleGoalStatus,
deleteGoal,
navigateToDay,
showDaySummary,
complete,
openFinishReviewModal,
};
})();