diff --git a/backend/routes/tasks/operations/recurring.js b/backend/routes/tasks/operations/recurring.js index 956caf5..c21085b 100644 --- a/backend/routes/tasks/operations/recurring.js +++ b/backend/routes/tasks/operations/recurring.js @@ -87,7 +87,7 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { const weekdays = Array.isArray(task.recurrence_weekdays) ? task.recurrence_weekdays : JSON.parse(task.recurrence_weekdays); - const todayWeekday = nextDate.getDay(); + const todayWeekday = nextDate.getUTCDay(); console.log('Weekly recurrence check:', { weekdays, todayWeekday, @@ -98,7 +98,7 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { task.recurrence_weekday !== null && task.recurrence_weekday !== undefined ) { - const todayWeekday = nextDate.getDay(); + const todayWeekday = nextDate.getUTCDay(); includesToday = task.recurrence_weekday === todayWeekday; } } else if (task.recurrence_type === 'daily') { @@ -145,8 +145,8 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { // If today doesn't match, calculate the next occurrence if (!includesToday) { if (task.recurrence_type === 'daily') { - nextDate.setDate( - nextDate.getDate() + (task.recurrence_interval || 1) + nextDate.setUTCDate( + nextDate.getUTCDate() + (task.recurrence_interval || 1) ); } else if (task.recurrence_type === 'weekly') { const interval = task.recurrence_interval || 1; @@ -154,16 +154,18 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { task.recurrence_weekday !== null && task.recurrence_weekday !== undefined ) { - const currentWeekday = nextDate.getDay(); + const currentWeekday = nextDate.getUTCDay(); const daysUntilTarget = (task.recurrence_weekday - currentWeekday + 7) % 7; if (daysUntilTarget === 0) { - nextDate.setDate(nextDate.getDate() + interval * 7); + nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7); } else { - nextDate.setDate(nextDate.getDate() + daysUntilTarget); + nextDate.setUTCDate( + nextDate.getUTCDate() + daysUntilTarget + ); } } else { - nextDate.setDate(nextDate.getDate() + interval * 7); + nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7); } } else { nextDate = calculateNextDueDate(task, startDate); @@ -188,8 +190,8 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { if (task.recurrence_type === 'daily') { nextDate = new Date(nextDate); - nextDate.setDate( - nextDate.getDate() + (task.recurrence_interval || 1) + nextDate.setUTCDate( + nextDate.getUTCDate() + (task.recurrence_interval || 1) ); } else if (task.recurrence_type === 'weekly') { nextDate = new Date(nextDate); @@ -200,14 +202,13 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { const weekdays = Array.isArray(task.recurrence_weekdays) ? task.recurrence_weekdays : JSON.parse(task.recurrence_weekdays); - const currentWeekday = nextDate.getDay(); // Find next matching weekday let found = false; for (let daysAhead = 1; daysAhead <= 7; daysAhead++) { const testDate = new Date(nextDate); - testDate.setDate(testDate.getDate() + daysAhead); - const testWeekday = testDate.getDay(); + testDate.setUTCDate(testDate.getUTCDate() + daysAhead); + const testWeekday = testDate.getUTCDay(); if (weekdays.includes(testWeekday)) { nextDate = testDate; @@ -218,12 +219,12 @@ async function calculateNextIterations(task, startFromDate, userTimezone) { if (!found) { // Fallback: add 7 days - nextDate.setDate(nextDate.getDate() + 7); + nextDate.setUTCDate(nextDate.getUTCDate() + 7); } } else { // Old behavior for single weekday - nextDate.setDate( - nextDate.getDate() + (task.recurrence_interval || 1) * 7 + nextDate.setUTCDate( + nextDate.getUTCDate() + (task.recurrence_interval || 1) * 7 ); } } else { diff --git a/backend/services/recurringTaskService.js b/backend/services/recurringTaskService.js index 1203f69..ec88861 100644 --- a/backend/services/recurringTaskService.js +++ b/backend/services/recurringTaskService.js @@ -52,7 +52,7 @@ const calculateNextDueDate = (task, fromDate) => { const calculateDailyRecurrence = (fromDate, interval) => { const nextDate = new Date(fromDate); - nextDate.setDate(nextDate.getDate() + interval); + nextDate.setUTCDate(nextDate.getUTCDate() + interval); return nextDate; }; @@ -60,22 +60,22 @@ const calculateWeeklyRecurrence = (fromDate, interval, weekday) => { const nextDate = new Date(fromDate); if (weekday !== null && weekday !== undefined) { - const currentWeekday = nextDate.getDay(); + const currentWeekday = nextDate.getUTCDay(); const daysUntilTarget = (weekday - currentWeekday + 7) % 7; if ( daysUntilTarget === 0 && nextDate.getTime() === fromDate.getTime() ) { - nextDate.setDate(nextDate.getDate() + interval * 7); + nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7); } else { - nextDate.setDate(nextDate.getDate() + daysUntilTarget); + nextDate.setUTCDate(nextDate.getUTCDate() + daysUntilTarget); if (nextDate <= fromDate) { - nextDate.setDate(nextDate.getDate() + interval * 7); + nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7); } } } else { - nextDate.setDate(nextDate.getDate() + interval * 7); + nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7); } return nextDate; diff --git a/backend/tests/integration/recurring-tasks.test.js b/backend/tests/integration/recurring-tasks.test.js index 76a3f84..2602401 100644 --- a/backend/tests/integration/recurring-tasks.test.js +++ b/backend/tests/integration/recurring-tasks.test.js @@ -24,7 +24,7 @@ describe('Recurring Tasks', () => { describe('Daily Recurrence', () => { it('should set correct due date for daily recurring task', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); @@ -51,7 +51,7 @@ describe('Recurring Tasks', () => { it('should handle daily recurrence with interval of 2', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const taskData = { name: 'Every Other Day Task', @@ -73,7 +73,7 @@ describe('Recurring Tasks', () => { new Date(task.due_date) ); const expectedDate = new Date(today); - expectedDate.setDate(expectedDate.getDate() + 2); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 2); expect(nextDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -82,7 +82,7 @@ describe('Recurring Tasks', () => { it('should handle daily recurrence with interval of 7', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const taskData = { name: 'Weekly via Daily', @@ -101,7 +101,7 @@ describe('Recurring Tasks', () => { new Date(task.due_date) ); const expectedDate = new Date(today); - expectedDate.setDate(expectedDate.getDate() + 7); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 7); expect(nextDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -112,8 +112,8 @@ describe('Recurring Tasks', () => { describe('Weekly Recurrence', () => { it('should set correct due date for weekly recurring task', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayWeekday = today.getDay(); + today.setUTCHours(0, 0, 0, 0); + const todayWeekday = today.getUTCDay(); const taskData = { name: 'Weekly Meeting', @@ -135,8 +135,8 @@ describe('Recurring Tasks', () => { it('should calculate next week for weekly recurrence', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayWeekday = today.getDay(); + today.setUTCHours(0, 0, 0, 0); + const todayWeekday = today.getUTCDay(); const taskData = { name: 'Weekly Review', @@ -154,7 +154,7 @@ describe('Recurring Tasks', () => { new Date(task.due_date) ); const expectedDate = new Date(today); - expectedDate.setDate(expectedDate.getDate() + 7); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 7); expect(nextDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -163,8 +163,8 @@ describe('Recurring Tasks', () => { it('should handle bi-weekly recurrence', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayWeekday = today.getDay(); + today.setUTCHours(0, 0, 0, 0); + const todayWeekday = today.getUTCDay(); const taskData = { name: 'Bi-weekly Task', @@ -182,7 +182,7 @@ describe('Recurring Tasks', () => { new Date(task.due_date) ); const expectedDate = new Date(today); - expectedDate.setDate(expectedDate.getDate() + 14); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 14); expect(nextDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -191,8 +191,8 @@ describe('Recurring Tasks', () => { it('should handle weekly recurrence on a different weekday', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayWeekday = today.getDay(); + today.setUTCHours(0, 0, 0, 0); + const todayWeekday = today.getUTCDay(); // Target a different weekday (e.g., if today is Monday (1), target Friday (5)) const targetWeekday = (todayWeekday + 4) % 7; @@ -211,7 +211,7 @@ describe('Recurring Tasks', () => { task, new Date(task.due_date) ); - expect(nextDate.getDay()).toBe(targetWeekday); + expect(nextDate.getUTCDay()).toBe(targetWeekday); }); }); @@ -413,7 +413,7 @@ describe('Recurring Tasks', () => { describe('Due Date Refresh on Completion', () => { it('should advance due date when daily recurring task is completed', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); // Create a daily recurring task const task = await Task.create({ @@ -444,7 +444,7 @@ describe('Recurring Tasks', () => { // Due date should be advanced by 1 day const newDueDate = new Date(task.due_date); const expectedDate = new Date(originalDueDate); - expectedDate.setDate(expectedDate.getDate() + 1); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 1); expect(newDueDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -460,8 +460,8 @@ describe('Recurring Tasks', () => { it('should advance due date when weekly recurring task is completed', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayWeekday = today.getDay(); + today.setUTCHours(0, 0, 0, 0); + const todayWeekday = today.getUTCDay(); const task = await Task.create({ name: 'Weekly Task', @@ -485,7 +485,7 @@ describe('Recurring Tasks', () => { // Due date should be advanced by 1 week const newDueDate = new Date(task.due_date); const expectedDate = new Date(originalDueDate); - expectedDate.setDate(expectedDate.getDate() + 7); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 7); expect(newDueDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -536,7 +536,7 @@ describe('Recurring Tasks', () => { it('should track multiple completions in RecurringCompletion table', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Daily Task', @@ -639,7 +639,7 @@ describe('Recurring Tasks', () => { // Next due date should be 2 days ago (original due date + 1 day) const newDueDate = new Date(task.due_date); const expectedDate = new Date(originalDueDate); - expectedDate.setDate(expectedDate.getDate() + 1); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 1); expect(newDueDate.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] @@ -767,7 +767,7 @@ describe('Recurring Tasks', () => { it('should handle interval > 1 with completion_based', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Every 3 Days Completion Based', @@ -799,7 +799,7 @@ describe('Recurring Tasks', () => { it('should respect updated completion_based flag', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); // Create task with completion_based = false const task = await Task.create({ @@ -840,7 +840,7 @@ describe('Recurring Tasks', () => { it('should handle multiple rapid completions with completion_based', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Rapid Completions', @@ -901,7 +901,7 @@ describe('Recurring Tasks', () => { describe('Recurrence End Date', () => { it('should stop recurring when end date is reached', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); const dayAfterTomorrow = new Date(today); @@ -963,7 +963,7 @@ describe('Recurring Tasks', () => { it('should allow recurring tasks without end date to continue indefinitely', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Infinite Task', @@ -995,7 +995,7 @@ describe('Recurring Tasks', () => { describe('Edge Cases', () => { it('should handle completing a task multiple times in quick succession', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Quick Complete Task', @@ -1029,7 +1029,7 @@ describe('Recurring Tasks', () => { // Second due date should be one day after first const expectedDate = new Date(dueDateAfterFirst); - expectedDate.setDate(expectedDate.getDate() + 1); + expectedDate.setUTCDate(expectedDate.getUTCDate() + 1); expect(dueDateAfterSecond.toISOString().split('T')[0]).toBe( expectedDate.toISOString().split('T')[0] ); @@ -1037,7 +1037,7 @@ describe('Recurring Tasks', () => { it('should handle uncompleting a recurring task', async () => { const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const task = await Task.create({ name: 'Uncomplete Task', @@ -1124,7 +1124,7 @@ describe('Recurring Tasks', () => { const dueDate = new Date(task.due_date); const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); diff --git a/frontend/components/Task/TaskDetails.tsx b/frontend/components/Task/TaskDetails.tsx index 8b25f48..6c20627 100644 --- a/frontend/components/Task/TaskDetails.tsx +++ b/frontend/components/Task/TaskDetails.tsx @@ -488,12 +488,9 @@ const TaskDetails: React.FC = () => { ) { try { setLoadingIterations(true); - const startFromDate = task.due_date - ? task.due_date.split('T')[0] - : undefined; + // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( - task.uid!, - startFromDate + task.uid! ); setNextIterations(iterations); } catch (error) { @@ -511,12 +508,9 @@ const TaskDetails: React.FC = () => { try { setLoadingIterations(true); - const startFromDate = task.due_date - ? task.due_date.split('T')[0] - : undefined; + // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( - parentTask.uid, - startFromDate + parentTask.uid ); setNextIterations(iterations); @@ -721,21 +715,15 @@ const TaskDetails: React.FC = () => { try { setLoadingIterations(true); if (isTemplateTask) { - const startFromDate = latestTask.due_date - ? latestTask.due_date.split('T')[0] - : undefined; + // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( - latestTask.uid!, - startFromDate + latestTask.uid! ); setNextIterations(iterations); } else if (canUseParentIterations && parentTask?.uid) { - const startFromDate = latestTask.due_date - ? latestTask.due_date.split('T')[0] - : undefined; + // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( - parentTask.uid, - startFromDate + parentTask.uid ); setNextIterations(iterations); }