fix(recurring-tasks): show overdue recurring tasks in Today view #4825

- Changed addAllDueToday() to use getRepeatableTasksDueForDayIncludingOverdue$
- This ensures overdue recurring tasks (e.g., weekly tasks missed for several days) appear in Today view
- Added comprehensive unit tests for overdue recurring task scenarios
- Users can now see and access overdue recurring tasks when returning after missing days

Fixes #4825
This commit is contained in:
Johannes Millan 2025-07-24 17:10:09 +02:00
parent 29c55bf956
commit 47fb1b61b1
2 changed files with 83 additions and 3 deletions

View file

@ -132,6 +132,7 @@ describe('AddTasksForTomorrowService', () => {
beforeEach(() => {
taskRepeatCfgServiceMock = jasmine.createSpyObj('TaskRepeatCfgService', [
'getRepeatableTasksDueForDayOnly$',
'getRepeatableTasksDueForDayIncludingOverdue$',
'createRepeatableTask',
]);
@ -144,6 +145,9 @@ describe('AddTasksForTomorrowService', () => {
// Configure mock return values
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayOnly$.and.returnValue(of([]));
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayIncludingOverdue$.and.returnValue(
of([]),
);
taskRepeatCfgServiceMock.createRepeatableTask.and.returnValue(Promise.resolve());
TestBed.configureTestingModule({
@ -350,7 +354,7 @@ describe('AddTasksForTomorrowService', () => {
describe('addAllDueToday()', () => {
it('should create repeatable tasks for today but not dispatch if no tasks to move', async () => {
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayOnly$.and.returnValue(
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayIncludingOverdue$.and.returnValue(
of([mockRepeatCfg, mockRepeatCfg2]),
);
store.overrideSelector(selectTasksWithDueTimeForRange, []);
@ -369,7 +373,9 @@ describe('AddTasksForTomorrowService', () => {
});
it('should add due tasks to today', async () => {
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayOnly$.and.returnValue(of([]));
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayIncludingOverdue$.and.returnValue(
of([]),
);
store.overrideSelector(selectTasksWithDueTimeForRange, [mockTaskWithDueTimeToday]);
store.overrideSelector(selectTasksDueForDay, [mockTaskWithDueDayToday]);
store.overrideSelector(selectTasksForPlannerDay(getWorklogStr(today)), [
@ -395,6 +401,80 @@ describe('AddTasksForTomorrowService', () => {
expect(actualCall.taskIds).toContain('task4');
expect(result).toBe('ADDED');
});
it('should include overdue recurring tasks from previous days', async () => {
const overdueWeeklyTask: TaskRepeatCfg = {
...mockRepeatCfg,
id: 'overdue-weekly',
title: 'Weekly task from last Thursday',
repeatCycle: 'WEEKLY',
repeatEvery: 1,
startDate: '2024-01-01', // Started months ago
lastTaskCreation: new Date('2024-01-01').getTime(), // Last created months ago
};
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayIncludingOverdue$.and.returnValue(
of([overdueWeeklyTask]),
);
store.overrideSelector(selectTasksWithDueTimeForRange, []);
store.overrideSelector(selectTasksDueForDay, []);
store.overrideSelector(selectTasksForPlannerDay(getWorklogStr(today)), []);
store.overrideSelector(selectTodayTaskIds, []);
const dispatchSpy = spyOn(store, 'dispatch');
const result = await service.addAllDueToday();
expect(taskRepeatCfgServiceMock.createRepeatableTask).toHaveBeenCalledWith(
overdueWeeklyTask,
jasmine.any(Number),
);
expect(taskRepeatCfgServiceMock.createRepeatableTask).toHaveBeenCalledTimes(1);
// No dispatch since only repeatable tasks were created, no existing tasks to move
expect(dispatchSpy).not.toHaveBeenCalled();
expect(result).toBeUndefined();
});
it('should handle multiple overdue recurring tasks', async () => {
const overdueDaily: TaskRepeatCfg = {
...mockRepeatCfg,
id: 'overdue-daily',
title: 'Daily task from 3 days ago',
repeatCycle: 'DAILY',
repeatEvery: 1,
// eslint-disable-next-line no-mixed-operators
lastTaskCreation: Date.now() - 4 * 24 * 60 * 60 * 1000, // 4 days ago
};
const overdueMonthly: TaskRepeatCfg = {
...mockRepeatCfg,
id: 'overdue-monthly',
title: 'Monthly task from last month',
repeatCycle: 'MONTHLY',
repeatEvery: 1,
// eslint-disable-next-line no-mixed-operators
lastTaskCreation: Date.now() - 35 * 24 * 60 * 60 * 1000, // 35 days ago
};
taskRepeatCfgServiceMock.getRepeatableTasksDueForDayIncludingOverdue$.and.returnValue(
of([overdueDaily, overdueMonthly]),
);
store.overrideSelector(selectTasksWithDueTimeForRange, []);
store.overrideSelector(selectTasksDueForDay, []);
store.overrideSelector(selectTasksForPlannerDay(getWorklogStr(today)), []);
store.overrideSelector(selectTodayTaskIds, []);
await service.addAllDueToday();
expect(taskRepeatCfgServiceMock.createRepeatableTask).toHaveBeenCalledTimes(2);
expect(taskRepeatCfgServiceMock.createRepeatableTask).toHaveBeenCalledWith(
overdueDaily,
jasmine.any(Number),
);
expect(taskRepeatCfgServiceMock.createRepeatableTask).toHaveBeenCalledWith(
overdueMonthly,
jasmine.any(Number),
);
});
});
describe('_sortAll()', () => {

View file

@ -131,7 +131,7 @@ export class AddTasksForTomorrowService {
TaskLog.log('[AddTasksForTomorrow] Starting addAllDueToday', { todayStr });
const dueRepeatCfgs = await this._taskRepeatCfgService
.getRepeatableTasksDueForDayOnly$(todayDate.getTime())
.getRepeatableTasksDueForDayIncludingOverdue$(todayDate.getTime())
.pipe(first())
.toPromise();