Fix recurrence occurences (#750)

This commit is contained in:
Chris 2025-12-29 12:13:08 +02:00 committed by GitHub
parent 703f6fe506
commit b4de9c23eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 74 deletions

View file

@ -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 {

View file

@ -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;

View file

@ -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);

View file

@ -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);
}