diff --git a/backend/migrations/20251227000001-remove-today-column.js b/backend/migrations/20251227000001-remove-today-column.js new file mode 100644 index 0000000..a7befd8 --- /dev/null +++ b/backend/migrations/20251227000001-remove-today-column.js @@ -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, + }, + ]); + }, +}; diff --git a/backend/models/task.js b/backend/models/task.js index dbbaf47..36c433b 100644 --- a/backend/models/task.js +++ b/backend/models/task.js @@ -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, diff --git a/backend/routes/tasks/core/builders.js b/backend/routes/tasks/core/builders.js index 254f5eb..55f9955 100644 --- a/backend/routes/tasks/core/builders.js +++ b/backend/routes/tasks/core/builders.js @@ -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 diff --git a/backend/routes/tasks/index.js b/backend/routes/tasks/index.js index d25afe0..df19cc7 100644 --- a/backend/routes/tasks/index.js +++ b/backend/routes/tasks/index.js @@ -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, }); diff --git a/backend/routes/tasks/operations/subtasks.js b/backend/routes/tasks/operations/subtasks.js index b52afe6..76d6cde 100644 --- a/backend/routes/tasks/operations/subtasks.js +++ b/backend/routes/tasks/operations/subtasks.js @@ -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 diff --git a/backend/routes/tasks/queries/metrics-computation.js b/backend/routes/tasks/queries/metrics-computation.js index 8ae8ad9..6cc9d2c 100644 --- a/backend/routes/tasks/queries/metrics-computation.js +++ b/backend/routes/tasks/queries/metrics-computation.js @@ -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 ); diff --git a/backend/routes/tasks/queries/metrics-queries.js b/backend/routes/tasks/queries/metrics-queries.js index bce0ff9..6f38be9 100644 --- a/backend/routes/tasks/queries/metrics-queries.js +++ b/backend/routes/tasks/queries/metrics-queries.js @@ -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 ( diff --git a/backend/routes/tasks/queries/query-builders.js b/backend/routes/tasks/queries/query-builders.js index 2e17a50..d6bca24 100644 --- a/backend/routes/tasks/queries/query-builders.js +++ b/backend/routes/tasks/queries/query-builders.js @@ -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 } }, { diff --git a/backend/routes/tasks/utils/logging.js b/backend/routes/tasks/utils/logging.js index 3e724c8..a9f3428 100644 --- a/backend/routes/tasks/utils/logging.js +++ b/backend/routes/tasks/utils/logging.js @@ -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, diff --git a/backend/services/backupService.js b/backend/services/backupService.js index f78797a..114611d 100644 --- a/backend/services/backupService.js +++ b/backend/services/backupService.js @@ -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, diff --git a/backend/tests/integration/tasks-metrics.test.js b/backend/tests/integration/tasks-metrics.test.js index e8c494a..bfd7e13 100644 --- a/backend/tests/integration/tasks-metrics.test.js +++ b/backend/tests/integration/tasks-metrics.test.js @@ -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'); + }); }); diff --git a/backend/tests/integration/tasks.test.js b/backend/tests/integration/tasks.test.js index 3540fb2..b85465b 100644 --- a/backend/tests/integration/tasks.test.js +++ b/backend/tests/integration/tasks.test.js @@ -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); diff --git a/backend/tests/unit/models/task.test.js b/backend/tests/unit/models/task.test.js index 4982576..5df2120 100644 --- a/backend/tests/unit/models/task.test.js +++ b/backend/tests/unit/models/task.test.js @@ -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'); diff --git a/frontend/components/Habits/HabitDetails.tsx b/frontend/components/Habits/HabitDetails.tsx index 84ad786..67ece4d 100644 --- a/frontend/components/Habits/HabitDetails.tsx +++ b/frontend/components/Habits/HabitDetails.tsx @@ -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); diff --git a/frontend/components/Habits/HabitModal.tsx b/frontend/components/Habits/HabitModal.tsx index d74c478..659394e 100644 --- a/frontend/components/Habits/HabitModal.tsx +++ b/frontend/components/Habits/HabitModal.tsx @@ -37,10 +37,10 @@ const HabitModal: React.FC = ({ 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) { diff --git a/frontend/components/Project/ProjectDetails.tsx b/frontend/components/Project/ProjectDetails.tsx index 1a4d065..c7ae6f1 100644 --- a/frontend/components/Project/ProjectDetails.tsx +++ b/frontend/components/Project/ProjectDetails.tsx @@ -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' diff --git a/frontend/components/Project/ProjectInsightsPanel.tsx b/frontend/components/Project/ProjectInsightsPanel.tsx index 33bde84..3e158ed 100644 --- a/frontend/components/Project/ProjectInsightsPanel.tsx +++ b/frontend/components/Project/ProjectInsightsPanel.tsx @@ -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 = ({ {String(nextBestAction.priority)} )} - {nextBestAction.today && ( + {isTaskInTodayPlan(nextBestAction) && ( {t('tasks.todayPlan', 'Today plan')} @@ -355,19 +366,19 @@ const ProjectInsightsPanel: React.FC = ({ 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')} diff --git a/frontend/components/Project/useProjectMetrics.ts b/frontend/components/Project/useProjectMetrics.ts index 54b2740..b1ce2f0 100644 --- a/frontend/components/Project/useProjectMetrics.ts +++ b/frontend/components/Project/useProjectMetrics.ts @@ -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) { diff --git a/frontend/components/Tag/TagDetails.tsx b/frontend/components/Tag/TagDetails.tsx index f85dfd9..b1f2a7c 100644 --- a/frontend/components/Tag/TagDetails.tsx +++ b/frontend/components/Tag/TagDetails.tsx @@ -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} /> ) diff --git a/frontend/components/Task/NextTaskSuggestion.tsx b/frontend/components/Task/NextTaskSuggestion.tsx index 33bbebe..e2bbe41 100644 --- a/frontend/components/Task/NextTaskSuggestion.tsx +++ b/frontend/components/Task/NextTaskSuggestion.tsx @@ -88,11 +88,10 @@ const NextTaskSuggestion: React.FC = ({ 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( diff --git a/frontend/components/Task/TaskDetails.tsx b/frontend/components/Task/TaskDetails.tsx index a55de7b..67b8b9f 100644 --- a/frontend/components/Task/TaskDetails.tsx +++ b/frontend/components/Task/TaskDetails.tsx @@ -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); diff --git a/frontend/components/Task/TaskItem.tsx b/frontend/components/Task/TaskItem.tsx index 4880e7c..5a0a326 100644 --- a/frontend/components/Task/TaskItem.tsx +++ b/frontend/components/Task/TaskItem.tsx @@ -141,7 +141,7 @@ const SubtasksDisplay: React.FC = ({ ); }; 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 = ({ 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' diff --git a/frontend/components/Task/TasksToday.tsx b/frontend/components/Task/TasksToday.tsx index 888d46e..565c7bd 100644 --- a/frontend/components/Task/TasksToday.tsx +++ b/frontend/components/Task/TasksToday.tsx @@ -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 => { - 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 => { 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} /> diff --git a/frontend/components/Tasks.tsx b/frontend/components/Tasks.tsx index 390c302..5541479 100644 --- a/frontend/components/Tasks.tsx +++ b/frontend/components/Tasks.tsx @@ -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 => { - 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} /> )} diff --git a/frontend/components/ViewDetail.tsx b/frontend/components/ViewDetail.tsx index 45bb9b4..0074f7a 100644 --- a/frontend/components/ViewDetail.tsx +++ b/frontend/components/ViewDetail.tsx @@ -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} /> )} diff --git a/frontend/entities/Task.ts b/frontend/entities/Task.ts index cb8633e..a20024c 100644 --- a/frontend/entities/Task.ts +++ b/frontend/entities/Task.ts @@ -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; diff --git a/frontend/store/useStore.ts b/frontend/store/useStore.ts index c9f78c2..27ee64d 100644 --- a/frontend/store/useStore.ts +++ b/frontend/store/useStore.ts @@ -65,7 +65,6 @@ interface TasksStore { updateTask: (taskUid: string, taskData: Task) => Promise; deleteTask: (taskUid: string) => Promise; toggleTaskCompletion: (taskUid: string) => Promise; - toggleTaskToday: (taskId: number) => Promise; loadTaskById: (taskId: number) => Promise; loadTaskByUid: (uid: string) => Promise; loadSubtasks: (parentTaskUid: string) => Promise; @@ -454,33 +453,6 @@ export const useStore = create((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 { diff --git a/frontend/utils/dateUtils.ts b/frontend/utils/dateUtils.ts index 18c7769..2103d71 100644 --- a/frontend/utils/dateUtils.ts +++ b/frontend/utils/dateUtils.ts @@ -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; diff --git a/frontend/utils/tasksService.ts b/frontend/utils/tasksService.ts index 437a3f9..1dc0bb6 100644 --- a/frontend/utils/tasksService.ts +++ b/frontend/utils/tasksService.ts @@ -174,20 +174,6 @@ export const fetchSubtasks = async (parentTaskUid: string): Promise => { return await response.json(); }; -export const toggleTaskToday = async ( - taskId: number, - currentTask?: Task -): Promise => { - if (!currentTask) { - currentTask = await fetchTaskById(taskId); - } - - return await updateTask(currentTask.uid!, { - ...currentTask, - today: !currentTask.today, - }); -}; - export interface TaskIteration { date: string; utc_date: string;