Fix date comparison bugs causing false past due warnings and Today view miscategorization (#826)
This commit is contained in:
parent
3ee54dbdc7
commit
d5d8b8f1a7
4 changed files with 89 additions and 73 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue