Fix date comparison bugs causing false past due warnings and Today view miscategorization (#826)

This commit is contained in:
Chris 2026-02-07 22:40:33 +02:00 committed by GitHub
parent 3ee54dbdc7
commit d5d8b8f1a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 89 additions and 73 deletions

View file

@ -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)

View file

@ -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
);

View file

@ -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')}
</p>
</div>
<p className={`text-sm font-semibold ${metrics.tasks_due_today.length > 0 ? 'text-red-500' : ''}`}>
<p
className={`text-sm font-semibold ${metrics.tasks_due_today.length > 0 ? 'text-red-500' : ''}`}
>
{metrics.tasks_due_today.length}
</p>
</div>
@ -1261,7 +1262,9 @@ const TasksToday: React.FC = () => {
{t('tasks.overdue', 'Overdue')}
</p>
</div>
<p className={`text-sm font-semibold ${metrics.tasks_overdue.length > 0 ? 'text-red-500' : ''}`}>
<p
className={`text-sm font-semibold ${metrics.tasks_overdue.length > 0 ? 'text-red-500' : ''}`}
>
{metrics.tasks_overdue.length}
</p>
</div>
@ -1331,7 +1334,9 @@ const TasksToday: React.FC = () => {
</div>
</div>
)}
<p className={`text-sm font-semibold ${metrics.tasks_completed_today.length > 0 ? 'text-green-500' : ''}`}>
<p
className={`text-sm font-semibold ${metrics.tasks_completed_today.length > 0 ? 'text-green-500' : ''}`}
>
{
metrics
.tasks_completed_today

View file

@ -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;
};
/**