diff --git a/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx b/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx index 5009815a..85581b35 100644 --- a/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx +++ b/frontend/src/components/HomeComponents/Tasks/ReportChart.tsx @@ -83,10 +83,21 @@ export const ReportChart: React.FC = ({ - - + + diff --git a/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx b/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx index 6be314cb..ee2b1fb9 100644 --- a/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx +++ b/frontend/src/components/HomeComponents/Tasks/ReportsView.tsx @@ -2,6 +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'; export const ReportsView: React.FC = ({ tasks }) => { const now = new Date(); @@ -16,10 +17,13 @@ export const ReportsView: React.FC = ({ tasks }) => { const countStatuses = (filterDate: Date) => { return tasks .filter((task) => { - const taskDateStr = task.modified || task.due; + const taskDateStr = task.end || task.due || task.entry; if (!taskDateStr) return false; - const modifiedDate = getStartOfDay(new Date(taskDateStr)); + const parsedDate = parseTaskwarriorDate(taskDateStr); + if (!parsedDate) return false; + + const modifiedDate = getStartOfDay(parsedDate); return modifiedDate >= filterDate; }) .reduce( @@ -36,9 +40,7 @@ export const ReportsView: React.FC = ({ tasks }) => { }; const dailyData = [{ name: 'Today', ...countStatuses(today) }]; - const sevenDaysAgo = getStartOfDay(new Date()); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - const weeklyData = [{ name: 'This Week', ...countStatuses(sevenDaysAgo) }]; + const weeklyData = [{ name: 'This Week', ...countStatuses(startOfWeek) }]; const monthlyData = [{ name: 'This Month', ...countStatuses(startOfMonth) }]; return ( diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 834b114c..34f7fd84 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -36,6 +36,7 @@ import { sortTasksById, getTimeSinceLastSync, hashKey, + parseTaskwarriorDate, } from './tasks-utils'; import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; @@ -120,14 +121,8 @@ export const Tasks = ( const isOverdue = (due?: string) => { if (!due) return false; - const parsed = new Date( - due.replace( - /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/, - '$1-$2-$3T$4:$5:$6Z' - ) - ); - - const dueDate = new Date(parsed); + const dueDate = parseTaskwarriorDate(due); + if (!dueDate) return false; dueDate.setHours(0, 0, 0, 0); const today = new Date(); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx index f895bd34..845c2b19 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx @@ -23,20 +23,25 @@ const createMockTask = ( depends = ['depends1', 'depends2'], } = overrides; - const getDateForOffset = (offset: DateOffset): Date => { + const getDateForOffset = (offset: DateOffset): string => { + let date: Date; switch (offset) { case 'dailyData': - return mockToday; + date = mockToday; + break; case 'weeklyData': - // Calcul du début de la semaine (dimanche) const startOfWeek = new Date(mockToday); startOfWeek.setUTCDate( startOfWeek.getUTCDate() - startOfWeek.getUTCDay() ); - return startOfWeek; + date = startOfWeek; + break; case 'monthlyData': - return new Date(mockToday.getUTCFullYear(), mockToday.getUTCMonth(), 1); + date = new Date(mockToday.getUTCFullYear(), mockToday.getUTCMonth(), 1); + break; } + // Return Taskwarrior format: YYYYMMDDTHHMMSSZ + return date.toISOString().replace(/[-:]/g, '').replace('.000', ''); }; return { @@ -48,12 +53,12 @@ const createMockTask = ( uuid: `mockUuid-${id}`, urgency: 1, priority: 'mockPriority', - due: 'mockDue', + due: status === 'pending' ? getDateForOffset(dateOffset) : '', start: 'mockStart', - end: 'mockEnd', - entry: 'mockEntry', + end: status === 'completed' ? getDateForOffset(dateOffset) : '', + entry: getDateForOffset(dateOffset), wait: 'mockWait', - modified: getDateForOffset(dateOffset).toISOString(), + modified: '', depends, rtype: 'mockRtype', recur: 'mockRecur', diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx index 495a51dc..162f9b9c 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx @@ -2,6 +2,10 @@ import { render, screen } from '@testing-library/react'; import { ReportsView } from '../ReportsView'; import { Task } from '@/components/utils/types'; +const toTWFormat = (date: Date): string => { + return date.toISOString().replace(/[-:]/g, '').replace('.000', ''); +}; + jest.mock('../ReportChart', () => ({ ReportChart: jest.fn(({ title, data, chartId }) => (
@@ -60,11 +64,11 @@ describe('ReportsView', () => { describe('Data Calculation', () => { it('counts completed tasks correctly', () => { - const today = new Date().toISOString(); + const today = toTWFormat(new Date()); const tasks = [ - createMockTask({ status: 'completed', modified: today }), - createMockTask({ status: 'completed', modified: today }), - createMockTask({ status: 'pending', modified: today }), + createMockTask({ status: 'completed', end: today }), + createMockTask({ status: 'completed', end: today }), + createMockTask({ status: 'pending', due: today }), ]; render(); @@ -77,10 +81,10 @@ describe('ReportsView', () => { }); it('counts pending tasks as ongoing', () => { - const today = new Date().toISOString(); + const today = toTWFormat(new Date()); const tasks = [ - createMockTask({ status: 'pending', modified: today }), - createMockTask({ status: 'pending', modified: today }), + createMockTask({ status: 'pending', due: today }), + createMockTask({ status: 'pending', due: today }), ]; render(); @@ -103,14 +107,14 @@ describe('ReportsView', () => { thisWeek.setDate(thisWeek.getDate() - 2); const tasks = [ - createMockTask({ status: 'completed', modified: today.toISOString() }), + createMockTask({ status: 'completed', end: toTWFormat(today) }), createMockTask({ status: 'completed', - modified: yesterday.toISOString(), + end: toTWFormat(yesterday), }), createMockTask({ status: 'completed', - modified: thisWeek.toISOString(), + end: toTWFormat(thisWeek), }), ]; @@ -132,11 +136,11 @@ describe('ReportsView', () => { }); it('uses modified date when available', () => { - const today = new Date().toISOString(); + const today = toTWFormat(new Date()); const tasks = [ createMockTask({ status: 'completed', - modified: today, + end: today, due: '2020-01-01T00:00:00Z', }), ]; @@ -149,12 +153,12 @@ describe('ReportsView', () => { expect(data[0].completed).toBe(1); }); - it('falls back to due date when modified is not available', () => { - const today = new Date().toISOString(); + it('falls back to due date when end is not available', () => { + const today = toTWFormat(new Date()); const tasks = [ createMockTask({ status: 'completed', - modified: '', + end: '', due: today, }), ]; @@ -167,12 +171,14 @@ describe('ReportsView', () => { expect(data[0].completed).toBe(1); }); - it('excludes tasks without modified or due dates', () => { + it('uses entry date as fallback when end and due are not available', () => { + const today = toTWFormat(new Date()); const tasks = [ createMockTask({ - status: 'completed', - modified: '', + status: 'pending', + end: '', due: '', + entry: today, }), ]; @@ -181,17 +187,16 @@ describe('ReportsView', () => { const dailyData = screen.getByTestId('daily-report-chart-data'); const data = JSON.parse(dailyData.textContent || '[]'); - expect(data[0].completed).toBe(0); - expect(data[0].ongoing).toBe(0); + expect(data[0].ongoing).toBe(1); }); it('handles mixed statuses correctly', () => { - const today = new Date().toISOString(); + const today = toTWFormat(new Date()); const tasks = [ - createMockTask({ status: 'completed', modified: today }), - createMockTask({ status: 'pending', modified: today }), - createMockTask({ status: 'deleted', modified: today }), - createMockTask({ status: 'recurring', modified: today }), + createMockTask({ status: 'completed', end: today }), + createMockTask({ status: 'pending', due: today }), + createMockTask({ status: 'deleted', end: today }), + createMockTask({ status: 'recurring', due: today }), ]; render(); @@ -216,7 +221,7 @@ describe('ReportsView', () => { const tasks = [ createMockTask({ status: 'completed', - modified: taskInWeek.toISOString(), + end: toTWFormat(taskInWeek), }), ]; @@ -238,7 +243,7 @@ describe('ReportsView', () => { const tasks = [ createMockTask({ status: 'completed', - modified: taskInMonth.toISOString(), + end: toTWFormat(taskInMonth), }), ]; 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 137244be..ca65b58b 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts @@ -12,6 +12,7 @@ import { bulkMarkTasksAsDeleted, getTimeSinceLastSync, hashKey, + parseTaskwarriorDate, } from '../tasks-utils'; import { Task } from '@/components/utils/types'; @@ -593,3 +594,23 @@ describe('bulkMarkTasksAsDeleted', () => { expect(result).toBe(false); }); }); + +describe('parseTaskwarriorDate', () => { + it('parses Taskwarrior date format correctly', () => { + const result = parseTaskwarriorDate('20241215T130002Z'); + expect(result).toEqual(new Date('2024-12-15T13:00:02Z')); + }); + + it('returns null for empty string', () => { + expect(parseTaskwarriorDate('')).toBeNull(); + }); + + it('returns null for invalid date format', () => { + expect(parseTaskwarriorDate('invalid-date')).toBeNull(); + }); + + it('handles ISO format gracefully', () => { + const result = parseTaskwarriorDate('20241215T130002Z'); + expect(result).toBeInstanceOf(Date); + }); +}); diff --git a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts index e4f11fed..f0e8446e 100644 --- a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts +++ b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts @@ -182,6 +182,23 @@ export const formattedDate = (dateString: string) => { } }; +export const parseTaskwarriorDate = (dateString: string) => { + // Taskwarrior date format: YYYYMMDDTHHMMSSZ + + if (!dateString) return null; + + const year = dateString.substring(0, 4); + const month = dateString.substring(4, 6); + const day = dateString.substring(6, 8); + const hour = dateString.substring(9, 11); + const min = dateString.substring(11, 13); + const sec = dateString.substring(13, 15); + const parsed = `${year}-${month}-${day}T${hour}:${min}:${sec}Z`; + + const date = new Date(parsed); + return isNaN(date.getTime()) ? null : date; +}; + export const sortTasksById = (tasks: Task[], order: 'asc' | 'desc') => { return tasks.sort((a, b) => { if (order === 'asc') {