diff --git a/frontend/components/Project/useProjectMetrics.ts b/frontend/components/Project/useProjectMetrics.ts index b1ce2f0..8a03101 100644 --- a/frontend/components/Project/useProjectMetrics.ts +++ b/frontend/components/Project/useProjectMetrics.ts @@ -7,6 +7,7 @@ import { isTaskPlanned, isTaskWaiting, } from '../../constants/taskStatus'; +import { parseDateString, getTodayDateString } from '../../utils/dateUtils'; // Check if task is in today's plan (has active status) const isTaskInTodayPlan = (task: Task): boolean => @@ -30,14 +31,14 @@ export const useProjectMetrics = ( dueSoon: 0, }; - const today = new Date(); - const startOfToday = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() - ); - const soonBoundary = new Date(startOfToday); - soonBoundary.setDate(startOfToday.getDate() + 7); + const todayStr = getTodayDateString(); + const soonBoundary = new Date(); + soonBoundary.setHours(0, 0, 0, 0); + soonBoundary.setDate(soonBoundary.getDate() + 7); + const soonYear = soonBoundary.getFullYear(); + const soonMonth = String(soonBoundary.getMonth() + 1).padStart(2, '0'); + const soonDay = String(soonBoundary.getDate()).padStart(2, '0'); + const soonStr = `${soonYear}-${soonMonth}-${soonDay}`; const isCompleted = (status: Task['status']) => status === 'done' || @@ -65,13 +66,11 @@ export const useProjectMetrics = ( } if (!isCompleted(status) && task.due_date) { - const dueDate = new Date(task.due_date); - if (!Number.isNaN(dueDate.getTime())) { - if (dueDate < startOfToday) { - stats.overdue += 1; - } else if (dueDate <= soonBoundary) { - stats.dueSoon += 1; - } + const dueDateStr = task.due_date.split('T')[0]; + if (dueDateStr < todayStr) { + stats.overdue += 1; + } else if (dueDateStr <= soonStr) { + stats.dueSoon += 1; } } }); @@ -125,16 +124,18 @@ export const useProjectMetrics = ( unscheduled: [] as Task[], }; - const now = new Date(); - const startOfToday = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() - ); - const weekBoundary = new Date(startOfToday); - weekBoundary.setDate(startOfToday.getDate() + 7); - const monthBoundary = new Date(startOfToday); - monthBoundary.setDate(startOfToday.getDate() + 30); + const todayStr = getTodayDateString(); + const toDateStr = (offsetDays: number): string => { + const d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() + offsetDays); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${dd}`; + }; + const weekStr = toDateStr(7); + const monthStr = toDateStr(30); const isCompleted = (status: Task['status']) => status === 'done' || @@ -150,17 +151,17 @@ export const useProjectMetrics = ( return; } - const due = new Date(task.due_date); - if (Number.isNaN(due.getTime())) { + const dueDateStr = task.due_date.split('T')[0]; + if (!dueDateStr) { buckets.unscheduled.push(task); return; } - if (due < startOfToday) { + if (dueDateStr < todayStr) { buckets.overdue.push(task); - } else if (due <= weekBoundary) { + } else if (dueDateStr <= weekStr) { buckets.week.push(task); - } else if (due <= monthBoundary) { + } else if (dueDateStr <= monthStr) { buckets.month.push(task); } else { buckets.unscheduled.push(task); @@ -262,7 +263,9 @@ export const useProjectMetrics = ( tasks.forEach((task) => { if (!task.due_date || isCompleted(task.status)) return; - const key = new Date(task.due_date).toISOString().split('T')[0]; + // Use the due_date string directly as key (YYYY-MM-DD format) + // This avoids timezone conversion issues with new Date().toISOString() + const key = task.due_date.split('T')[0]; if (counts[key] !== undefined) { counts[key] += 1; } @@ -344,15 +347,17 @@ export const useProjectMetrics = ( } if (task.due_date) { - const due = new Date(task.due_date); - const diffDays = Math.floor( - (due.getTime() - startOfToday.getTime()) / - (1000 * 60 * 60 * 24) - ); - if (diffDays < 0) score -= 25; - else if (diffDays === 0) score -= 20; - else if (diffDays <= 2) score -= 15; - else if (diffDays <= 7) score -= 10; + const due = parseDateString(task.due_date); + if (due) { + const diffDays = Math.floor( + (due.getTime() - startOfToday.getTime()) / + (1000 * 60 * 60 * 24) + ); + if (diffDays < 0) score -= 25; + else if (diffDays === 0) score -= 20; + else if (diffDays <= 2) score -= 15; + else if (diffDays <= 7) score -= 10; + } } score += getPriorityScore(task.priority); @@ -392,9 +397,8 @@ export const useProjectMetrics = ( now.getMonth(), now.getDate() ); - const due = new Date(task.due_date); - if (Number.isNaN(due.getTime())) - return t('tasks.noDue', 'No due date') as string; + const due = parseDateString(task.due_date); + if (!due) return t('tasks.noDue', 'No due date') as string; const diffDays = Math.floor( (due.getTime() - startOfToday.getTime()) / (1000 * 60 * 60 * 24) diff --git a/frontend/components/Task/TaskDetails.tsx b/frontend/components/Task/TaskDetails.tsx index 4586427..0f15925 100644 --- a/frontend/components/Task/TaskDetails.tsx +++ b/frontend/components/Task/TaskDetails.tsx @@ -31,7 +31,11 @@ import { TaskDeferUntilCard, TaskAttachmentsCard, } from './TaskDetails/'; -import { isTaskOverdueInTodayPlan, isTaskPastDue } from '../../utils/dateUtils'; +import { + isTaskOverdueInTodayPlan, + isTaskPastDue, + getTodayDateString, +} from '../../utils/dateUtils'; const TaskDetails: React.FC = () => { const { uid } = useParams<{ uid: string }>(); @@ -309,12 +313,10 @@ const TaskDetails: React.FC = () => { } if (editedDueDate) { - const dueDate = new Date(editedDueDate); - const today = new Date(); - today.setHours(0, 0, 0, 0); - dueDate.setHours(0, 0, 0, 0); + const todayStr = getTodayDateString(); + const dueDateStr = editedDueDate.split('T')[0]; - if (!isNaN(dueDate.getTime()) && dueDate < today) { + if (dueDateStr < todayStr) { showErrorToast( t( 'task.dueDateInPastWarning', @@ -490,7 +492,8 @@ const TaskDetails: React.FC = () => { } const currentCount = task?.subtasks?.length || 0; - const subtasksDisappeared = lastKnownSubtaskCount.current > 0 && currentCount === 0; + const subtasksDisappeared = + lastKnownSubtaskCount.current > 0 && currentCount === 0; const needsInitialLoad = !hasLoadedSubtasks && currentCount === 0; if (needsInitialLoad || subtasksDisappeared) { @@ -535,9 +538,7 @@ const TaskDetails: React.FC = () => { ) { try { setLoadingIterations(true); - const iterations = await fetchTaskNextIterations( - task.uid! - ); + const iterations = await fetchTaskNextIterations(task.uid!); setNextIterations(iterations); } catch (error) { console.error('Error loading next iterations:', error); @@ -630,7 +631,8 @@ const TaskDetails: React.FC = () => { if (uid) { const updatedTask = await fetchTaskByUid(uid); - lastKnownSubtaskCount.current = updatedTask.subtasks?.length || 0; + lastKnownSubtaskCount.current = + updatedTask.subtasks?.length || 0; const existingIndex = tasksStore.tasks.findIndex( (t: Task) => t.uid === uid ); diff --git a/frontend/components/Task/TasksToday.tsx b/frontend/components/Task/TasksToday.tsx index defae1f..28b9b62 100644 --- a/frontend/components/Task/TasksToday.tsx +++ b/frontend/components/Task/TasksToday.tsx @@ -6,6 +6,7 @@ import i18n from 'i18next'; import { useNavigate } from 'react-router-dom'; import { getLocalesPath, getApiPath } from '../../config/paths'; import { sortTasksByPriorityDueDateProject } from '../../utils/taskSortUtils'; +import { getTodayDateString } from '../../utils/dateUtils'; import { ClipboardDocumentListIcon, ArrowPathIcon, @@ -838,12 +839,10 @@ const TasksToday: React.FC = () => { (t) => t.id === updatedTask.id ) ) { - const today = new Date(); - const todayStr = format(today, 'yyyy-MM-dd'); - const dueDateStr = format( - new Date(updatedTask.due_date), - 'yyyy-MM-dd' - ); + const todayStr = getTodayDateString(); + const dueDateStr = (updatedTask.due_date || '').split( + 'T' + )[0]; if (dueDateStr === todayStr) { // Due today @@ -1248,7 +1247,9 @@ const TasksToday: React.FC = () => { {t('tasks.dueToday')}

