Fix project view due date timezone bug and recurring multi-weekday bug (#836)
This commit is contained in:
parent
5a1f0650ae
commit
45aec304a3
4 changed files with 144 additions and 10 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue