Fix bug 733 (#735)

* Refactor today

* fixup! Refactor today

* fixup! fixup! Refactor today
This commit is contained in:
Chris 2025-12-27 21:00:52 +02:00 committed by GitHub
parent ad8ab3ec72
commit e73c354e7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 267 additions and 302 deletions

View file

@ -0,0 +1,30 @@
'use strict';
const {
safeRemoveColumn,
safeAddColumns,
} = require('../utils/migration-utils');
/**
* Migration to remove the deprecated 'today' column from tasks table.
* The 'today' field is no longer used - task visibility in the today view
* is now determined by status (in_progress, planned, waiting).
*
* @type {import('sequelize-cli').Migration}
*/
module.exports = {
async up(queryInterface, Sequelize) {
await safeRemoveColumn(queryInterface, 'tasks', 'today');
},
async down(queryInterface, Sequelize) {
await safeAddColumns(queryInterface, 'tasks', [
{
name: 'today',
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
]);
},
};

View file

@ -28,11 +28,6 @@ module.exports = (sequelize) => {
type: DataTypes.DATE,
allowNull: true,
},
today: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
priority: {
type: DataTypes.INTEGER,
allowNull: true,

View file

@ -28,7 +28,6 @@ function buildTaskAttributes(body, userId, timezone, isUpdate = false) {
defer_until: processDeferUntilForStorage(body.defer_until, timezone),
status: parseStatus(body.status),
note: body.note,
today: body.today !== undefined ? body.today : false,
recurrence_type: recurrenceType,
recurrence_interval: body.recurrence_interval || null,
recurrence_end_date: body.recurrence_end_date || null,
@ -81,7 +80,6 @@ function buildUpdateAttributes(body, task, timezone) {
? parseStatus(body.status)
: Task.STATUS.NOT_STARTED,
note: body.note,
today: body.today !== undefined ? body.today : task.today,
recurrence_type: recurrenceType,
recurrence_interval:
body.recurrence_interval !== undefined

View file

@ -457,7 +457,6 @@ router.patch('/task/:uid', requireTaskWriteAccess, async (req, res) => {
tags,
Tags,
subtasks,
today,
recurrence_type,
recurrence_interval,
recurrence_end_date,
@ -539,15 +538,6 @@ router.patch('/task/:uid', requireTaskWriteAccess, async (req, res) => {
return res.status(400).json({ error: error.message });
}
if (
today !== undefined &&
task.today === true &&
today === false &&
task.status === Task.STATUS.IN_PROGRESS
) {
taskAttributes.status = Task.STATUS.NOT_STARTED;
}
await handleCompletionStatus(taskAttributes, status, task);
if (project_id !== undefined) {
@ -729,22 +719,6 @@ router.patch('/task/:uid', requireTaskWriteAccess, async (req, res) => {
req.currentUser.id
);
if (today !== undefined && today !== oldValues.today) {
try {
await logEvent({
taskId: task.id,
userId: req.currentUser.id,
eventType: 'today_changed',
fieldName: 'today',
oldValue: oldValues.today,
newValue: today,
metadata: { source: 'web', action: 'update_today' },
});
} catch (eventError) {
logError('Error logging today change event:', eventError);
}
}
const taskWithAssociations = await taskRepository.findById(task.id, {
include: TASK_INCLUDES,
});

View file

@ -73,7 +73,6 @@ async function createSubtasks(parentTaskId, subtasks, userId) {
? new Date(subtask.completed_at)
: new Date()
: null,
today: subtask.today || false,
recurrence_type: 'none',
completion_based: false,
order: maxOrder + index + 1, // Assign sequential order values

View file

@ -77,6 +77,7 @@ async function computeSuggestedTasks(
totalOpenTasks,
tasksInProgress,
tasksDueToday,
tasksOverdue,
todayPlanTasks
) {
if (
@ -90,6 +91,7 @@ async function computeSuggestedTasks(
const excludedTaskIds = [
...tasksInProgress.map((t) => t.id),
...tasksDueToday.map((t) => t.id),
...tasksOverdue.map((t) => t.id),
...todayPlanTasks.map((t) => t.id),
];
@ -225,6 +227,7 @@ async function computeTaskMetrics(
totalOpenTasks,
tasksInProgress,
tasksDueToday,
tasksOverdue,
todayPlanTasks
);

View file

@ -7,6 +7,22 @@ const {
} = require('../../../utils/timezone-utils');
const { getTaskIncludeConfig } = require('./query-builders');
// Statuses that indicate a task is in the "today plan" (actively being worked on)
// Used to exclude from overdue/due-today sections to avoid duplicates
const TODAY_PLAN_STATUSES = [
Task.STATUS.IN_PROGRESS,
Task.STATUS.WAITING,
Task.STATUS.PLANNED,
'in_progress',
'waiting',
'planned',
];
// Helper to check if a task is in the today plan based on status
function isTaskInTodayPlan(task) {
return TODAY_PLAN_STATUSES.includes(task.status);
}
async function countTotalOpenTasks(visibleTasksWhere) {
return await Task.count({
where: {
@ -125,11 +141,11 @@ async function fetchTasksDueToday(visibleTasksWhere, userTimezone) {
Task.STATUS.ARCHIVED,
'done',
'archived',
...TODAY_PLAN_STATUSES,
],
},
parent_task_id: null,
recurring_parent_id: null,
today: { [Op.or]: [false, null] },
[Op.or]: [
{
due_date: {
@ -173,11 +189,12 @@ async function fetchOverdueTasks(visibleTasksWhere, userTimezone) {
Task.STATUS.ARCHIVED,
'done',
'archived',
// Exclude tasks in today plan (they show in Planned section)
...TODAY_PLAN_STATUSES,
],
},
parent_task_id: null,
recurring_parent_id: null,
today: { [Op.or]: [false, null] },
[Op.or]: [
{ due_date: { [Op.lt]: todayBounds.start } },
sequelize.literal(`EXISTS (

View file

@ -106,16 +106,19 @@ async function filterTasksByParams(
const safeTimezone = getSafeTimezone(userTimezone);
const todayBounds = getTodayBoundsInUTC(safeTimezone);
whereClause.status = {
[Op.notIn]: [
Task.STATUS.DONE,
Task.STATUS.ARCHIVED,
'done',
'archived',
],
};
// Tasks in today view are those with active statuses (in_progress, planned, waiting)
const todayPlanStatuses = [
Task.STATUS.IN_PROGRESS,
Task.STATUS.WAITING,
Task.STATUS.PLANNED,
'in_progress',
'waiting',
'planned',
];
whereClause[Op.or] = [
{
// Non-recurring tasks with active status
[Op.and]: [
{
[Op.or]: [
@ -124,18 +127,20 @@ async function filterTasksByParams(
],
},
{ recurring_parent_id: null },
{ today: true },
{ status: { [Op.in]: todayPlanStatuses } },
],
},
{
// Recurring parent tasks with active status
[Op.and]: [
{ recurrence_type: { [Op.ne]: 'none' } },
{ recurrence_type: { [Op.ne]: null } },
{ recurring_parent_id: null },
{ today: true },
{ status: { [Op.in]: todayPlanStatuses } },
],
},
{
// Recurring instances due today
[Op.and]: [
{ recurring_parent_id: { [Op.ne]: null } },
{

View file

@ -13,7 +13,6 @@ function captureOldValues(task) {
defer_until: task.defer_until,
project_id: task.project_id,
note: task.note,
today: task.today,
recurrence_type: task.recurrence_type,
recurrence_interval: task.recurrence_interval,
recurrence_end_date: task.recurrence_end_date,

View file

@ -418,7 +418,6 @@ async function importUserData(userId, backupData, options = { merge: true }) {
name: taskData.name,
due_date: taskData.due_date,
defer_until: taskData.defer_until,
today: taskData.today,
priority: taskData.priority,
status: taskData.status,
note: taskData.note,

View file

@ -95,4 +95,117 @@ describe('Task Metrics Suggested Tasks', () => {
expect(names).toContain('Deferred Past Task');
expect(names).not.toContain('Deferred Future Task');
});
it('excludes overdue tasks from suggested results (they show in Overdue section)', async () => {
// Need at least 3 open tasks for suggested to compute
await createTask({ name: 'Ready Task 1', priority: 'high' });
await createTask({ name: 'Ready Task 2', priority: 'medium' });
await createTask({ name: 'Ready Task 3', priority: 'low' });
await createTask({
name: 'Overdue Task',
due_date: dayFromNow(-3),
});
const metrics = await getTaskMetrics(user.id, 'UTC');
const suggestedNames = metrics.suggested_tasks.map((task) => task.name);
const overdueNames = metrics.tasks_overdue.map((task) => task.name);
// Overdue task should be in overdue section, not suggested
expect(overdueNames).toContain('Overdue Task');
expect(suggestedNames).not.toContain('Overdue Task');
});
});
describe('Task Metrics Overdue and Due Today Tasks', () => {
let user;
const createTask = async (overrides = {}) => {
const { priority, status, ...rest } = overrides;
return await Task.create({
name: rest.name || 'Test task',
user_id: user.id,
status:
typeof status === 'string'
? Task.getStatusValue(status)
: (status ?? Task.STATUS.NOT_STARTED),
priority:
typeof priority === 'string'
? Task.getPriorityValue(priority)
: (priority ?? Task.PRIORITY.LOW),
parent_task_id: null,
recurring_parent_id: null,
...rest,
});
};
beforeEach(async () => {
user = await createTestUser({ email: 'overdue-test@example.com' });
});
it('excludes overdue tasks with active status from tasks_overdue (they show in Planned)', async () => {
// Create an overdue task with IN_PROGRESS status (shows in Planned section)
await createTask({
name: 'Overdue In Progress',
due_date: dayFromNow(-3),
status: Task.STATUS.IN_PROGRESS,
});
// Create a regular overdue task with NOT_STARTED status
await createTask({
name: 'Regular Overdue Task',
due_date: dayFromNow(-2),
status: Task.STATUS.NOT_STARTED,
});
const metrics = await getTaskMetrics(user.id, 'UTC');
const overdueNames = metrics.tasks_overdue.map((task) => task.name);
// IN_PROGRESS task should NOT be in overdue (it's in Planned section)
expect(overdueNames).not.toContain('Overdue In Progress');
// NOT_STARTED task should be in overdue
expect(overdueNames).toContain('Regular Overdue Task');
});
it('excludes due today tasks with active status from tasks_due_today (they show in Planned)', async () => {
const today = new Date();
today.setHours(12, 0, 0, 0);
// Create a task due today with PLANNED status (shows in Planned section)
await createTask({
name: 'Due Today Planned',
due_date: today,
status: Task.STATUS.PLANNED,
});
// Create a regular due today task with NOT_STARTED status
await createTask({
name: 'Regular Due Today',
due_date: today,
status: Task.STATUS.NOT_STARTED,
});
const metrics = await getTaskMetrics(user.id, 'UTC');
const dueTodayNames = metrics.tasks_due_today.map((task) => task.name);
// PLANNED task should NOT be in due today (it's in Planned section)
expect(dueTodayNames).not.toContain('Due Today Planned');
// NOT_STARTED task should be in due today
expect(dueTodayNames).toContain('Regular Due Today');
});
it('includes tasks with WAITING status in Planned section, not in overdue', async () => {
await createTask({
name: 'Overdue Waiting',
due_date: dayFromNow(-1),
status: Task.STATUS.WAITING,
});
const metrics = await getTaskMetrics(user.id, 'UTC');
const overdueNames = metrics.tasks_overdue.map((task) => task.name);
const todayPlanNames = metrics.tasks_today_plan.map(
(task) => task.name
);
// WAITING task should be in today plan, not in overdue
expect(overdueNames).not.toContain('Overdue Waiting');
expect(todayPlanNames).toContain('Overdue Waiting');
});
});

View file

@ -77,13 +77,13 @@ describe('Tasks Routes', () => {
task1 = await Task.create({
name: 'Task 1',
user_id: user.id,
today: true,
status: Task.STATUS.IN_PROGRESS, // Active status shows in today view
});
task2 = await Task.create({
name: 'Task 2',
user_id: user.id,
today: false,
status: Task.STATUS.NOT_STARTED, // Not active, won't show in today view
});
});
@ -97,7 +97,7 @@ describe('Tasks Routes', () => {
expect(response.body.tasks.map((t) => t.id)).toContain(task2.id);
});
it('should filter today tasks (returns only tasks with today=true)', async () => {
it('should filter today tasks (returns tasks with active status)', async () => {
const response = await agent.get('/api/tasks?type=today');
expect(response.status).toBe(200);

View file

@ -22,7 +22,6 @@ describe('Task Model', () => {
expect(task.name).toBe(taskData.name);
expect(task.user_id).toBe(user.id);
expect(task.today).toBe(false);
expect(task.priority).toBe(0);
expect(task.status).toBe(0);
expect(task.recurrence_type).toBe('none');
@ -127,7 +126,6 @@ describe('Task Model', () => {
user_id: user.id,
});
expect(task.today).toBe(false);
expect(task.priority).toBe(0);
expect(task.status).toBe(0);
expect(task.recurrence_type).toBe('none');
@ -158,7 +156,6 @@ describe('Task Model', () => {
const task = await Task.create({
name: 'Test Task',
due_date: dueDate,
today: true,
priority: Task.PRIORITY.HIGH,
status: Task.STATUS.IN_PROGRESS,
note: 'Test Note',
@ -166,7 +163,6 @@ describe('Task Model', () => {
});
expect(task.due_date).toEqual(dueDate);
expect(task.today).toBe(true);
expect(task.priority).toBe(Task.PRIORITY.HIGH);
expect(task.status).toBe(Task.STATUS.IN_PROGRESS);
expect(task.note).toBe('Test Note');

View file

@ -267,7 +267,7 @@ const HabitDetails: React.FC = () => {
habit_flexibility_mode: editableValues.habit_flexibility_mode,
recurrence_type: 'daily',
recurrence_interval: 1,
today: true,
status: 'planned', // Show in today's plan
};
const created = await createHabit(habitData);

View file

@ -37,10 +37,10 @@ const HabitModal: React.FC<HabitModalProps> = ({
const handleSave = async () => {
try {
// Set today flag for new habits to show in "Planned" section
// Set planned status for new habits to show in "Planned" section
const habitData = { ...formData };
if (!habit?.uid) {
habitData.today = true;
habitData.status = 'planned';
}
if (habit?.uid) {

View file

@ -25,11 +25,7 @@ import {
deleteProject,
fetchProjects,
} from '../../utils/projectsService';
import {
createTask,
deleteTask,
toggleTaskToday,
} from '../../utils/tasksService';
import { createTask, deleteTask } from '../../utils/tasksService';
import {
updateNote,
deleteNote as apiDeleteNote,
@ -379,40 +375,6 @@ const ProjectDetails: React.FC = () => {
);
};
const handleToggleToday = async (taskId: number, task?: Task) => {
try {
const updatedTask = await toggleTaskToday(taskId, task);
setTasks((prev) =>
prev.map((t) =>
t.id === taskId
? {
...t,
today: updatedTask.today,
today_move_count: updatedTask.today_move_count,
}
: t
)
);
} catch {
if (!uidSlug) return;
try {
const projectData = await fetchProjectBySlug(uidSlug);
setProject(projectData);
setTasks(projectData.tasks || projectData.Tasks || []);
const fetchedNotes =
projectData.notes || projectData.Notes || [];
setNotes(
fetchedNotes.map((note) => {
if (note.Tags && !note.tags) note.tags = note.Tags;
return note;
})
);
} catch {
// silent
}
}
};
const handleSaveProject = async (updatedProject: Project) => {
if (!updatedProject.uid) return;
const savedProject = await updateProject(
@ -1059,7 +1021,7 @@ const ProjectDetails: React.FC = () => {
handleTaskCompletionToggle
}
onTaskDelete={handleTaskDelete}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
allProjects={allProjects}
showCompleted={
taskStatusFilter !== 'active'

View file

@ -1,6 +1,17 @@
import React from 'react';
import { Task } from '../../entities/Task';
import { TFunction } from 'i18next';
import {
isTaskInProgress,
isTaskPlanned,
isTaskWaiting,
} from '../../constants/taskStatus';
// Check if task is in today's plan (has active status)
const isTaskInTodayPlan = (task: Task): boolean =>
isTaskInProgress(task.status) ||
isTaskPlanned(task.status) ||
isTaskWaiting(task.status);
interface DueBuckets {
overdue: Task[];
@ -336,7 +347,7 @@ const ProjectInsightsPanel: React.FC<ProjectInsightsPanelProps> = ({
{String(nextBestAction.priority)}
</span>
)}
{nextBestAction.today && (
{isTaskInTodayPlan(nextBestAction) && (
<span className="px-2 py-1 rounded-full bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-200">
{t('tasks.todayPlan', 'Today plan')}
</span>
@ -355,19 +366,19 @@ const ProjectInsightsPanel: React.FC<ProjectInsightsPanelProps> = ({
disabled={
(nextBestAction.status === 'in_progress' ||
nextBestAction.status === 1) &&
nextBestAction.today
isTaskInTodayPlan(nextBestAction)
}
className={`inline-flex items-center justify-center px-3 py-1.5 text-sm font-medium text-white rounded-md transition-colors ${
(nextBestAction.status === 'in_progress' ||
nextBestAction.status === 1) &&
nextBestAction.today
isTaskInTodayPlan(nextBestAction)
? 'bg-gray-400 dark:bg-gray-700 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{(nextBestAction.status === 'in_progress' ||
nextBestAction.status === 1) &&
nextBestAction.today
isTaskInTodayPlan(nextBestAction)
? t('tasks.inProgress', 'In progress')
: t('tasks.startNow', 'Start now')}
</button>

View file

@ -2,6 +2,17 @@ import { useMemo, useCallback } from 'react';
import React from 'react';
import { Task } from '../../entities/Task';
import { TFunction } from 'i18next';
import {
isTaskInProgress,
isTaskPlanned,
isTaskWaiting,
} from '../../constants/taskStatus';
// Check if task is in today's plan (has active status)
const isTaskInTodayPlan = (task: Task): boolean =>
isTaskInProgress(task.status) ||
isTaskPlanned(task.status) ||
isTaskWaiting(task.status);
export const useProjectMetrics = (
tasks: Task[],
@ -346,7 +357,7 @@ export const useProjectMetrics = (
score += getPriorityScore(task.priority);
if (task.today) {
if (isTaskInTodayPlan(task)) {
score -= 6;
}
@ -419,9 +430,8 @@ export const useProjectMetrics = (
const isAlreadyInProgress =
nextBestAction.status === 'in_progress' ||
nextBestAction.status === 1;
const isAlreadyToday = !!nextBestAction.today;
if (isAlreadyInProgress && isAlreadyToday) {
if (isAlreadyInProgress) {
return;
}
@ -429,7 +439,6 @@ export const useProjectMetrics = (
await handleTaskUpdate({
...nextBestAction,
status: 'in_progress',
today: true,
});
if (showSuccessToast) {

View file

@ -312,30 +312,6 @@ const TagDetails: React.FC = () => {
}
};
const handleToggleToday = async (taskId: number, task?: Task) => {
try {
// Use the proper service function that includes auth
const { toggleTaskToday } = await import(
'../../utils/tasksService'
);
const updatedTask = await toggleTaskToday(taskId, task);
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === taskId
? {
...task,
today: updatedTask.today,
today_move_count: updatedTask.today_move_count,
}
: task
)
);
} catch (error) {
console.error('Error toggling today status:', error);
}
};
const handleTaskCompletionToggle = (updatedTask: Task) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
@ -733,7 +709,7 @@ const TagDetails: React.FC = () => {
onTaskDelete={handleTaskDelete}
projects={projectLookupList}
hideProjectName={false}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompletedTasks}
searchQuery={taskSearchQuery}
/>
@ -747,7 +723,7 @@ const TagDetails: React.FC = () => {
onTaskDelete={handleTaskDelete}
projects={projectLookupList}
hideProjectName={false}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompletedTasks}
/>
)

View file

@ -88,11 +88,10 @@ const NextTaskSuggestion: React.FC<NextTaskSuggestionProps> = ({
setIsUpdating(true);
try {
// Universal rule: when setting status to in_progress, also add to today
// Setting status to in_progress makes it appear in today's plan
const updatedTask = {
...suggestedTask,
status: 'in_progress' as const,
today: true,
};
await onTaskUpdate(updatedTask);
showSuccessToast(

View file

@ -30,7 +30,7 @@ import {
TaskDeferUntilCard,
TaskAttachmentsCard,
} from './TaskDetails/';
import { isTaskOverdue, isTaskPastDue } from '../../utils/dateUtils';
import { isTaskOverdueInTodayPlan, isTaskPastDue } from '../../utils/dateUtils';
const TaskDetails: React.FC = () => {
const { uid } = useParams<{ uid: string }>();
@ -163,7 +163,7 @@ const TaskDetails: React.FC = () => {
setIsEditingRecurrence(true);
};
const isOverdue = task ? isTaskOverdue(task) : false;
const isOverdue = task ? isTaskOverdueInTodayPlan(task) : false;
const isPastDue = task ? isTaskPastDue(task) : false;
useEffect(() => {
@ -758,8 +758,7 @@ const TaskDetails: React.FC = () => {
try {
const nextStatusPayload: Task = {
...task,
status: isCurrentlyInProgress ? 0 : 1,
today: isCurrentlyInProgress ? task.today : true,
status: isCurrentlyInProgress ? 0 : 1, // 0=not_started, 1=in_progress
};
await updateTask(task.uid, nextStatusPayload);

View file

@ -141,7 +141,7 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
);
};
import { toggleTaskCompletion, fetchSubtasks } from '../../utils/tasksService';
import { isTaskOverdue } from '../../utils/dateUtils';
import { isTaskOverdueInTodayPlan } from '../../utils/dateUtils';
import { useTranslation } from 'react-i18next';
import ConfirmDialog from '../Shared/ConfirmDialog';
import { getApiPath } from '../../config/paths';
@ -366,7 +366,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
const isInProgress = task.status === 'in_progress' || task.status === 1;
// Check if task is overdue (created yesterday or earlier and not completed)
const isOverdue = isTaskOverdue(task);
const isOverdue = isTaskOverdueInTodayPlan(task);
const priorityBorderClass = isTaskCompleted(task.status)
? 'border-l-4 border-l-green-500'

View file

@ -20,16 +20,14 @@ import {
QueueListIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
import {
fetchTasks,
updateTask,
deleteTask,
toggleTaskToday,
} from '../../utils/tasksService';
import { fetchTasks, updateTask, deleteTask } from '../../utils/tasksService';
import {
isTaskDone,
isTaskActive,
isHabitArchived,
isTaskInProgress,
isTaskPlanned,
isTaskWaiting,
} from '../../constants/taskStatus';
import { fetchProjects } from '../../utils/projectsService';
import { Task } from '../../entities/Task';
@ -779,10 +777,12 @@ const TasksToday: React.FC = () => {
}
} else {
// If not completed, add to relevant active lists
if (
updatedTask.today &&
updatedTask.status !== 'cancelled'
) {
// Check if task has "today plan" status (in_progress, planned, or waiting)
const isInTodayPlan =
isTaskInProgress(updatedTask.status) ||
isTaskPlanned(updatedTask.status) ||
isTaskWaiting(updatedTask.status);
if (isInTodayPlan && updatedTask.status !== 'cancelled') {
newMetrics.today_plan_tasks = updateOrAddTask(
newMetrics.today_plan_tasks,
updatedTask
@ -826,8 +826,13 @@ const TasksToday: React.FC = () => {
);
}
}
// Task is suggested if not in today plan, no project, and no due date
const notInTodayPlan =
!isTaskInProgress(updatedTask.status) &&
!isTaskPlanned(updatedTask.status) &&
!isTaskWaiting(updatedTask.status);
const isSuggested =
!updatedTask.today &&
notInTodayPlan &&
!updatedTask.project_id &&
!updatedTask.due_date;
const isActive = isTaskActive(updatedTask.status);
@ -970,35 +975,6 @@ const TasksToday: React.FC = () => {
[]
);
const handleToggleToday = useCallback(
async (taskId: number, task?: Task): Promise<void> => {
if (!isMounted.current) return;
try {
await toggleTaskToday(taskId, task);
// Reload tasks to reflect the change
const result = await fetchTasks('?type=today');
if (isMounted.current) {
useStore.getState().tasksStore.setTasks(result.tasks);
setMetrics({
...result.metrics,
tasks_in_progress: result.tasks_in_progress || [],
tasks_due_today: result.tasks_due_today || [],
tasks_overdue: result.tasks_overdue || [],
today_plan_tasks: result.tasks_today_plan || [],
suggested_tasks: result.suggested_tasks || [],
tasks_completed_today:
result.tasks_completed_today || [],
} as any);
}
} catch (error) {
console.error('Error toggling task today status:', error);
}
},
[]
);
const handleTaskCompletionToggle = useCallback(
async (updatedTask: Task): Promise<void> => {
if (!isMounted.current) return;
@ -1426,7 +1402,7 @@ const TasksToday: React.FC = () => {
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
@ -1520,7 +1496,7 @@ const TasksToday: React.FC = () => {
projects={localProjects}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
@ -1615,7 +1591,7 @@ const TasksToday: React.FC = () => {
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
@ -1717,7 +1693,7 @@ const TasksToday: React.FC = () => {
}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
/>
{suggestedDisplayLimit <
@ -1802,7 +1778,7 @@ const TasksToday: React.FC = () => {
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={true}
/>

View file

@ -8,11 +8,7 @@ import NewTask from './Task/NewTask';
import { Task } from '../entities/Task';
import { getTitleAndIcon } from './Task/getTitleAndIcon';
import { getDescription } from './Task/getDescription';
import {
createTask,
toggleTaskToday,
GroupedTasks,
} from '../utils/tasksService';
import { createTask, GroupedTasks } from '../utils/tasksService';
import { useStore } from '../store/useStore';
import { useToast } from './Shared/ToastContext';
import { SortOption } from './Shared/SortFilterButton';
@ -465,37 +461,6 @@ const Tasks: React.FC = () => {
}
};
const handleToggleToday = async (
taskId: number,
task?: Task
): Promise<void> => {
try {
await toggleTaskToday(taskId, task);
const params = new URLSearchParams(location.search);
const type = params.get('type') || 'all';
const tag = params.get('tag');
const project = params.get('project');
const priority = params.get('priority');
let apiPath = `tasks?type=${type}&order_by=${orderBy}`;
if (tag) apiPath += `&tag=${tag}`;
if (project) apiPath += `&project=${project}`;
if (priority) apiPath += `&priority=${priority}`;
const response = await fetch(getApiPath(apiPath), {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setTasks(data.tasks || data);
}
} catch (error) {
console.error('Error toggling task today status:', error);
setError('Error toggling task today status.');
}
};
const handleSortChange = (order: string) => {
setOrderBy(order);
localStorage.setItem('order_by', order);
@ -952,7 +917,7 @@ const Tasks: React.FC = () => {
onTaskDelete={handleTaskDelete}
projects={projects}
hideProjectName={false}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompleted}
searchQuery={taskSearchQuery}
/>
@ -966,7 +931,7 @@ const Tasks: React.FC = () => {
}
onTaskDelete={handleTaskDelete}
projects={projects}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompleted}
/>
)}

View file

@ -567,27 +567,6 @@ const ViewDetail: React.FC = () => {
}
};
const handleToggleToday = async (taskId: number, task?: Task) => {
try {
const { toggleTaskToday } = await import('../utils/tasksService');
const updatedTask = await toggleTaskToday(taskId, task);
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === taskId
? {
...task,
today: updatedTask.today,
today_move_count: updatedTask.today_move_count,
}
: task
)
);
} catch (error) {
console.error('Error toggling today status:', error);
}
};
const handleTaskCompletionToggle = (updatedTask: Task) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
@ -1159,7 +1138,7 @@ const ViewDetail: React.FC = () => {
onTaskDelete={handleTaskDelete}
projects={projectLookupList}
hideProjectName={false}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompletedTasks}
searchQuery={taskSearchQuery}
/>
@ -1173,7 +1152,7 @@ const ViewDetail: React.FC = () => {
onTaskDelete={handleTaskDelete}
projects={projectLookupList}
hideProjectName={false}
onToggleToday={handleToggleToday}
onToggleToday={undefined}
showCompletedTasks={showCompletedTasks}
/>
)}

View file

@ -12,8 +12,6 @@ export interface Task {
due_date?: string;
defer_until?: string;
note?: string;
today?: boolean;
today_move_count?: number;
tags?: Tag[];
project_id?: number;
Project?: Project;

View file

@ -65,7 +65,6 @@ interface TasksStore {
updateTask: (taskUid: string, taskData: Task) => Promise<Task>;
deleteTask: (taskUid: string) => Promise<void>;
toggleTaskCompletion: (taskUid: string) => Promise<Task>;
toggleTaskToday: (taskId: number) => Promise<Task>;
loadTaskById: (taskId: number) => Promise<Task>;
loadTaskByUid: (uid: string) => Promise<Task>;
loadSubtasks: (parentTaskUid: string) => Promise<Task[]>;
@ -454,33 +453,6 @@ export const useStore = create<StoreState>((set: any) => ({
throw error;
}
},
toggleTaskToday: async (taskId) => {
const { toggleTaskToday } = await import('../utils/tasksService');
try {
const currentTask = useStore
.getState()
.tasksStore.tasks.find((t) => t.id === taskId);
const updatedTask = await toggleTaskToday(taskId, currentTask);
set((state) => ({
tasksStore: {
...state.tasksStore,
tasks: state.tasksStore.tasks.map((task) =>
task.id === taskId ? updatedTask : task
),
},
}));
return updatedTask;
} catch (error) {
console.error(
'toggleTaskToday: Failed to toggle task today status:',
error
);
set((state) => ({
tasksStore: { ...state.tasksStore, isError: true },
}));
throw error;
}
},
loadTaskById: async (taskId) => {
const { fetchTaskById } = await import('../utils/tasksService');
try {

View file

@ -4,6 +4,18 @@ import { enUS } from 'date-fns/locale/en-US';
import { es } from 'date-fns/locale/es';
import { el } from 'date-fns/locale/el';
import i18n from '../i18n';
import {
isTaskInProgress,
isTaskPlanned,
isTaskWaiting,
} from '../constants/taskStatus';
import { StatusType } from '../entities/Task';
// Check if task is in today's plan (has active status)
export const isTaskInTodayPlan = (
status: StatusType | number | undefined | null
): boolean =>
isTaskInProgress(status) || isTaskPlanned(status) || isTaskWaiting(status);
let userTimezone: string | null = null;
@ -198,19 +210,17 @@ export const formatDateTime = (
* @param task - The task to check
* @returns True if the task is likely overdue in today plan, false otherwise
*/
export const isTaskOverdue = (task: {
today?: boolean;
export const isTaskOverdueInTodayPlan = (task: {
created_at?: string;
today_move_count?: number;
status: string | number;
status: StatusType | number;
completed_at: string | null;
}): boolean => {
// If task is not in today plan, it's not overdue
if (!task.today) {
// If task is not in today plan (no active status), it's not overdue in today plan
if (!isTaskInTodayPlan(task.status)) {
return false;
}
// Only hide overdue badge if task is actually completed (done/archived), not just in progress
// Only hide overdue badge if task is actually completed (done/archived)
if (
task.completed_at ||
task.status === 'done' ||
@ -221,11 +231,6 @@ export const isTaskOverdue = (task: {
return false;
}
// If task has been moved to today multiple times, it's likely been sitting around
if (task.today_move_count && task.today_move_count > 1) {
return true;
}
// If no creation date, can't determine if overdue
if (!task.created_at) {
return false;

View file

@ -174,20 +174,6 @@ export const fetchSubtasks = async (parentTaskUid: string): Promise<Task[]> => {
return await response.json();
};
export const toggleTaskToday = async (
taskId: number,
currentTask?: Task
): Promise<Task> => {
if (!currentTask) {
currentTask = await fetchTaskById(taskId);
}
return await updateTask(currentTask.uid!, {
...currentTask,
today: !currentTask.today,
});
};
export interface TaskIteration {
date: string;
utc_date: string;