-

0 ? 'text-red-500' : ''}`}> +

0 ? 'text-red-500' : ''}`} + > {metrics.tasks_due_today.length}

@@ -1261,7 +1262,9 @@ const TasksToday: React.FC = () => { {t('tasks.overdue', 'Overdue')}

-

0 ? 'text-red-500' : ''}`}> +

0 ? 'text-red-500' : ''}`} + > {metrics.tasks_overdue.length}

@@ -1331,7 +1334,9 @@ const TasksToday: React.FC = () => { )} -

0 ? 'text-green-500' : ''}`}> +

0 ? 'text-green-500' : ''}`} + > { metrics .tasks_completed_today diff --git a/frontend/utils/dateUtils.ts b/frontend/utils/dateUtils.ts index 8b7695c..5ea32eb 100644 --- a/frontend/utils/dateUtils.ts +++ b/frontend/utils/dateUtils.ts @@ -17,6 +17,14 @@ export const isTaskInTodayPlan = ( ): boolean => isTaskInProgress(status) || isTaskPlanned(status) || isTaskWaiting(status); +export const getTodayDateString = (): string => { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + let userTimezone: string | null = null; export const setUserTimezone = (timezone: string): void => { @@ -31,7 +39,9 @@ export const setUserTimezone = (timezone: string): void => { * @param dateString - Date string in YYYY-MM-DD format * @returns Date object at local midnight, or null if invalid */ -export const parseDateString = (dateString: string | null | undefined): Date | null => { +export const parseDateString = ( + dateString: string | null | undefined +): Date | null => { if (!dateString) return null; // Adding T00:00:00 makes JavaScript interpret the date as local time const date = new Date(dateString + 'T00:00:00'); @@ -91,15 +101,10 @@ export const isTaskPastDue = (task: { return false; } - // Check if due date is in the past - const dueDate = parseDateString(task.due_date); - if (!dueDate) return false; - - const today = new Date(); - today.setHours(0, 0, 0, 0); // Start of today - dueDate.setHours(0, 0, 0, 0); // Start of due date - - return dueDate < today; + // Check if due date is in the past using string comparison (YYYY-MM-DD) + const todayStr = getTodayDateString(); + const dueDateStr = task.due_date.split('T')[0]; + return dueDateStr < todayStr; }; /**