Fix project view due date timezone bug and recurring multi-weekday bug (#836)

This commit is contained in:
Chris 2026-02-12 12:12:24 +02:00 committed by GitHub
parent 5a1f0650ae
commit 45aec304a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 10 deletions

View file

@ -42,7 +42,8 @@ const projectsController = {
async getOne(req, res, next) {
try {
const uid = extractUidFromSlug(req.params.uidSlug);
const project = await projectsService.getByUid(uid);
const timezone = req.currentUser?.timezone;
const project = await projectsService.getByUid(uid, timezone);
res.json(project);
} catch (error) {
next(error);

View file

@ -7,6 +7,10 @@ const { NotFoundError, ValidationError } = require('../../shared/errors');
const { validateTagName } = require('../tags/tagsService');
const permissionsService = require('../../services/permissionsService');
const { sortTags } = require('../tasks/core/serializers');
const {
getSafeTimezone,
processDueDateForResponse,
} = require('../../utils/timezone-utils');
const { uid: generateUid } = require('../../utils/uid');
const { extractUidFromSlug } = require('../../utils/slug-utils');
const { logError } = require('../../services/logService');
@ -181,7 +185,7 @@ class ProjectsService {
/**
* Get project by UID.
*/
async getByUid(uid) {
async getByUid(uid, userTimezone) {
const validatedUid = validateUid(uid);
const project =
await projectsRepository.findByUidWithIncludes(validatedUid);
@ -190,6 +194,7 @@ class ProjectsService {
throw new NotFoundError('Project not found');
}
const safeTimezone = getSafeTimezone(userTimezone);
const projectJson = project.toJSON();
const normalizedTasks = projectJson.Tasks
@ -201,11 +206,10 @@ class ProjectsService {
...subtask,
tags: sortTags(subtask.Tags),
})),
due_date: task.due_date
? typeof task.due_date === 'string'
? task.due_date.split('T')[0]
: task.due_date.toISOString().split('T')[0]
: null,
due_date: processDueDateForResponse(
task.due_date,
safeTimezone
),
};
delete normalizedTask.Tags;
delete normalizedTask.Subtasks;

View file

@ -21,7 +21,8 @@ const calculateNextDueDate = (task, fromDate) => {
return calculateWeeklyRecurrence(
startDate,
task.recurrence_interval || 1,
task.recurrence_weekday
task.recurrence_weekday,
task.recurrence_weekdays
);
case 'monthly':
@ -56,10 +57,28 @@ const calculateDailyRecurrence = (fromDate, interval) => {
return nextDate;
};
const calculateWeeklyRecurrence = (fromDate, interval, weekday) => {
const calculateWeeklyRecurrence = (fromDate, interval, weekday, weekdays) => {
const nextDate = new Date(fromDate);
if (weekday !== null && weekday !== undefined) {
// Handle multiple weekdays (e.g. Tuesday AND Thursday)
const parsedWeekdays = weekdays
? Array.isArray(weekdays)
? weekdays
: JSON.parse(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;
}
}
// 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

@ -215,6 +215,116 @@ describe('Recurring Tasks', () => {
);
expect(nextDate.getUTCDay()).toBe(targetWeekday);
});
it('should advance to next selected weekday with multiple weekdays (Issue #715)', 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: 'Tue/Thu Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
recurrence_weekdays: [2, 4], // Tuesday, Thursday
due_date: tuesday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Completing on Tuesday should advance to Thursday (2 days), not next Tuesday (7 days)
const nextDate = calculateNextDueDate(task, tuesday);
expect(nextDate.getUTCDay()).toBe(4); // Thursday
expect(nextDate.toISOString().split('T')[0]).toBe('2026-02-12');
});
it('should wrap around to next week when completing on last selected weekday', async () => {
// Use a fixed Thursday: 2026-02-12
const thursday = new Date(Date.UTC(2026, 1, 12, 0, 0, 0, 0));
expect(thursday.getUTCDay()).toBe(4); // Sanity: Thursday
const task = await Task.create({
name: 'Tue/Thu Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
recurrence_weekdays: [2, 4], // Tuesday, Thursday
due_date: thursday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Completing on Thursday should wrap to next Tuesday (5 days)
const nextDate = calculateNextDueDate(task, thursday);
expect(nextDate.getUTCDay()).toBe(2); // Tuesday
expect(nextDate.toISOString().split('T')[0]).toBe('2026-02-17');
});
it('should advance due date correctly on completion with multiple weekdays via API', async () => {
// Use a fixed Tuesday: 2026-02-10
const tuesday = new Date(Date.UTC(2026, 1, 10, 0, 0, 0, 0));
const task = await Task.create({
name: 'Tue/Thu Via API',
recurrence_type: 'weekly',
recurrence_interval: 1,
recurrence_weekdays: [2, 4], // Tuesday, Thursday
due_date: tuesday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Complete the task on Tuesday
await agent
.patch(`/api/task/${task.uid}`)
.send({ status: Task.STATUS.DONE });
await task.reload();
// Should advance to Thursday, not next Tuesday
expect(task.status).toBe(Task.STATUS.NOT_STARTED);
const newDueDate = new Date(task.due_date);
expect(newDueDate.getUTCDay()).toBe(4); // Thursday
// Complete again on Thursday
await agent
.patch(`/api/task/${task.uid}`)
.send({ status: Task.STATUS.DONE });
await task.reload();
// Should wrap to next Tuesday
expect(task.status).toBe(Task.STATUS.NOT_STARTED);
const nextDueDate = new Date(task.due_date);
expect(nextDueDate.getUTCDay()).toBe(2); // Tuesday
});
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));
expect(monday.getUTCDay()).toBe(1); // Sanity: Monday
const task = await Task.create({
name: 'MWF Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
recurrence_weekdays: [1, 3, 5], // Mon, Wed, Fri
due_date: monday,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Monday -> Wednesday
const next1 = calculateNextDueDate(task, monday);
expect(next1.getUTCDay()).toBe(3);
expect(next1.toISOString().split('T')[0]).toBe('2026-02-11');
// Wednesday -> Friday
const next2 = calculateNextDueDate(task, next1);
expect(next2.getUTCDay()).toBe(5);
expect(next2.toISOString().split('T')[0]).toBe('2026-02-13');
// Friday -> next Monday
const next3 = calculateNextDueDate(task, next2);
expect(next3.getUTCDay()).toBe(1);
expect(next3.toISOString().split('T')[0]).toBe('2026-02-16');
});
});
describe('Monthly Recurrence', () => {