Skip to content

Commit e0922de

Browse files
committed
fix(reports): correct date parsing and weekly range calculation (#322)
- 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 a89f58c commit e0922de

File tree

8 files changed

+91
-46
lines changed

8 files changed

+91
-46
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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ const createMockTask = (
5050
priority: 'mockPriority',
5151
due: 'mockDue',
5252
start: 'mockStart',
53-
end: 'mockEnd',
54-
entry: 'mockEntry',
53+
end:
54+
status === 'completed' ? getDateForOffset(dateOffset).toISOString() : '',
55+
entry: getDateForOffset(dateOffset).toISOString(),
5556
wait: 'mockWait',
56-
modified: getDateForOffset(dateOffset).toISOString(),
57+
modified: '',
5758
depends,
5859
rtype: 'mockRtype',
5960
recur: 'mockRecur',

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

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ describe('ReportsView', () => {
6262
it('counts completed tasks correctly', () => {
6363
const today = new Date().toISOString();
6464
const tasks = [
65-
createMockTask({ status: 'completed', modified: today }),
66-
createMockTask({ status: 'completed', modified: today }),
67-
createMockTask({ status: 'pending', modified: today }),
65+
createMockTask({ status: 'completed', end: today }),
66+
createMockTask({ status: 'completed', end: today }),
67+
createMockTask({ status: 'pending', due: today }),
6868
];
6969

7070
render(<ReportsView tasks={tasks} />);
@@ -79,8 +79,8 @@ describe('ReportsView', () => {
7979
it('counts pending tasks as ongoing', () => {
8080
const today = new Date().toISOString();
8181
const tasks = [
82-
createMockTask({ status: 'pending', modified: today }),
83-
createMockTask({ status: 'pending', modified: today }),
82+
createMockTask({ status: 'pending', due: today }),
83+
createMockTask({ status: 'pending', due: today }),
8484
];
8585

8686
render(<ReportsView tasks={tasks} />);
@@ -103,14 +103,14 @@ describe('ReportsView', () => {
103103
thisWeek.setDate(thisWeek.getDate() - 2);
104104

105105
const tasks = [
106-
createMockTask({ status: 'completed', modified: today.toISOString() }),
106+
createMockTask({ status: 'completed', end: today.toISOString() }),
107107
createMockTask({
108108
status: 'completed',
109-
modified: yesterday.toISOString(),
109+
end: yesterday.toISOString(),
110110
}),
111111
createMockTask({
112112
status: 'completed',
113-
modified: thisWeek.toISOString(),
113+
end: thisWeek.toISOString(),
114114
}),
115115
];
116116

@@ -136,7 +136,7 @@ describe('ReportsView', () => {
136136
const tasks = [
137137
createMockTask({
138138
status: 'completed',
139-
modified: today,
139+
end: today,
140140
due: '2020-01-01T00:00:00Z',
141141
}),
142142
];
@@ -149,12 +149,12 @@ describe('ReportsView', () => {
149149
expect(data[0].completed).toBe(1);
150150
});
151151

152-
it('falls back to due date when modified is not available', () => {
152+
it('falls back to due date when end is not available', () => {
153153
const today = new Date().toISOString();
154154
const tasks = [
155155
createMockTask({
156156
status: 'completed',
157-
modified: '',
157+
end: '',
158158
due: today,
159159
}),
160160
];
@@ -167,12 +167,14 @@ describe('ReportsView', () => {
167167
expect(data[0].completed).toBe(1);
168168
});
169169

170-
it('excludes tasks without modified or due dates', () => {
170+
it('uses entry date as fallback when end and due are not available', () => {
171+
const today = new Date().toISOString();
171172
const tasks = [
172173
createMockTask({
173-
status: 'completed',
174-
modified: '',
174+
status: 'pending',
175+
end: '',
175176
due: '',
177+
entry: today,
176178
}),
177179
];
178180

@@ -181,17 +183,16 @@ describe('ReportsView', () => {
181183
const dailyData = screen.getByTestId('daily-report-chart-data');
182184
const data = JSON.parse(dailyData.textContent || '[]');
183185

184-
expect(data[0].completed).toBe(0);
185-
expect(data[0].ongoing).toBe(0);
186+
expect(data[0].ongoing).toBe(1);
186187
});
187188

188189
it('handles mixed statuses correctly', () => {
189190
const today = new Date().toISOString();
190191
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 }),
192+
createMockTask({ status: 'completed', end: today }),
193+
createMockTask({ status: 'pending', due: today }),
194+
createMockTask({ status: 'deleted', end: today }),
195+
createMockTask({ status: 'recurring', due: today }),
195196
];
196197

197198
render(<ReportsView tasks={tasks} />);
@@ -216,7 +217,7 @@ describe('ReportsView', () => {
216217
const tasks = [
217218
createMockTask({
218219
status: 'completed',
219-
modified: taskInWeek.toISOString(),
220+
end: taskInWeek.toISOString(),
220221
}),
221222
];
222223

@@ -238,7 +239,7 @@ describe('ReportsView', () => {
238239
const tasks = [
239240
createMockTask({
240241
status: 'completed',
241-
modified: taskInMonth.toISOString(),
242+
end: taskInMonth.toISOString(),
242243
}),
243244
];
244245

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta
114114
completed: 0
115115
</span>
116116
<span>
117-
ongoing: 1
117+
ongoing: 0
118118
</span>
119119
</div>
120120
<div
@@ -265,7 +265,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta
265265
completed: 0
266266
</span>
267267
<span>
268-
ongoing: 1
268+
ongoing: 0
269269
</span>
270270
</div>
271271
<div
@@ -416,7 +416,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only one ta
416416
completed: 0
417417
</span>
418418
<span>
419-
ongoing: 1
419+
ongoing: 0
420420
</span>
421421
</div>
422422
<div
@@ -576,7 +576,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa
576576
completed: 1
577577
</span>
578578
<span>
579-
ongoing: 1
579+
ongoing: 0
580580
</span>
581581
</div>
582582
<div
@@ -727,7 +727,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa
727727
completed: 2
728728
</span>
729729
<span>
730-
ongoing: 2
730+
ongoing: 0
731731
</span>
732732
</div>
733733
<div
@@ -878,7 +878,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa
878878
completed: 4
879879
</span>
880880
<span>
881-
ongoing: 4
881+
ongoing: 0
882882
</span>
883883
</div>
884884
<div

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('2024-12-15T13:00:02Z');
614+
expect(result).toBeInstanceOf(Date);
615+
});
616+
});

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ 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 parsed = dateString.replace(
191+
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,
192+
'$1-$2-$3T$4:$5:$6Z'
193+
);
194+
195+
const date = new Date(parsed);
196+
return isNaN(date.getTime()) ? null : date;
197+
};
198+
185199
export const sortTasksById = (tasks: Task[], order: 'asc' | 'desc') => {
186200
return tasks.sort((a, b) => {
187201
if (order === 'asc') {

0 commit comments

Comments
 (0)