Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions frontend/src/components/HomeComponents/Tasks/ReportChart.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show values above bars and removed cursor highlight effect (for better UX)

Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,21 @@ export const ReportChart: React.FC<ReportChartProps> = ({
<Tooltip
contentStyle={{ backgroundColor: '#333', border: 'none' }}
labelClassName="text-white"
cursor={false}
/>
<Legend wrapperClassName="text-white" />
<Bar dataKey="completed" fill="#E776CB" name="Completed" />
<Bar dataKey="ongoing" fill="#5FD9FA" name="Ongoing" />
<Bar
dataKey="completed"
fill="#E776CB"
name="Completed"
label={{ position: 'top', fill: 'white', fontSize: 12 }}
/>
<Bar
dataKey="ongoing"
fill="#5FD9FA"
name="Ongoing"
label={{ position: 'top', fill: 'white', fontSize: 12 }}
/>
</BarChart>
</ResponsiveContainer>
</div>
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/HomeComponents/Tasks/ReportsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using shared parseTaskwarriorDate utility


export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
const now = new Date();
Expand All @@ -16,10 +17,13 @@ export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
const countStatuses = (filterDate: Date) => {
return tasks
.filter((task) => {
const taskDateStr = task.modified || task.due;
const taskDateStr = task.end || task.due || task.entry;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completed tasks now use end date for filtering and entry as fallback for pending tasks without a due date

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(
Expand All @@ -36,9 +40,7 @@ export const ReportsView: React.FC<ReportsViewProps> = ({ 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) }];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed weekly report to use startOfWeek, now this ensures "This Week" actually represents the current calendar week, not just the last 7 days

const monthlyData = [{ name: 'This Month', ...countStatuses(startOfMonth) }];

return (
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored isOverdue to use the shared parseTaskwarriorDate utility

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
sortTasksById,
getTimeSinceLastSync,
hashKey,
parseTaskwarriorDate,
} from './tasks-utils';
import Pagination from './Pagination';
import { url } from '@/components/utils/URLs';
Expand Down Expand Up @@ -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();
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated mock task data to use valid dates

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated dates, test names, and fallbacks

Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<div data-testid={chartId}>
Expand Down Expand Up @@ -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(<ReportsView tasks={tasks} />);
Expand All @@ -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(<ReportsView tasks={tasks} />);
Expand All @@ -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),
}),
];

Expand All @@ -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',
}),
];
Expand All @@ -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,
}),
];
Expand All @@ -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,
}),
];

Expand All @@ -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(<ReportsView tasks={tasks} />);
Expand All @@ -216,7 +221,7 @@ describe('ReportsView', () => {
const tasks = [
createMockTask({
status: 'completed',
modified: taskInWeek.toISOString(),
end: toTWFormat(taskInWeek),
}),
];

Expand All @@ -238,7 +243,7 @@ describe('ReportsView', () => {
const tasks = [
createMockTask({
status: 'completed',
modified: taskInMonth.toISOString(),
end: toTWFormat(taskInMonth),
}),
];

Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests for parseTaskwarriorDate function

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
bulkMarkTasksAsDeleted,
getTimeSinceLastSync,
hashKey,
parseTaskwarriorDate,
} from '../tasks-utils';
import { Task } from '@/components/utils/types';

Expand Down Expand Up @@ -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);
});
});
17 changes: 17 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/tasks-utils.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

centralized Taskwarrior date parsing

Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Loading