Skip to content

Commit 3e17ded

Browse files
fix(reports): correct date parsing and weekly range calculation (#340)
- Add parseTaskwarriorDate utility for Taskwarrior date format (YYYYMMDDTHHMMSSZ) - Fix weekly report to use start of week instead of last 7 days - Use end date for completed tasks instead of modified date - Use entry date as fallback for pending tasks without due date - Add count labels on chart bars for better visibility - Refactor isOverdue to use shared date parsing utility Fixes: #322
1 parent 6c2a106 commit 3e17ded

File tree

7 files changed

+107
-51
lines changed

7 files changed

+107
-51
lines changed

frontend/src/components/HomeComponents/Tasks/ReportChart.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,21 @@ export const ReportChart: React.FC<ReportChartProps> = ({
8383
<Tooltip
8484
contentStyle={{ backgroundColor: '#333', border: 'none' }}
8585
labelClassName="text-white"
86+
cursor={false}
8687
/>
8788
<Legend wrapperClassName="text-white" />
88-
<Bar dataKey="completed" fill="#E776CB" name="Completed" />
89-
<Bar dataKey="ongoing" fill="#5FD9FA" name="Ongoing" />
89+
<Bar
90+
dataKey="completed"
91+
fill="#E776CB"
92+
name="Completed"
93+
label={{ position: 'top', fill: 'white', fontSize: 12 }}
94+
/>
95+
<Bar
96+
dataKey="ongoing"
97+
fill="#5FD9FA"
98+
name="Ongoing"
99+
label={{ position: 'top', fill: 'white', fontSize: 12 }}
100+
/>
90101
</BarChart>
91102
</ResponsiveContainer>
92103
</div>

frontend/src/components/HomeComponents/Tasks/ReportsView.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { ReportsViewProps } from '../../utils/types';
33
import { getStartOfDay } from '../../utils/utils';
44
import { ReportChart } from './ReportChart';
5+
import { parseTaskwarriorDate } from '../Tasks/tasks-utils';
56

67
export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
78
const now = new Date();
@@ -16,10 +17,13 @@ export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
1617
const countStatuses = (filterDate: Date) => {
1718
return tasks
1819
.filter((task) => {
19-
const taskDateStr = task.modified || task.due;
20+
const taskDateStr = task.end || task.due || task.entry;
2021
if (!taskDateStr) return false;
2122

22-
const modifiedDate = getStartOfDay(new Date(taskDateStr));
23+
const parsedDate = parseTaskwarriorDate(taskDateStr);
24+
if (!parsedDate) return false;
25+
26+
const modifiedDate = getStartOfDay(parsedDate);
2327
return modifiedDate >= filterDate;
2428
})
2529
.reduce(
@@ -36,9 +40,7 @@ export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
3640
};
3741

3842
const dailyData = [{ name: 'Today', ...countStatuses(today) }];
39-
const sevenDaysAgo = getStartOfDay(new Date());
40-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
41-
const weeklyData = [{ name: 'This Week', ...countStatuses(sevenDaysAgo) }];
43+
const weeklyData = [{ name: 'This Week', ...countStatuses(startOfWeek) }];
4244
const monthlyData = [{ name: 'This Month', ...countStatuses(startOfMonth) }];
4345

4446
return (

frontend/src/components/HomeComponents/Tasks/Tasks.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
sortTasksById,
3737
getTimeSinceLastSync,
3838
hashKey,
39+
parseTaskwarriorDate,
3940
} from './tasks-utils';
4041
import Pagination from './Pagination';
4142
import { url } from '@/components/utils/URLs';
@@ -120,14 +121,8 @@ export const Tasks = (
120121
const isOverdue = (due?: string) => {
121122
if (!due) return false;
122123

123-
const parsed = new Date(
124-
due.replace(
125-
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,
126-
'$1-$2-$3T$4:$5:$6Z'
127-
)
128-
);
129-
130-
const dueDate = new Date(parsed);
124+
const dueDate = parseTaskwarriorDate(due);
125+
if (!dueDate) return false;
131126
dueDate.setHours(0, 0, 0, 0);
132127

133128
const today = new Date();

frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,25 @@ const createMockTask = (
2323
depends = ['depends1', 'depends2'],
2424
} = overrides;
2525

26-
const getDateForOffset = (offset: DateOffset): Date => {
26+
const getDateForOffset = (offset: DateOffset): string => {
27+
let date: Date;
2728
switch (offset) {
2829
case 'dailyData':
29-
return mockToday;
30+
date = mockToday;
31+
break;
3032
case 'weeklyData':
31-
// Calcul du début de la semaine (dimanche)
3233
const startOfWeek = new Date(mockToday);
3334
startOfWeek.setUTCDate(
3435
startOfWeek.getUTCDate() - startOfWeek.getUTCDay()
3536
);
36-
return startOfWeek;
37+
date = startOfWeek;
38+
break;
3739
case 'monthlyData':
38-
return new Date(mockToday.getUTCFullYear(), mockToday.getUTCMonth(), 1);
40+
date = new Date(mockToday.getUTCFullYear(), mockToday.getUTCMonth(), 1);
41+
break;
3942
}
43+
// Return Taskwarrior format: YYYYMMDDTHHMMSSZ
44+
return date.toISOString().replace(/[-:]/g, '').replace('.000', '');
4045
};
4146

4247
return {
@@ -48,12 +53,12 @@ const createMockTask = (
4853
uuid: `mockUuid-${id}`,
4954
urgency: 1,
5055
priority: 'mockPriority',
51-
due: 'mockDue',
56+
due: status === 'pending' ? getDateForOffset(dateOffset) : '',
5257
start: 'mockStart',
53-
end: 'mockEnd',
54-
entry: 'mockEntry',
58+
end: status === 'completed' ? getDateForOffset(dateOffset) : '',
59+
entry: getDateForOffset(dateOffset),
5560
wait: 'mockWait',
56-
modified: getDateForOffset(dateOffset).toISOString(),
61+
modified: '',
5762
depends,
5863
rtype: 'mockRtype',
5964
recur: 'mockRecur',

frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { render, screen } from '@testing-library/react';
22
import { ReportsView } from '../ReportsView';
33
import { Task } from '@/components/utils/types';
44

5+
const toTWFormat = (date: Date): string => {
6+
return date.toISOString().replace(/[-:]/g, '').replace('.000', '');
7+
};
8+
59
jest.mock('../ReportChart', () => ({
610
ReportChart: jest.fn(({ title, data, chartId }) => (
711
<div data-testid={chartId}>
@@ -60,11 +64,11 @@ describe('ReportsView', () => {
6064

6165
describe('Data Calculation', () => {
6266
it('counts completed tasks correctly', () => {
63-
const today = new Date().toISOString();
67+
const today = toTWFormat(new Date());
6468
const tasks = [
65-
createMockTask({ status: 'completed', modified: today }),
66-
createMockTask({ status: 'completed', modified: today }),
67-
createMockTask({ status: 'pending', modified: today }),
69+
createMockTask({ status: 'completed', end: today }),
70+
createMockTask({ status: 'completed', end: today }),
71+
createMockTask({ status: 'pending', due: today }),
6872
];
6973

7074
render(<ReportsView tasks={tasks} />);
@@ -77,10 +81,10 @@ describe('ReportsView', () => {
7781
});
7882

7983
it('counts pending tasks as ongoing', () => {
80-
const today = new Date().toISOString();
84+
const today = toTWFormat(new Date());
8185
const tasks = [
82-
createMockTask({ status: 'pending', modified: today }),
83-
createMockTask({ status: 'pending', modified: today }),
86+
createMockTask({ status: 'pending', due: today }),
87+
createMockTask({ status: 'pending', due: today }),
8488
];
8589

8690
render(<ReportsView tasks={tasks} />);
@@ -103,14 +107,14 @@ describe('ReportsView', () => {
103107
thisWeek.setDate(thisWeek.getDate() - 2);
104108

105109
const tasks = [
106-
createMockTask({ status: 'completed', modified: today.toISOString() }),
110+
createMockTask({ status: 'completed', end: toTWFormat(today) }),
107111
createMockTask({
108112
status: 'completed',
109-
modified: yesterday.toISOString(),
113+
end: toTWFormat(yesterday),
110114
}),
111115
createMockTask({
112116
status: 'completed',
113-
modified: thisWeek.toISOString(),
117+
end: toTWFormat(thisWeek),
114118
}),
115119
];
116120

@@ -132,11 +136,11 @@ describe('ReportsView', () => {
132136
});
133137

134138
it('uses modified date when available', () => {
135-
const today = new Date().toISOString();
139+
const today = toTWFormat(new Date());
136140
const tasks = [
137141
createMockTask({
138142
status: 'completed',
139-
modified: today,
143+
end: today,
140144
due: '2020-01-01T00:00:00Z',
141145
}),
142146
];
@@ -149,12 +153,12 @@ describe('ReportsView', () => {
149153
expect(data[0].completed).toBe(1);
150154
});
151155

152-
it('falls back to due date when modified is not available', () => {
153-
const today = new Date().toISOString();
156+
it('falls back to due date when end is not available', () => {
157+
const today = toTWFormat(new Date());
154158
const tasks = [
155159
createMockTask({
156160
status: 'completed',
157-
modified: '',
161+
end: '',
158162
due: today,
159163
}),
160164
];
@@ -167,12 +171,14 @@ describe('ReportsView', () => {
167171
expect(data[0].completed).toBe(1);
168172
});
169173

170-
it('excludes tasks without modified or due dates', () => {
174+
it('uses entry date as fallback when end and due are not available', () => {
175+
const today = toTWFormat(new Date());
171176
const tasks = [
172177
createMockTask({
173-
status: 'completed',
174-
modified: '',
178+
status: 'pending',
179+
end: '',
175180
due: '',
181+
entry: today,
176182
}),
177183
];
178184

@@ -181,17 +187,16 @@ describe('ReportsView', () => {
181187
const dailyData = screen.getByTestId('daily-report-chart-data');
182188
const data = JSON.parse(dailyData.textContent || '[]');
183189

184-
expect(data[0].completed).toBe(0);
185-
expect(data[0].ongoing).toBe(0);
190+
expect(data[0].ongoing).toBe(1);
186191
});
187192

188193
it('handles mixed statuses correctly', () => {
189-
const today = new Date().toISOString();
194+
const today = toTWFormat(new Date());
190195
const tasks = [
191-
createMockTask({ status: 'completed', modified: today }),
192-
createMockTask({ status: 'pending', modified: today }),
193-
createMockTask({ status: 'deleted', modified: today }),
194-
createMockTask({ status: 'recurring', modified: today }),
196+
createMockTask({ status: 'completed', end: today }),
197+
createMockTask({ status: 'pending', due: today }),
198+
createMockTask({ status: 'deleted', end: today }),
199+
createMockTask({ status: 'recurring', due: today }),
195200
];
196201

197202
render(<ReportsView tasks={tasks} />);
@@ -216,7 +221,7 @@ describe('ReportsView', () => {
216221
const tasks = [
217222
createMockTask({
218223
status: 'completed',
219-
modified: taskInWeek.toISOString(),
224+
end: toTWFormat(taskInWeek),
220225
}),
221226
];
222227

@@ -238,7 +243,7 @@ describe('ReportsView', () => {
238243
const tasks = [
239244
createMockTask({
240245
status: 'completed',
241-
modified: taskInMonth.toISOString(),
246+
end: toTWFormat(taskInMonth),
242247
}),
243248
];
244249

frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
bulkMarkTasksAsDeleted,
1313
getTimeSinceLastSync,
1414
hashKey,
15+
parseTaskwarriorDate,
1516
} from '../tasks-utils';
1617
import { Task } from '@/components/utils/types';
1718

@@ -593,3 +594,23 @@ describe('bulkMarkTasksAsDeleted', () => {
593594
expect(result).toBe(false);
594595
});
595596
});
597+
598+
describe('parseTaskwarriorDate', () => {
599+
it('parses Taskwarrior date format correctly', () => {
600+
const result = parseTaskwarriorDate('20241215T130002Z');
601+
expect(result).toEqual(new Date('2024-12-15T13:00:02Z'));
602+
});
603+
604+
it('returns null for empty string', () => {
605+
expect(parseTaskwarriorDate('')).toBeNull();
606+
});
607+
608+
it('returns null for invalid date format', () => {
609+
expect(parseTaskwarriorDate('invalid-date')).toBeNull();
610+
});
611+
612+
it('handles ISO format gracefully', () => {
613+
const result = parseTaskwarriorDate('20241215T130002Z');
614+
expect(result).toBeInstanceOf(Date);
615+
});
616+
});

frontend/src/components/HomeComponents/Tasks/tasks-utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,23 @@ export const formattedDate = (dateString: string) => {
182182
}
183183
};
184184

185+
export const parseTaskwarriorDate = (dateString: string) => {
186+
// Taskwarrior date format: YYYYMMDDTHHMMSSZ
187+
188+
if (!dateString) return null;
189+
190+
const year = dateString.substring(0, 4);
191+
const month = dateString.substring(4, 6);
192+
const day = dateString.substring(6, 8);
193+
const hour = dateString.substring(9, 11);
194+
const min = dateString.substring(11, 13);
195+
const sec = dateString.substring(13, 15);
196+
const parsed = `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
197+
198+
const date = new Date(parsed);
199+
return isNaN(date.getTime()) ? null : date;
200+
};
201+
185202
export const sortTasksById = (tasks: Task[], order: 'asc' | 'desc') => {
186203
return tasks.sort((a, b) => {
187204
if (order === 'asc') {

0 commit comments

Comments
 (0)