Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion backend/modules/tasks/queries/metrics-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ async function countTasksPendingOverMonth(visibleTasksWhere) {
}

async function fetchTasksInProgress(visibleTasksWhere) {
const now = new Date();
return await Task.findAll({
where: {
...visibleTasksWhere,
status: { [Op.in]: [Task.STATUS.IN_PROGRESS, 'in_progress'] },
// Exclude tasks deferred to the future
[Op.or]: [
{ defer_until: null },
{ defer_until: { [Op.lte]: now } },
],
parent_task_id: null,
recurring_parent_id: null,
},
Expand All @@ -67,6 +73,7 @@ async function fetchTasksInProgress(visibleTasksWhere) {
}

async function fetchTodayPlanTasks(visibleTasksWhere) {
const now = new Date();
const todayPlanStatuses = [
Task.STATUS.IN_PROGRESS,
Task.STATUS.WAITING,
Expand Down Expand Up @@ -97,7 +104,16 @@ async function fetchTodayPlanTasks(visibleTasksWhere) {
[Op.notIn]: excludedStatuses,
},
parent_task_id: null,
// Exclude recurring parent tasks - only include non-recurring tasks or recurring instances
},
// Exclude tasks deferred to the future
{
[Op.or]: [
{ defer_until: null },
{ defer_until: { [Op.lte]: now } },
],
},
// Exclude recurring parent tasks - only include non-recurring tasks or recurring instances
{
[Op.or]: [
{
// Non-recurring tasks
Expand Down
12 changes: 12 additions & 0 deletions backend/modules/tasks/queries/query-builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ async function filterTasksByParams(
case 'today': {
const safeTimezone = getSafeTimezone(userTimezone);
const todayBounds = getTodayBoundsInUTC(safeTimezone);
const now = new Date();

// Tasks in today view are those with active statuses (in_progress, planned, waiting)
const todayPlanStatuses = [
Expand All @@ -116,6 +117,14 @@ async function filterTasksByParams(
'planned',
];

// Exclude tasks deferred to the future
const notDeferredCondition = {
[Op.or]: [
{ defer_until: null },
{ defer_until: { [Op.lte]: now } },
],
};

whereClause[Op.or] = [
{
// Non-recurring tasks with active status
Expand All @@ -128,6 +137,7 @@ async function filterTasksByParams(
},
{ recurring_parent_id: null },
{ status: { [Op.in]: todayPlanStatuses } },
notDeferredCondition,
],
},
{
Expand All @@ -137,6 +147,7 @@ async function filterTasksByParams(
{ recurrence_type: { [Op.ne]: null } },
{ recurring_parent_id: null },
{ status: { [Op.in]: todayPlanStatuses } },
notDeferredCondition,
],
},
{
Expand All @@ -151,6 +162,7 @@ async function filterTasksByParams(
],
},
},
notDeferredCondition,
],
},
];
Expand Down
38 changes: 38 additions & 0 deletions backend/tests/integration/tasks-metrics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,44 @@ describe('Task Metrics Overdue and Due Today Tasks', () => {
expect(dueTodayNames).toContain('Regular Due Today');
});

it('excludes deferred planned tasks from today_plan until defer time arrives', async () => {
const tomorrow = dayFromNow(1);
const yesterday = dayFromNow(-1);

// Planned task deferred to tomorrow — should NOT be in today_plan
await createTask({
name: 'Deferred Future Planned',
status: Task.STATUS.PLANNED,
defer_until: tomorrow,
});

// Planned task deferred to yesterday — should be in today_plan
await createTask({
name: 'Deferred Past Planned',
status: Task.STATUS.PLANNED,
defer_until: yesterday,
});

// In-progress task deferred to tomorrow — should NOT be in tasks_in_progress
await createTask({
name: 'Deferred Future In Progress',
status: Task.STATUS.IN_PROGRESS,
defer_until: tomorrow,
});

const metrics = await getTaskMetrics(user.id, 'UTC');
const todayPlanNames = metrics.tasks_today_plan.map(
(task) => task.name
);
const inProgressNames = metrics.tasks_in_progress.map(
(task) => task.name
);

expect(todayPlanNames).toContain('Deferred Past Planned');
expect(todayPlanNames).not.toContain('Deferred Future Planned');
expect(inProgressNames).not.toContain('Deferred Future In Progress');
});

it('includes tasks with WAITING status in Planned section, not in overdue', async () => {
await createTask({
name: 'Overdue Waiting',
Expand Down
66 changes: 66 additions & 0 deletions backend/tests/integration/tasks-today-plan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,72 @@ describe('Tasks Today Plan - Status-Based Filtering', () => {
expect(response.body.tasks_today_plan).toHaveLength(0);
});

it('should exclude tasks deferred to the future', async () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

// Planned task with future defer_until — should be excluded
await Task.create({
name: 'Deferred Future Planned',
user_id: user.id,
status: Task.STATUS.PLANNED,
defer_until: tomorrow,
});

// Planned task with past defer_until — should be included
const pastDeferredTask = await Task.create({
name: 'Deferred Past Planned',
user_id: user.id,
status: Task.STATUS.PLANNED,
defer_until: yesterday,
});

// Planned task with no defer_until — should be included
const noDeferTask = await Task.create({
name: 'No Defer Planned',
user_id: user.id,
status: Task.STATUS.PLANNED,
});

const response = await agent
.get('/api/tasks?type=today&include_lists=true')
.expect(200);

expect(response.body.tasks_today_plan).toBeDefined();
expect(response.body.tasks_today_plan).toHaveLength(2);

const taskIds = response.body.tasks_today_plan.map((t) => t.id);
expect(taskIds).toContain(pastDeferredTask.id);
expect(taskIds).toContain(noDeferTask.id);
});

it('should include a deferred planned task once defer time has passed', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);

const task = await Task.create({
name: 'Recently Undeferred Planned',
user_id: user.id,
status: Task.STATUS.PLANNED,
defer_until: oneHourAgo,
});

const response = await agent
.get('/api/tasks?type=today&include_lists=true')
.expect(200);

expect(response.body.tasks_today_plan).toBeDefined();
const taskIds = response.body.tasks_today_plan.map((t) => t.id);
expect(taskIds).toContain(task.id);

// Verify the task retains its planned status
const taskInResponse = response.body.tasks_today_plan.find(
(t) => t.id === task.id
);
expect(taskInResponse.status).toBe(Task.STATUS.PLANNED);
});

it('should order tasks by priority DESC, due_date ASC, project_id ASC', async () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
Expand Down