diff --git a/docs/settings/misc-settings.md b/docs/settings/misc-settings.md index 7733b380..1c9f167d 100644 --- a/docs/settings/misc-settings.md +++ b/docs/settings/misc-settings.md @@ -10,6 +10,10 @@ These settings control various plugin features and display options that don't fi **Show project subtasks widget** - Display a collapsible widget showing all tasks that reference the current note as a project. This widget appears in the editor when viewing notes that have tasks referencing them as projects, providing quick access to related tasks. +## Task Display + +**Hide completed tasks from overdue** - Control whether completed tasks appear as overdue in the agenda view. When enabled (default), completed tasks will not appear in the "Overdue" section of the agenda view, even if their due or scheduled date has passed. This setting affects task grouping and overdue detection throughout the plugin, helping keep overdue lists focused on actionable items. + ## Performance Settings **Disable note indexing** - Disable indexing and caching of non-task notes to improve performance in large vaults. Note: This will disable the Notes view and notes display in the Agenda view. Requires plugin restart to take effect. diff --git a/src/editor/ProjectNoteDecorations.ts b/src/editor/ProjectNoteDecorations.ts index 1890b44b..5e1a1f42 100644 --- a/src/editor/ProjectNoteDecorations.ts +++ b/src/editor/ProjectNoteDecorations.ts @@ -25,7 +25,8 @@ class ProjectSubtasksWidget extends WidgetType { this.filterService = new FilterService( plugin.cacheManager, plugin.statusManager, - plugin.priorityManager + plugin.priorityManager, + plugin ); // Initialize with default filter query diff --git a/src/main.ts b/src/main.ts index c315e861..e68c2e37 100644 --- a/src/main.ts +++ b/src/main.ts @@ -185,7 +185,8 @@ export default class TaskNotesPlugin extends Plugin { this.filterService = new FilterService( this.cacheManager, this.statusManager, - this.priorityManager + this.priorityManager, + this ); this.viewStateManager = new ViewStateManager(this.app, this); this.projectSubtasksService = new ProjectSubtasksService(this); diff --git a/src/services/FilterService.ts b/src/services/FilterService.ts index 8e48b25e..435c5058 100644 --- a/src/services/FilterService.ts +++ b/src/services/FilterService.ts @@ -42,7 +42,8 @@ export class FilterService extends EventEmitter { constructor( cacheManager: MinimalNativeCache, statusManager: StatusManager, - priorityManager: PriorityManager + priorityManager: PriorityManager, + private plugin?: any // Plugin reference for accessing settings ) { super(); this.cacheManager = cacheManager; @@ -769,25 +770,28 @@ export class FilterService extends EventEmitter { const referenceDate = targetDate || new Date(); referenceDate.setHours(0, 0, 0, 0); + const isCompleted = this.statusManager.isCompletedStatus(task.status); + const hideCompletedFromOverdue = this.plugin?.settings?.hideCompletedFromOverdue ?? true; + // For recurring tasks, check if due on the target date if (task.recurrence) { if (isDueByRRule(task, referenceDate)) { // If due on target date, determine which group based on target date vs today const referenceDateStr = format(referenceDate, 'yyyy-MM-dd'); - return this.getDateGroupFromDateString(referenceDateStr); + return this.getDateGroupFromDateStringWithTask(referenceDateStr, isCompleted, hideCompletedFromOverdue); } else { // Recurring task not due on target date // If it has an original due date, use that, otherwise no due date if (task.due) { - return this.getDueDateGroupFromDate(task.due); + return this.getDateGroupFromDateStringWithTask(task.due, isCompleted, hideCompletedFromOverdue); } return 'No due date'; } } - // Non-recurring task - use original logic + // Non-recurring task - use completion-aware logic if (!task.due) return 'No due date'; - return this.getDueDateGroupFromDate(task.due); + return this.getDateGroupFromDateStringWithTask(task.due, isCompleted, hideCompletedFromOverdue); } /** @@ -797,7 +801,9 @@ export class FilterService extends EventEmitter { private getDateGroupFromDateString(dateString: string): string { const todayStr = getTodayString(); - // Use time-aware overdue detection + // Use time-aware overdue detection with completion-aware logic + // For categorization purposes, we need the task to determine completion status + // This call is for categorization only, specific task overdue checks happen elsewhere if (isOverdueTimeAware(dateString)) return 'Overdue'; // Extract date part for day-level comparisons @@ -829,9 +835,87 @@ export class FilterService extends EventEmitter { return this.getDateGroupFromDateString(dueDate); } + /** + * Helper method to get due date group for a specific task (completion-aware) + */ + private getDueDateGroupForTask(task: TaskInfo): string { + if (!task.due) return 'No due date'; + + const isCompleted = this.statusManager.isCompletedStatus(task.status); + const hideCompletedFromOverdue = this.plugin?.settings?.hideCompletedFromOverdue ?? true; + + return this.getDateGroupFromDateStringWithTask(task.due, isCompleted, hideCompletedFromOverdue); + } + + /** + * Get date group from date string with task completion awareness + */ + private getDateGroupFromDateStringWithTask(dateString: string, isCompleted: boolean, hideCompletedFromOverdue: boolean): string { + const todayStr = getTodayString(); + + // Use completion-aware overdue detection + if (isOverdueTimeAware(dateString, isCompleted, hideCompletedFromOverdue)) return 'Overdue'; + + // Extract date part for day-level comparisons + const datePart = getDatePart(dateString); + if (isSameDateSafe(datePart, todayStr)) return 'Today'; + + try { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = format(tomorrow, 'yyyy-MM-dd'); + if (isSameDateSafe(datePart, tomorrowStr)) return 'Tomorrow'; + + const thisWeek = new Date(); + thisWeek.setDate(thisWeek.getDate() + 7); + const thisWeekStr = format(thisWeek, 'yyyy-MM-dd'); + if (isBeforeDateSafe(datePart, thisWeekStr) || isSameDateSafe(datePart, thisWeekStr)) return 'This week'; + + return 'Later'; + } catch (error) { + console.error(`Error categorizing date ${dateString}:`, error); + return 'Invalid Date'; + } + } + private getScheduledDateGroup(task: TaskInfo, targetDate?: Date): string { if (!task.scheduled) return 'No scheduled date'; - return this.getScheduledDateGroupFromDate(task.scheduled); + + const isCompleted = this.statusManager.isCompletedStatus(task.status); + const hideCompletedFromOverdue = this.plugin?.settings?.hideCompletedFromOverdue ?? true; + + return this.getScheduledDateGroupForTask(task.scheduled, isCompleted, hideCompletedFromOverdue); + } + + /** + * Get scheduled date group with task completion awareness + */ + private getScheduledDateGroupForTask(scheduledDate: string, isCompleted: boolean, hideCompletedFromOverdue: boolean): string { + const todayStr = getTodayString(); + + // Use completion-aware overdue detection for past scheduled + if (isOverdueTimeAware(scheduledDate, isCompleted, hideCompletedFromOverdue)) return 'Past scheduled'; + + // Extract date part for day-level comparisons + const datePart = getDatePart(scheduledDate); + if (isSameDateSafe(datePart, todayStr)) return 'Today'; + + try { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = format(tomorrow, 'yyyy-MM-dd'); + if (isSameDateSafe(datePart, tomorrowStr)) return 'Tomorrow'; + + const thisWeek = new Date(); + thisWeek.setDate(thisWeek.getDate() + 7); + const thisWeekStr = format(thisWeek, 'yyyy-MM-dd'); + if (isBeforeDateSafe(datePart, thisWeekStr) || isSameDateSafe(datePart, thisWeekStr)) return 'This week'; + + return 'Later'; + } catch (error) { + console.error(`Error categorizing scheduled date ${scheduledDate}:`, error); + return 'Invalid Date'; + } } /** @@ -1271,16 +1355,19 @@ export class FilterService extends EventEmitter { // If showing overdue tasks and this is today, include overdue tasks on today if (includeOverdue && isViewingToday) { + const isCompleted = this.statusManager.isCompletedStatus(task.status); + const hideCompletedFromOverdue = this.plugin?.settings?.hideCompletedFromOverdue ?? true; + // Check if due date is overdue (show on today) if (task.due && getDatePart(task.due) !== dateStr) { - if (isOverdueTimeAware(task.due)) { + if (isOverdueTimeAware(task.due, isCompleted, hideCompletedFromOverdue)) { return true; } } // Check if scheduled date is overdue (show on today) if (task.scheduled && getDatePart(task.scheduled) !== dateStr) { - if (isOverdueTimeAware(task.scheduled)) { + if (isOverdueTimeAware(task.scheduled, isCompleted, hideCompletedFromOverdue)) { return true; } } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 5a670bdf..fbcefe1d 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -56,6 +56,8 @@ export interface TaskNotesSettings { autoStopTimeTrackingNotification: boolean; // Project subtasks widget settings showProjectSubtasks: boolean; + // Overdue behavior settings + hideCompletedFromOverdue: boolean; // ICS integration settings icsIntegration: ICSIntegrationSettings; // Saved filter views @@ -297,6 +299,8 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = { autoStopTimeTrackingNotification: false, // Project subtasks widget defaults showProjectSubtasks: true, + // Overdue behavior defaults + hideCompletedFromOverdue: true, // ICS integration defaults icsIntegration: { defaultNoteTemplate: '', @@ -1514,6 +1518,21 @@ export class TaskNotesSettingTab extends PluginSettingTab { this.plugin.notifyDataChanged(); }); }); + // Hide completed tasks from overdue + new Setting(container) + .setName('Hide completed tasks from overdue') + .setDesc('When enabled, completed tasks will not appear as overdue in the agenda view, even if their due/scheduled date has passed') + .addToggle(toggle => { + toggle.toggleEl.setAttribute('aria-label', 'Hide completed tasks from overdue status'); + return toggle + .setValue(this.plugin.settings.hideCompletedFromOverdue) + .onChange(async (value) => { + this.plugin.settings.hideCompletedFromOverdue = value; + await this.plugin.saveSettings(); + // Refresh views to apply the change + this.plugin.notifyDataChanged(); + }); + }); // Notes indexing toggle new Setting(container) diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index 343e9ce0..2b224f30 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -670,9 +670,14 @@ export function isBeforeDateTimeAware(date1: string, date2: string): boolean { /** * Check if a date/datetime is overdue (past current date/time) */ -export function isOverdueTimeAware(dateString: string): boolean { +export function isOverdueTimeAware(dateString: string, isCompleted?: boolean, hideCompletedFromOverdue?: boolean): boolean { if (!dateString) return false; + // If the setting is enabled and task is completed, don't consider it overdue + if (hideCompletedFromOverdue && isCompleted) { + return false; + } + try { const taskDate = parseDate(dateString); const now = new Date(); diff --git a/src/views/NotesView.ts b/src/views/NotesView.ts index 80776418..254881e9 100644 --- a/src/views/NotesView.ts +++ b/src/views/NotesView.ts @@ -194,7 +194,7 @@ export class NotesView extends ItemView { .setName('No notes found') .setHeading(); emptyState.createEl('p', { - text: 'No notes found for the selected date. Try selecting a different date or create some notes.', + text: 'No notes found for the selected date. Try selecting a different date in the Mini Calendar view or create some notes.', cls: 'notes-view__empty-description' }); } diff --git a/test-overdue-setting.md b/test-overdue-setting.md new file mode 100644 index 00000000..d1dbea40 --- /dev/null +++ b/test-overdue-setting.md @@ -0,0 +1,19 @@ +# Test: Hide Completed Tasks from Overdue Setting + +## Test Steps +1. Enable the "Hide completed tasks from overdue" setting in Settings > Misc +2. Create a task with a due date in the past (e.g., yesterday) +3. Mark the task as completed +4. Check the agenda view - the completed task should NOT appear as overdue +5. Disable the setting +6. Check the agenda view again - the completed task should now appear as overdue + +## Expected Behavior +- When setting is enabled (default): Completed tasks don't show as overdue +- When setting is disabled: Completed tasks show as overdue if their date has passed + +## Implementation Details +- Setting: `hideCompletedFromOverdue` (boolean, default: true) +- Function: `isOverdueTimeAware(dateString, isCompleted?, hideCompletedFromOverdue?)` +- Location: Settings > Misc tab +- Impact: Affects agenda view, task grouping, and overdue categorization \ No newline at end of file diff --git a/tests/unit/services/FilterService-fix-verification.test.ts b/tests/unit/services/FilterService-fix-verification.test.ts index 52dafef8..d320ae74 100644 --- a/tests/unit/services/FilterService-fix-verification.test.ts +++ b/tests/unit/services/FilterService-fix-verification.test.ts @@ -46,7 +46,7 @@ describe('FilterService - Issue 153 Fix Verification', () => { mockPriorityManager = {} as any; - filterService = new FilterService(mockCacheManager, mockStatusManager, mockPriorityManager); + filterService = new FilterService(mockCacheManager, mockStatusManager, mockPriorityManager, null); }); describe('Timezone-safe date comparisons', () => { diff --git a/tests/unit/services/FilterService.test.ts b/tests/unit/services/FilterService.test.ts index 3386f839..7461732d 100644 --- a/tests/unit/services/FilterService.test.ts +++ b/tests/unit/services/FilterService.test.ts @@ -55,7 +55,7 @@ describe('FilterService - Issue 153 Timezone Bug Fix Verification', () => { } as any; mockPriorityManager = {} as any; - filterService = new FilterService(mockCacheManager, mockStatusManager, mockPriorityManager); + filterService = new FilterService(mockCacheManager, mockStatusManager, mockPriorityManager, null); }); describe('Date comparison edge cases that cause issue 153', () => {