Fix bi-weekly+ recurring tasks reverting to weekly (#844) (#890)

This commit is contained in:
Chris 2026-03-02 23:36:47 +02:00 committed by GitHub
parent b81abd9bfe
commit c656c2aa67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 108 additions and 39 deletions

View file

@ -152,18 +152,21 @@ async function calculateNextIterations(task, startFromDate, userTimezone) {
const weekdays = Array.isArray(task.recurrence_weekdays)
? task.recurrence_weekdays
: JSON.parse(task.recurrence_weekdays);
let found = false;
for (let daysAhead = 1; daysAhead <= 7; daysAhead++) {
const testDate = new Date(nextDate);
testDate.setUTCDate(testDate.getUTCDate() + daysAhead);
if (weekdays.includes(testDate.getUTCDay())) {
nextDate = testDate;
found = true;
break;
}
}
if (!found) {
nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7);
const sorted = [...weekdays].sort((a, b) => a - b);
const currentDay = nextDate.getUTCDay();
const laterInWeek = sorted.filter((d) => d > currentDay);
if (laterInWeek.length > 0) {
nextDate.setUTCDate(
nextDate.getUTCDate() + (laterInWeek[0] - currentDay)
);
} else {
const daysToNextFirst =
(7 - currentDay + sorted[0]) % 7 || 7;
nextDate.setUTCDate(
nextDate.getUTCDate() +
daysToNextFirst +
(interval - 1) * 7
);
}
} else if (
task.recurrence_weekday !== null &&
@ -213,28 +216,26 @@ async function calculateNextIterations(task, startFromDate, userTimezone) {
// Handle multiple weekdays
if (task.recurrence_weekdays) {
// Sequelize getter already parses JSON, so it's already an array
const weekdays = Array.isArray(task.recurrence_weekdays)
? task.recurrence_weekdays
: JSON.parse(task.recurrence_weekdays);
const interval = task.recurrence_interval || 1;
const sorted = [...weekdays].sort((a, b) => a - b);
const currentDay = nextDate.getUTCDay();
const laterInWeek = sorted.filter((d) => d > currentDay);
// Find next matching weekday
let found = false;
for (let daysAhead = 1; daysAhead <= 7; daysAhead++) {
const testDate = new Date(nextDate);
testDate.setUTCDate(testDate.getUTCDate() + daysAhead);
const testWeekday = testDate.getUTCDay();
if (weekdays.includes(testWeekday)) {
nextDate = testDate;
found = true;
break;
}
}
if (!found) {
// Fallback: add 7 days
nextDate.setUTCDate(nextDate.getUTCDate() + 7);
if (laterInWeek.length > 0) {
nextDate.setUTCDate(
nextDate.getUTCDate() + (laterInWeek[0] - currentDay)
);
} else {
const daysToNextFirst =
(7 - currentDay + sorted[0]) % 7 || 7;
nextDate.setUTCDate(
nextDate.getUTCDate() +
daysToNextFirst +
(interval - 1) * 7
);
}
} else {
// Old behavior for single weekday

View file

@ -68,16 +68,22 @@ const calculateWeeklyRecurrence = (fromDate, interval, weekday, weekdays) => {
: null;
if (parsedWeekdays && parsedWeekdays.length > 0) {
// Find the next matching weekday from tomorrow onward
for (let daysAhead = 1; daysAhead <= 7; daysAhead++) {
const testDate = new Date(nextDate);
testDate.setUTCDate(testDate.getUTCDate() + daysAhead);
if (parsedWeekdays.includes(testDate.getUTCDay())) {
return testDate;
}
const sorted = [...parsedWeekdays].sort((a, b) => a - b);
const currentDay = nextDate.getUTCDay();
// Find next weekday later in the current week
const laterInWeek = sorted.filter((d) => d > currentDay);
if (laterInWeek.length > 0) {
nextDate.setUTCDate(
nextDate.getUTCDate() + (laterInWeek[0] - currentDay)
);
} else {
// Wrap to first weekday of next cycle (interval weeks ahead)
const daysToNextFirst = (7 - currentDay + sorted[0]) % 7 || 7;
nextDate.setUTCDate(
nextDate.getUTCDate() + daysToNextFirst + (interval - 1) * 7
);
}
// Fallback: advance by interval weeks
nextDate.setUTCDate(nextDate.getUTCDate() + interval * 7);
} else if (weekday !== null && weekday !== undefined) {
const currentWeekday = nextDate.getUTCDay();
const daysUntilTarget = (weekday - currentWeekday + 7) % 7;

View file

@ -295,6 +295,68 @@ describe('Recurring Tasks', () => {
expect(nextDueDate.getUTCDay()).toBe(2); // Tuesday
});
it('should respect bi-weekly interval with multiple weekdays (Issue #844)', async () => {
// Use a fixed Tuesday: 2026-02-10
const tuesday = new Date(Date.UTC(2026, 1, 10, 0, 0, 0, 0));
expect(tuesday.getUTCDay()).toBe(2); // Sanity: Tuesday
const task = await Task.create({
name: 'Bi-weekly Tue/Thu Task',
recurrence_type: 'weekly',
recurrence_interval: 2,
recurrence_weekdays: [2, 4], // Tuesday, Thursday
due_date: tuesday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Tuesday -> Thursday (same cycle week)
const next1 = calculateNextDueDate(task, tuesday);
expect(next1.getUTCDay()).toBe(4); // Thursday
expect(next1.toISOString().split('T')[0]).toBe('2026-02-12');
// Thursday -> next cycle Tuesday (skip 1 week, land on week 3's Tuesday)
const next2 = calculateNextDueDate(task, next1);
expect(next2.getUTCDay()).toBe(2); // Tuesday
expect(next2.toISOString().split('T')[0]).toBe('2026-02-24');
// Tuesday (week 3) -> Thursday (week 3, same cycle)
const next3 = calculateNextDueDate(task, next2);
expect(next3.getUTCDay()).toBe(4); // Thursday
expect(next3.toISOString().split('T')[0]).toBe('2026-02-26');
// Thursday (week 3) -> Tuesday (week 5)
const next4 = calculateNextDueDate(task, next3);
expect(next4.getUTCDay()).toBe(2); // Tuesday
expect(next4.toISOString().split('T')[0]).toBe('2026-03-10');
});
it('should respect tri-weekly interval with single weekday via weekdays array (Issue #844)', async () => {
// Use a fixed Monday: 2026-02-09
const monday = new Date(Date.UTC(2026, 1, 9, 0, 0, 0, 0));
expect(monday.getUTCDay()).toBe(1);
const task = await Task.create({
name: 'Every 3 weeks on Monday',
recurrence_type: 'weekly',
recurrence_interval: 3,
recurrence_weekdays: [1], // Monday only
due_date: monday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Monday Feb 9 -> Monday Mar 2 (3 weeks later)
const next1 = calculateNextDueDate(task, monday);
expect(next1.getUTCDay()).toBe(1);
expect(next1.toISOString().split('T')[0]).toBe('2026-03-02');
// Monday Mar 2 -> Monday Mar 23 (3 weeks later)
const next2 = calculateNextDueDate(task, next1);
expect(next2.getUTCDay()).toBe(1);
expect(next2.toISOString().split('T')[0]).toBe('2026-03-23');
});
it('should handle three weekdays (Mon/Wed/Fri)', async () => {
// Use a fixed Monday: 2026-02-09
const monday = new Date(Date.UTC(2026, 1, 9, 0, 0, 0, 0));