Skip to content

Commit 121cb70

Browse files
authored
Fix deferred planned tasks not appearing in Today view after defer time (#892)
1 parent 74d2027 commit 121cb70

File tree

4 files changed

+133
-1
lines changed

4 files changed

+133
-1
lines changed

backend/modules/tasks/queries/metrics-queries.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ async function countTasksPendingOverMonth(visibleTasksWhere) {
5050
}
5151

5252
async function fetchTasksInProgress(visibleTasksWhere) {
53+
const now = new Date();
5354
return await Task.findAll({
5455
where: {
5556
...visibleTasksWhere,
5657
status: { [Op.in]: [Task.STATUS.IN_PROGRESS, 'in_progress'] },
58+
// Exclude tasks deferred to the future
59+
[Op.or]: [
60+
{ defer_until: null },
61+
{ defer_until: { [Op.lte]: now } },
62+
],
5763
parent_task_id: null,
5864
recurring_parent_id: null,
5965
},
@@ -67,6 +73,7 @@ async function fetchTasksInProgress(visibleTasksWhere) {
6773
}
6874

6975
async function fetchTodayPlanTasks(visibleTasksWhere) {
76+
const now = new Date();
7077
const todayPlanStatuses = [
7178
Task.STATUS.IN_PROGRESS,
7279
Task.STATUS.WAITING,
@@ -97,7 +104,16 @@ async function fetchTodayPlanTasks(visibleTasksWhere) {
97104
[Op.notIn]: excludedStatuses,
98105
},
99106
parent_task_id: null,
100-
// Exclude recurring parent tasks - only include non-recurring tasks or recurring instances
107+
},
108+
// Exclude tasks deferred to the future
109+
{
110+
[Op.or]: [
111+
{ defer_until: null },
112+
{ defer_until: { [Op.lte]: now } },
113+
],
114+
},
115+
// Exclude recurring parent tasks - only include non-recurring tasks or recurring instances
116+
{
101117
[Op.or]: [
102118
{
103119
// Non-recurring tasks

backend/modules/tasks/queries/query-builders.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async function filterTasksByParams(
105105
case 'today': {
106106
const safeTimezone = getSafeTimezone(userTimezone);
107107
const todayBounds = getTodayBoundsInUTC(safeTimezone);
108+
const now = new Date();
108109

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

120+
// Exclude tasks deferred to the future
121+
const notDeferredCondition = {
122+
[Op.or]: [
123+
{ defer_until: null },
124+
{ defer_until: { [Op.lte]: now } },
125+
],
126+
};
127+
119128
whereClause[Op.or] = [
120129
{
121130
// Non-recurring tasks with active status
@@ -128,6 +137,7 @@ async function filterTasksByParams(
128137
},
129138
{ recurring_parent_id: null },
130139
{ status: { [Op.in]: todayPlanStatuses } },
140+
notDeferredCondition,
131141
],
132142
},
133143
{
@@ -137,6 +147,7 @@ async function filterTasksByParams(
137147
{ recurrence_type: { [Op.ne]: null } },
138148
{ recurring_parent_id: null },
139149
{ status: { [Op.in]: todayPlanStatuses } },
150+
notDeferredCondition,
140151
],
141152
},
142153
{
@@ -151,6 +162,7 @@ async function filterTasksByParams(
151162
],
152163
},
153164
},
165+
notDeferredCondition,
154166
],
155167
},
156168
];

backend/tests/integration/tasks-metrics.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,44 @@ describe('Task Metrics Overdue and Due Today Tasks', () => {
191191
expect(dueTodayNames).toContain('Regular Due Today');
192192
});
193193

194+
it('excludes deferred planned tasks from today_plan until defer time arrives', async () => {
195+
const tomorrow = dayFromNow(1);
196+
const yesterday = dayFromNow(-1);
197+
198+
// Planned task deferred to tomorrow — should NOT be in today_plan
199+
await createTask({
200+
name: 'Deferred Future Planned',
201+
status: Task.STATUS.PLANNED,
202+
defer_until: tomorrow,
203+
});
204+
205+
// Planned task deferred to yesterday — should be in today_plan
206+
await createTask({
207+
name: 'Deferred Past Planned',
208+
status: Task.STATUS.PLANNED,
209+
defer_until: yesterday,
210+
});
211+
212+
// In-progress task deferred to tomorrow — should NOT be in tasks_in_progress
213+
await createTask({
214+
name: 'Deferred Future In Progress',
215+
status: Task.STATUS.IN_PROGRESS,
216+
defer_until: tomorrow,
217+
});
218+
219+
const metrics = await getTaskMetrics(user.id, 'UTC');
220+
const todayPlanNames = metrics.tasks_today_plan.map(
221+
(task) => task.name
222+
);
223+
const inProgressNames = metrics.tasks_in_progress.map(
224+
(task) => task.name
225+
);
226+
227+
expect(todayPlanNames).toContain('Deferred Past Planned');
228+
expect(todayPlanNames).not.toContain('Deferred Future Planned');
229+
expect(inProgressNames).not.toContain('Deferred Future In Progress');
230+
});
231+
194232
it('includes tasks with WAITING status in Planned section, not in overdue', async () => {
195233
await createTask({
196234
name: 'Overdue Waiting',

backend/tests/integration/tasks-today-plan.test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,72 @@ describe('Tasks Today Plan - Status-Based Filtering', () => {
216216
expect(response.body.tasks_today_plan).toHaveLength(0);
217217
});
218218

219+
it('should exclude tasks deferred to the future', async () => {
220+
const tomorrow = new Date();
221+
tomorrow.setDate(tomorrow.getDate() + 1);
222+
const yesterday = new Date();
223+
yesterday.setDate(yesterday.getDate() - 1);
224+
225+
// Planned task with future defer_until — should be excluded
226+
await Task.create({
227+
name: 'Deferred Future Planned',
228+
user_id: user.id,
229+
status: Task.STATUS.PLANNED,
230+
defer_until: tomorrow,
231+
});
232+
233+
// Planned task with past defer_until — should be included
234+
const pastDeferredTask = await Task.create({
235+
name: 'Deferred Past Planned',
236+
user_id: user.id,
237+
status: Task.STATUS.PLANNED,
238+
defer_until: yesterday,
239+
});
240+
241+
// Planned task with no defer_until — should be included
242+
const noDeferTask = await Task.create({
243+
name: 'No Defer Planned',
244+
user_id: user.id,
245+
status: Task.STATUS.PLANNED,
246+
});
247+
248+
const response = await agent
249+
.get('/api/tasks?type=today&include_lists=true')
250+
.expect(200);
251+
252+
expect(response.body.tasks_today_plan).toBeDefined();
253+
expect(response.body.tasks_today_plan).toHaveLength(2);
254+
255+
const taskIds = response.body.tasks_today_plan.map((t) => t.id);
256+
expect(taskIds).toContain(pastDeferredTask.id);
257+
expect(taskIds).toContain(noDeferTask.id);
258+
});
259+
260+
it('should include a deferred planned task once defer time has passed', async () => {
261+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
262+
263+
const task = await Task.create({
264+
name: 'Recently Undeferred Planned',
265+
user_id: user.id,
266+
status: Task.STATUS.PLANNED,
267+
defer_until: oneHourAgo,
268+
});
269+
270+
const response = await agent
271+
.get('/api/tasks?type=today&include_lists=true')
272+
.expect(200);
273+
274+
expect(response.body.tasks_today_plan).toBeDefined();
275+
const taskIds = response.body.tasks_today_plan.map((t) => t.id);
276+
expect(taskIds).toContain(task.id);
277+
278+
// Verify the task retains its planned status
279+
const taskInResponse = response.body.tasks_today_plan.find(
280+
(t) => t.id === task.id
281+
);
282+
expect(taskInResponse.status).toBe(Task.STATUS.PLANNED);
283+
});
284+
219285
it('should order tasks by priority DESC, due_date ASC, project_id ASC', async () => {
220286
const tomorrow = new Date();
221287
tomorrow.setDate(tomorrow.getDate() + 1);

0 commit comments

Comments
 (0)