From d5d8b8f1a70531370f0c2830ab7ff75e1b6605d9 Mon Sep 17 00:00:00 2001
From: Chris
Date: Sat, 7 Feb 2026 22:40:33 +0200
Subject: [PATCH] Fix date comparison bugs causing false past due warnings and
Today view miscategorization (#826)
---
.../components/Project/useProjectMetrics.ts | 90 ++++++++++---------
frontend/components/Task/TaskDetails.tsx | 24 ++---
frontend/components/Task/TasksToday.tsx | 23 +++--
frontend/utils/dateUtils.ts | 25 +++---
4 files changed, 89 insertions(+), 73 deletions(-)
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;
};
/**