diff --git a/src/app/features/add-tasks-for-tomorrow/add-tasks-for-tomorrow.service.spec.ts b/src/app/features/add-tasks-for-tomorrow/add-tasks-for-tomorrow.service.spec.ts index f6243ede76..639e17198b 100644 --- a/src/app/features/add-tasks-for-tomorrow/add-tasks-for-tomorrow.service.spec.ts +++ b/src/app/features/add-tasks-for-tomorrow/add-tasks-for-tomorrow.service.spec.ts @@ -183,6 +183,8 @@ describe('AddTasksForTomorrowService', () => { planner: { days: {}, }, + tasks: { ids: [], entities: {} }, + projects: { ids: [], entities: {} }, }, }), ], diff --git a/src/app/features/planner/store/planner.selectors.spec.ts b/src/app/features/planner/store/planner.selectors.spec.ts index 566591f0f1..bdde195f48 100644 --- a/src/app/features/planner/store/planner.selectors.spec.ts +++ b/src/app/features/planner/store/planner.selectors.spec.ts @@ -7,6 +7,7 @@ import * as fromSelectors from './planner.selectors'; import { plannerFeatureKey, PlannerState } from './planner.reducer'; import { Task, TaskState } from '../../tasks/task.model'; import { TASK_FEATURE_NAME } from '../../tasks/store/task.reducer'; +import { PROJECT_FEATURE_NAME } from '../../project/store/project.reducer'; import { appStateFeatureKey } from '../../../root-store/app-state/app-state.reducer'; import { getDbDateStr } from '../../../util/get-db-date-str'; @@ -340,16 +341,6 @@ describe('Planner Selectors - selectPlannerDays', () => { }; }; - const emptyTaskState: TaskState = { - ids: [], - entities: {}, - currentTaskId: null, - selectedTaskId: null, - lastCurrentTaskId: null, - isDataLoaded: true, - taskDetailTargetPanel: null, - }; - const emptyPlannerState: PlannerState = { days: {}, addPlannedTasksDialogLastShown: undefined, @@ -373,12 +364,7 @@ describe('Planner Selectors - selectPlannerDays', () => { it('should return a PlannerDay for each day date', () => { const selector = createPlannerDaysSelector([today]); - const result = selector.projector( - emptyTaskState, - emptyPlannerState, - defaultScheduleConfig, - 0, - ); + const result = selector.projector([], emptyPlannerState, defaultScheduleConfig, 0); expect(result.length).toBe(1); expect(result[0].dayDate).toBe(today); @@ -388,18 +374,13 @@ describe('Planner Selectors - selectPlannerDays', () => { it('should include tasks from planner state for a non-today day', () => { const tomorrow = getDbDateStr(new Date(Date.now() + 86400000)); const task = createMockTask({ id: 't1', title: 'Plan task' }); - const taskState: TaskState = { - ...emptyTaskState, - ids: ['t1'], - entities: { t1: task }, - }; const plannerState: PlannerState = { ...emptyPlannerState, days: { [tomorrow]: ['t1'] }, }; const selector = fromSelectors.selectPlannerDays([tomorrow], [], [], [], [], today); - const result = selector.projector(taskState, plannerState, defaultScheduleConfig, 0); + const result = selector.projector([task], plannerState, defaultScheduleConfig, 0); expect(result[0].tasks.length).toBe(1); expect(result[0].tasks[0].id).toBe('t1'); @@ -407,16 +388,11 @@ describe('Planner Selectors - selectPlannerDays', () => { it('should include unplanned today tasks passed to factory', () => { const task = createMockTask({ id: 't1', title: 'Today task' }); - const taskState: TaskState = { - ...emptyTaskState, - ids: ['t1'], - entities: { t1: task }, - }; // Pass t1 as a todayListTaskId (unplanned since allPlannedTasks is empty) const selector = fromSelectors.selectPlannerDays([today], [], ['t1'], [], [], today); const result = selector.projector( - taskState, + [task], emptyPlannerState, defaultScheduleConfig, 0, @@ -436,12 +412,7 @@ describe('Planner Selectors - selectPlannerDays', () => { lunchBreakEnd: '13:00', }; const selector = createPlannerDaysSelector([today]); - const result = selector.projector( - emptyTaskState, - emptyPlannerState, - scheduleConfig, - 0, - ); + const result = selector.projector([], emptyPlannerState, scheduleConfig, 0); // 8 hours = 28800000 ms expect(result[0].availableHours).toBe(28800000); @@ -450,12 +421,7 @@ describe('Planner Selectors - selectPlannerDays', () => { it('should not set availableHours when schedule is disabled', () => { const selector = createPlannerDaysSelector([today]); - const result = selector.projector( - emptyTaskState, - emptyPlannerState, - defaultScheduleConfig, - 0, - ); + const result = selector.projector([], emptyPlannerState, defaultScheduleConfig, 0); expect(result[0].availableHours).toBeUndefined(); expect(result[0].progressPercentage).toBeUndefined(); @@ -464,18 +430,13 @@ describe('Planner Selectors - selectPlannerDays', () => { it('should include additional days from planner state not in dayDates', () => { const tomorrow = getDbDateStr(new Date(Date.now() + 86400000)); const task = createMockTask({ id: 't1' }); - const taskState: TaskState = { - ...emptyTaskState, - ids: ['t1'], - entities: { t1: task }, - }; const plannerState: PlannerState = { ...emptyPlannerState, days: { [tomorrow]: ['t1'] }, }; const selector = createPlannerDaysSelector([today]); - const result = selector.projector(taskState, plannerState, defaultScheduleConfig, 0); + const result = selector.projector([task], plannerState, defaultScheduleConfig, 0); // Should include both today (from dayDates) and tomorrow (from planner state) expect(result.length).toBe(2); @@ -491,12 +452,7 @@ describe('Planner Selectors - selectPlannerDays', () => { }; const selector = createPlannerDaysSelector([today]); - const result = selector.projector( - emptyTaskState, - plannerState, - defaultScheduleConfig, - 0, - ); + const result = selector.projector([], plannerState, defaultScheduleConfig, 0); expect(result[0].tasks.length).toBe(0); }); @@ -535,12 +491,7 @@ describe('Planner Selectors - selectPlannerDays', () => { [], today, ); - const result = selector.projector( - emptyTaskState, - emptyPlannerState, - scheduleConfig, - 0, - ); + const result = selector.projector([], emptyPlannerState, scheduleConfig, 0); // timeEstimate should include the timed event duration (7200000 ms = 2 hours) expect(result[0].timeEstimate).toBe(7200000); @@ -585,12 +536,7 @@ describe('Planner Selectors - selectPlannerDays', () => { [], today, ); - const result = selector.projector( - emptyTaskState, - emptyPlannerState, - scheduleConfig, - 0, - ); + const result = selector.projector([], emptyPlannerState, scheduleConfig, 0); // timeEstimate should NOT include all-day events (they use raw 24h duration) // so it should be 0 when there are no timed events @@ -659,11 +605,6 @@ describe('Planner Selectors - selectPlannerDays', () => { title: 'Task with estimate', timeEstimate: 3600000, }); // 1 hour - const taskState: TaskState = { - ...emptyTaskState, - ids: ['t1'], - entities: { t1: task }, - }; const plannerState: PlannerState = { ...emptyPlannerState, days: { [today]: ['t1'] }, @@ -693,7 +634,7 @@ describe('Planner Selectors - selectPlannerDays', () => { [], today, ); - const result = selector.projector(taskState, plannerState, scheduleConfig, 0); + const result = selector.projector([task], plannerState, scheduleConfig, 0); // timeEstimate = task (3600000) + timed event (7200000) = 10800000 ms = 3 hours expect(result[0].timeEstimate).toBe(10800000); @@ -797,6 +738,7 @@ describe('Planner Selectors - selectAllTasksDueToday', () => { [appStateFeatureKey]: { todayStr, startOfNextDayDiffMs: 0 }, [TASK_FEATURE_NAME]: { ...mockTaskState, ...taskState }, [plannerFeatureKey]: { ...mockPlannerState, ...plannerState }, + [PROJECT_FEATURE_NAME]: { ids: [], entities: {} }, }); describe('selectAllTasksDueToday', () => { @@ -908,21 +850,6 @@ describe('Planner Selectors - selectAllTasksDueToday', () => { expect(result[0].id).toBe('taskDueToday'); }); - it('should handle missing entity references in taskState.ids gracefully', () => { - const mockState = createMockState( - { - ids: ['taskDueToday', 'nonExistentTask'], - entities: { taskDueToday: mockTasks.taskDueToday }, - }, - { days: {} }, - ); - - const result = fromSelectors.selectAllTasksDueToday(mockState); - - expect(result.length).toBe(1); - expect(result[0].id).toBe('taskDueToday'); - }); - it('should return correct count when combining planner and due tasks', () => { const mockState = createMockState(); const result = fromSelectors.selectAllTasksDueToday(mockState); @@ -995,6 +922,7 @@ describe('Planner Selectors - selectAllTasksDueToday', () => { ...mockPlannerState, days: plannerDays, }, + [PROJECT_FEATURE_NAME]: { ids: [], entities: {} }, }); it('should include dueWithTime task at 2 AM next day when offset extends today', () => { diff --git a/src/app/features/planner/store/planner.selectors.ts b/src/app/features/planner/store/planner.selectors.ts index 617c10ce16..c6d6a5d87e 100644 --- a/src/app/features/planner/store/planner.selectors.ts +++ b/src/app/features/planner/store/planner.selectors.ts @@ -1,6 +1,6 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; import * as fromPlanner from './planner.reducer'; -import { selectTaskFeatureState } from '../../tasks/store/task.selectors'; +import { selectAllTasksInActiveProjects } from '../../tasks/store/task.selectors'; import { NoStartTimeRepeatProjection, PlannerDay, @@ -11,12 +11,7 @@ import { ScheduleItemType, } from '../planner.model'; import { ScheduleFromCalendarEvent } from '../../schedule/schedule.model'; -import { - TaskCopy, - TaskState, - TaskWithDueDay, - TaskWithDueTime, -} from '../../tasks/task.model'; +import { Task, TaskCopy, TaskWithDueDay, TaskWithDueTime } from '../../tasks/task.model'; import { TaskRepeatCfg } from '../../task-repeat-cfg/task-repeat-cfg.model'; import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string'; import { isValidSplitTime } from '../../../util/is-valid-split-time'; @@ -43,30 +38,26 @@ export const selectPlannerState = createFeatureSelector { - // Start with tasks from planner state for today - const allDue: (TaskWithDueTime | TaskWithDueDay)[] = ( - plannerState.days[todayStr] || [] - ) - .map((tid) => taskState.entities[tid]) - // there is a chance that the task is not in the store anymore - .filter((t): t is TaskWithDueDay => !!t); + // activeTasks excludes tasks from archived projects, so filter planner IDs through it. + const plannedTodayIds = new Set(plannerState.days[todayStr] || []); + const allDue: (TaskWithDueTime | TaskWithDueDay)[] = activeTasks.filter( + (t): t is TaskWithDueDay => plannedTodayIds.has(t.id), + ); // Use Set for O(1) lookup const allDueIds = new Set(allDue.map((t) => t.id)); - // PERF: Single pass over ids instead of two Object.values() calls - for (const id of taskState.ids) { + for (const task of activeTasks) { + const id = task.id; if (allDueIds.has(id)) continue; - const task = taskState.entities[id]; - if (!task) continue; // Check if task is due today // Priority: dueWithTime takes precedence over dueDay (mutual exclusivity pattern) @@ -90,12 +81,11 @@ export const selectAllTasksDueToday = createSelector( export const selectTasksForPlannerDay = (day: string) => { return createSelector( selectPlannerState, - selectTaskFeatureState, - (plannerState, taskState) => - (plannerState.days[day] || []) - .map((tid) => taskState.entities[tid] as TaskCopy) - // there is a chance that the task is not in the store anymore - .filter((t) => !!t), + selectAllTasksInActiveProjects, + (plannerState, activeTasks) => { + const dayIds = new Set(plannerState.days[day] || []); + return activeTasks.filter((t) => dayIds.has(t.id)) as TaskCopy[]; + }, ); }; @@ -114,11 +104,13 @@ export const selectPlannerDays = ( const unplannedTaskIdsToday = todayListTaskIds.filter((id) => !allPlannedIdSet.has(id)); return createSelector( - selectTaskFeatureState, + selectAllTasksInActiveProjects, selectPlannerState, selectTimelineConfig, selectStartOfNextDayDiffMs, - (taskState, plannerState, scheduleConfig, startOfNextDayDiffMs): PlannerDay[] => { + (activeTasks, plannerState, scheduleConfig, startOfNextDayDiffMs): PlannerDay[] => { + const taskMap = new Map(activeTasks.map((t) => [t.id, t])); + const allDatesWithData = Object.keys(plannerState.days); const dayDatesToUse = [ ...dayDates, @@ -128,13 +120,13 @@ export const selectPlannerDays = ( ]; // Pre-compute deadline tasks grouped by day (O(N) once, then O(1) per day) - const deadlineMap = groupDeadlineTasksByDay(taskState, startOfNextDayDiffMs); + const deadlineMap = groupDeadlineTasksByDay(activeTasks, startOfNextDayDiffMs); return dayDatesToUse.map((dayDate) => getPlannerDay( dayDate, todayStr, - taskState, + taskMap, plannerState, taskRepeatCfgs, allPlannedTasks, @@ -150,29 +142,25 @@ export const selectPlannerDays = ( }; export const selectPlannerDayMap = createSelector( - selectTaskFeatureState, + selectAllTasksInActiveProjects, selectPlannerState, - (taskState, plannerState): PlannerDayMap => { + (activeTasks, plannerState): PlannerDayMap => { + const taskMap = new Map(activeTasks.map((t) => [t.id, t])); const map: PlannerDayMap = {}; Object.keys(plannerState.days).forEach((dayDate) => { const tids = plannerState.days[dayDate] || []; - const normalTasks = tids - .map((id) => taskState.entities[id] as TaskCopy) - // filter out deleted tasks - .filter((t) => !!t); - map[dayDate] = normalTasks; + map[dayDate] = tids.map((id) => taskMap.get(id) as TaskCopy).filter((t) => !!t); }); return map; }, ); -// Extracted common function const getPlannerDay = ( dayDate: string, todayStr: string, - taskState: TaskState, + taskMap: Map, plannerState: any, taskRepeatCfgs: TaskRepeatCfg[], allPlannedTasks: TaskWithDueTime[], @@ -190,7 +178,7 @@ const getPlannerDay = ( ? unplannedTaskIdsToday : plannerState.days[dayDate] || []; const normalTasks = tIds - .map((id) => taskState.entities[id] as TaskCopy) + .map((id) => taskMap.get(id) as TaskCopy) .filter((t) => !!t) // Filter out tasks with dueDay in future if it is Today's column .filter((t) => !isTodayI || !t.dueDay || t.dueDay <= todayStr); @@ -392,13 +380,12 @@ const isPlannerAllDayCalendarEvent = (calEv: ScheduleFromCalendarEvent): boolean * O(N) single pass — callers can then do O(1) map lookups per day. */ const groupDeadlineTasksByDay = ( - taskState: TaskState, + activeTasks: Task[], startOfNextDayDiffMs: number = 0, ): Record => { const result: Record = {}; - for (const id of taskState.ids) { - const task = taskState.entities[id] as TaskCopy | undefined; - if (!task || task.isDone) continue; + for (const task of activeTasks) { + if (task.isDone) continue; let dayKey: string | undefined; if (task.deadlineWithTime) { @@ -407,7 +394,7 @@ const groupDeadlineTasksByDay = ( dayKey = task.deadlineDay; } if (dayKey) { - (result[dayKey] ??= []).push(task); + (result[dayKey] ??= []).push(task as TaskCopy); } } return result;