Fix bug 733 (#735)
* Refactor today * fixup! Refactor today * fixup! fixup! Refactor today
This commit is contained in:
parent
ad8ab3ec72
commit
e73c354e7e
29 changed files with 267 additions and 302 deletions
30
backend/migrations/20251227000001-remove-today-column.js
Normal file
30
backend/migrations/20251227000001-remove-today-column.js
Normal 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,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue