From 133f534e33d1082e099f3ceb51de0d27c2e2389e Mon Sep 17 00:00:00 2001 From: Shiva Gupta Date: Tue, 30 Dec 2025 00:57:03 +0530 Subject: [PATCH] feat(reports): add "Overdue" as third category in Reports charts - Add isOverdue utility function to tasks-utils.ts for shared usage - Update ReportsView to categorize pending tasks with past due dates as overdue - Add overdue bar (red #F33434) to ReportChart component - Update Tasks.tsx to import isOverdue from shared utility - Add comprehensive test coverage for overdue categorization Categorization logic: - status === 'completed' -> Completed (pink) - status === 'pending' && due < today -> Overdue (red) - status === 'pending' && (due >= today || no due) -> Ongoing (blue) Fixes: #341 --- .../HomeComponents/Tasks/ReportChart.tsx | 6 ++ .../HomeComponents/Tasks/ReportsView.tsx | 11 +++- .../components/HomeComponents/Tasks/Tasks.tsx | 15 +---- .../Tasks/__tests__/ReportChart.test.tsx | 11 +++- .../Tasks/__tests__/ReportsView.test.tsx | 60 +++++++++++++++++-- .../__snapshots__/ReportChart.test.tsx.snap | 10 ++++ .../__snapshots__/ReportView.test.tsx.snap | 58 +++++++++++++++++- .../Tasks/__tests__/tasks-utils.test.ts | 35 +++++++++++ .../HomeComponents/Tasks/tasks-utils.ts | 13 ++++ 9 files changed, 195 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx b/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx index 85581b35..358b3798 100644 --- a/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx +++ b/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx @@ -98,6 +98,12 @@ export const ReportChart: React.FC = ({ name="Ongoing" label={{ position: 'top', fill: 'white', fontSize: 12 }} /> + diff --git a/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx b/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx index ee2b1fb9..7c5b9330 100644 --- a/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx +++ b/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ReportsViewProps } from '../../utils/types'; import { getStartOfDay } from '../../utils/utils'; import { ReportChart } from './ReportChart'; -import { parseTaskwarriorDate } from '../Tasks/tasks-utils'; +import { parseTaskwarriorDate, isOverdue } from '../Tasks/tasks-utils'; export const ReportsView: React.FC = ({ tasks }) => { const now = new Date(); @@ -14,6 +14,7 @@ export const ReportsView: React.FC = ({ tasks }) => { const startOfMonth = getStartOfDay( new Date(now.getFullYear(), now.getMonth(), 1) ); + const countStatuses = (filterDate: Date) => { return tasks .filter((task) => { @@ -31,11 +32,15 @@ export const ReportsView: React.FC = ({ tasks }) => { if (task.status === 'completed') { acc.completed += 1; } else if (task.status === 'pending') { - acc.ongoing += 1; + if (isOverdue(task.due)) { + acc.overdue += 1; + } else { + acc.ongoing += 1; + } } return acc; }, - { completed: 0, ongoing: 0 } + { completed: 0, ongoing: 0, overdue: 0 } ); }; diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index b548762d..ad8cf240 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -36,7 +36,7 @@ import { sortTasksById, getTimeSinceLastSync, hashKey, - parseTaskwarriorDate, + isOverdue, } from './tasks-utils'; import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; @@ -118,19 +118,6 @@ export const Tasks = ( // Handler for dialog open/close - const isOverdue = (due?: string) => { - if (!due) return false; - - const dueDate = parseTaskwarriorDate(due); - if (!dueDate) return false; - dueDate.setHours(0, 0, 0, 0); - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - return dueDate < today; - }; - const debouncedSearch = debounce((value: string) => { setDebouncedTerm(value); setCurrentPage(1); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx index e85b9a8a..1142ae1b 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx @@ -27,7 +27,7 @@ jest.mock('../report-download-utils', () => ({ })); describe('ReportChart', () => { - const mockData = [{ name: 'Today', completed: 5, ongoing: 3 }]; + const mockData = [{ name: 'Today', completed: 5, ongoing: 3, overdue: 2 }]; const defaultProps = { data: mockData, @@ -87,6 +87,15 @@ describe('ReportChart', () => { expect(ongoingBar).toHaveAttribute('data-name', 'Ongoing'); }); + it('displays overdue bar with correct color', () => { + render(); + + const overdueBar = screen.getByTestId('bar-overdue'); + + expect(overdueBar).toHaveAttribute('data-fill', '#F33434'); + expect(overdueBar).toHaveAttribute('data-name', 'Overdue'); + }); + it('renders chart axes', () => { render(); expect(screen.getByTestId('x-axis')).toBeInTheDocument(); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx index 162f9b9c..49fbe3e2 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx @@ -96,6 +96,50 @@ describe('ReportsView', () => { expect(data[0].completed).toBe(0); }); + it('counts pending tasks with past due date as overdue', () => { + const todayDate = new Date(); + const today = toTWFormat(todayDate); + + const pastDate = new Date(); + pastDate.setDate(pastDate.getDate() - 10); + const pastDue = toTWFormat(pastDate); + + const tasks = [ + createMockTask({ status: 'pending', due: pastDue }), + createMockTask({ status: 'pending', due: pastDue }), + createMockTask({ status: 'pending', due: today }), + createMockTask({ status: 'completed', end: today }), + ]; + + render(); + + const monthlyData = screen.getByTestId('monthly-report-chart-data'); + const data = JSON.parse(monthlyData.textContent || '[]'); + + expect(data[0].overdue).toBe(2); + expect(data[0].ongoing).toBe(1); + expect(data[0].completed).toBe(1); + }); + + it('treats task with no due date as ongoing', () => { + const today = toTWFormat(new Date()); + + const tasks = [ + createMockTask({ + status: 'pending', + entry: today, + }), + ]; + + render(); + const dailyData = screen.getByTestId('daily-report-chart-data'); + const data = JSON.parse(dailyData.textContent || '[]'); + + expect(data[0].ongoing).toBe(1); + expect(data[0].overdue).toBe(0); + expect(data[0].completed).toBe(0); + }); + it('filters tasks by date range correctly', () => { const referenceDate = new Date('2024-01-10T12:00:00Z'); @@ -135,7 +179,7 @@ describe('ReportsView', () => { jest.useRealTimers(); }); - it('uses modified date when available', () => { + it('uses end date when available', () => { const today = toTWFormat(new Date()); const tasks = [ createMockTask({ @@ -191,21 +235,29 @@ describe('ReportsView', () => { }); it('handles mixed statuses correctly', () => { - const today = toTWFormat(new Date()); + const todayDate = new Date(); + const today = toTWFormat(todayDate); + + const pastDate = new Date(); + pastDate.setDate(pastDate.getDate() - 10); + const pastDue = toTWFormat(pastDate); + const tasks = [ createMockTask({ status: 'completed', end: today }), createMockTask({ status: 'pending', due: today }), + createMockTask({ status: 'pending', due: pastDue }), createMockTask({ status: 'deleted', end: today }), createMockTask({ status: 'recurring', due: today }), ]; render(); - const dailyData = screen.getByTestId('daily-report-chart-data'); - const data = JSON.parse(dailyData.textContent || '[]'); + const monthlyData = screen.getByTestId('monthly-report-chart-data'); + const data = JSON.parse(monthlyData.textContent || '[]'); expect(data[0].completed).toBe(1); expect(data[0].ongoing).toBe(1); + expect(data[0].overdue).toBe(1); }); }); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportChart.test.tsx.snap b/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportChart.test.tsx.snap index 21d170d2..33892cde 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportChart.test.tsx.snap +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportChart.test.tsx.snap @@ -128,6 +128,11 @@ exports[`ReportChart Component using Snapshot renders correctly with one data en data-name="Ongoing" data-testid="bar-ongoing" /> +
@@ -262,6 +267,11 @@ exports[`ReportChart Component using Snapshot renders correctly with several dat data-name="Ongoing" data-testid="bar-ongoing" /> +
diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportView.test.tsx.snap b/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportView.test.tsx.snap index 1d9df42e..2310c32b 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportView.test.tsx.snap +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/__snapshots__/ReportView.test.tsx.snap @@ -116,6 +116,9 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta ongoing: 1 + + overdue: 0 +
+
@@ -267,6 +276,9 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta ongoing: 1 + + overdue: 0 +
+
@@ -418,6 +436,9 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta ongoing: 1 + + overdue: 0 +
+
@@ -578,6 +605,9 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa ongoing: 1 + + overdue: 0 +
+
@@ -727,7 +763,10 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa completed: 2 - ongoing: 2 + ongoing: 1 + + + overdue: 1
+
@@ -878,7 +923,10 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa completed: 4 - ongoing: 4 + ongoing: 1 + + + overdue: 3
+
diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts index ca65b58b..0cd855cc 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts @@ -13,6 +13,7 @@ import { getTimeSinceLastSync, hashKey, parseTaskwarriorDate, + isOverdue, } from '../tasks-utils'; import { Task } from '@/components/utils/types'; @@ -614,3 +615,37 @@ describe('parseTaskwarriorDate', () => { expect(result).toBeInstanceOf(Date); }); }); + +describe('isOverdue', () => { + beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date('2025-11-11T10:00:00.000Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns false for undefined due date', () => { + expect(isOverdue(undefined)).toBe(false); + }); + + it('returns false for empty string due date', () => { + expect(isOverdue('')).toBe(false); + }); + + it('returns false for future due date', () => { + expect(isOverdue('20251215T130002Z')).toBe(false); + }); + + it('returns true for past due date', () => { + expect(isOverdue('20251015T130002Z')).toBe(true); + }); + + it('returns false for today due date', () => { + expect(isOverdue('20251111T130002Z')).toBe(false); + }); + + it('returns false for invalid date format', () => { + expect(isOverdue('invalid-date')).toBe(false); + }); +}); diff --git a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts index f0e8446e..5819c916 100644 --- a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts +++ b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts @@ -199,6 +199,19 @@ export const parseTaskwarriorDate = (dateString: string) => { return isNaN(date.getTime()) ? null : date; }; +export const isOverdue = (due?: string) => { + if (!due) return false; + + const dueDate = parseTaskwarriorDate(due); + if (!dueDate) return false; + dueDate.setHours(0, 0, 0, 0); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return dueDate < today; +}; + export const sortTasksById = (tasks: Task[], order: 'asc' | 'desc') => { return tasks.sort((a, b) => { if (order === 'asc') {