Fix an issue with check to done task not moving to completed
This commit is contained in:
parent
ec428673a1
commit
3fdc31fb65
15 changed files with 700 additions and 187 deletions
|
|
@ -26,11 +26,17 @@ async function serializeTask(task) {
|
|||
due_date: subtask.due_date
|
||||
? subtask.due_date.toISOString().split('T')[0]
|
||||
: null,
|
||||
completed_at: subtask.completed_at
|
||||
? subtask.completed_at.toISOString()
|
||||
: null,
|
||||
}))
|
||||
: [],
|
||||
due_date: task.due_date
|
||||
? task.due_date.toISOString().split('T')[0]
|
||||
: null,
|
||||
completed_at: task.completed_at
|
||||
? task.completed_at.toISOString()
|
||||
: null,
|
||||
today_move_count: todayMoveCount,
|
||||
};
|
||||
}
|
||||
|
|
@ -90,22 +96,39 @@ async function checkAndUpdateParentTaskCompletion(parentTaskId, userId) {
|
|||
);
|
||||
|
||||
if (allSubtasksDone) {
|
||||
// Update parent task to done
|
||||
await Task.update(
|
||||
{
|
||||
status: Task.STATUS.DONE,
|
||||
completed_at: new Date(),
|
||||
// Check if parent is already done to avoid unnecessary updates
|
||||
const parentTask = await Task.findOne({
|
||||
where: {
|
||||
id: parentTaskId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id: parentTaskId,
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
if (
|
||||
parentTask &&
|
||||
parentTask.status !== Task.STATUS.DONE &&
|
||||
parentTask.status !== 'done'
|
||||
) {
|
||||
// Update parent task to done
|
||||
await Task.update(
|
||||
{
|
||||
status: Task.STATUS.DONE,
|
||||
completed_at: new Date(),
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
where: {
|
||||
id: parentTaskId,
|
||||
user_id: userId,
|
||||
},
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error checking parent task completion:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,16 +161,20 @@ async function undoneParentTaskIfNeeded(parentTaskId, userId) {
|
|||
},
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error undoing parent task:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to complete all subtasks when parent is done
|
||||
async function completeAllSubtasks(parentTaskId, userId) {
|
||||
try {
|
||||
await Task.update(
|
||||
// Update all subtasks to be completed - this ensures completed_at is set for all
|
||||
const result = await Task.update(
|
||||
{
|
||||
status: Task.STATUS.DONE,
|
||||
completed_at: new Date(),
|
||||
|
|
@ -159,15 +186,17 @@ async function completeAllSubtasks(parentTaskId, userId) {
|
|||
},
|
||||
}
|
||||
);
|
||||
return result[0] > 0; // Return true if any subtasks were actually updated
|
||||
} catch (error) {
|
||||
console.error('Error completing all subtasks:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to undone all subtasks when parent is undone
|
||||
async function undoneAllSubtasks(parentTaskId, userId) {
|
||||
try {
|
||||
await Task.update(
|
||||
const result = await Task.update(
|
||||
{
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
completed_at: null,
|
||||
|
|
@ -176,11 +205,16 @@ async function undoneAllSubtasks(parentTaskId, userId) {
|
|||
where: {
|
||||
parent_task_id: parentTaskId,
|
||||
user_id: userId,
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.DONE, 'done'],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
return result[0] > 0; // Return true if any subtasks were actually updated
|
||||
} catch (error) {
|
||||
console.error('Error undoing all subtasks:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,9 +282,11 @@ async function filterTasksByParams(params, userId) {
|
|||
default:
|
||||
if (params.status === 'done') {
|
||||
whereClause.status = { [Op.in]: [Task.STATUS.DONE, 'done'] };
|
||||
} else {
|
||||
} else if (!params.client_side_filtering) {
|
||||
// Only exclude completed tasks if not doing client-side filtering
|
||||
whereClause.status = { [Op.notIn]: [Task.STATUS.DONE, 'done'] };
|
||||
}
|
||||
// If client_side_filtering is true, don't add any status filter (include all)
|
||||
}
|
||||
|
||||
// Filter by tag
|
||||
|
|
@ -827,6 +863,17 @@ router.get('/task/:id', async (req, res) => {
|
|||
through: { attributes: [] },
|
||||
},
|
||||
{ model: Project, attributes: ['name'], required: false },
|
||||
{
|
||||
model: Task,
|
||||
as: 'Subtasks',
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
@ -1266,22 +1313,60 @@ router.patch('/task/:id', async (req, res) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Update edited subtasks
|
||||
const editedSubtasks = subtasks.filter(
|
||||
(s) => s.isEdited && s.id && s.name && s.name.trim()
|
||||
// Update edited subtasks and status changes
|
||||
const subtasksToUpdate = subtasks.filter(
|
||||
(s) =>
|
||||
s.id &&
|
||||
((s.isEdited && s.name && s.name.trim()) ||
|
||||
s._statusChanged)
|
||||
);
|
||||
if (editedSubtasks.length > 0) {
|
||||
const updatePromises = editedSubtasks.map((subtask) =>
|
||||
Task.update(
|
||||
{ name: subtask.name.trim() },
|
||||
{
|
||||
where: {
|
||||
id: subtask.id,
|
||||
user_id: req.currentUser.id,
|
||||
},
|
||||
if (subtasksToUpdate.length > 0) {
|
||||
const updatePromises = subtasksToUpdate.map((subtask) => {
|
||||
const updateData = {};
|
||||
|
||||
if (
|
||||
subtask.isEdited &&
|
||||
subtask.name &&
|
||||
subtask.name.trim()
|
||||
) {
|
||||
updateData.name = subtask.name.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
subtask._statusChanged ||
|
||||
subtask.status !== undefined
|
||||
) {
|
||||
updateData.status = subtask.status
|
||||
? typeof subtask.status === 'string'
|
||||
? Task.getStatusValue(subtask.status)
|
||||
: subtask.status
|
||||
: Task.STATUS.NOT_STARTED;
|
||||
|
||||
if (
|
||||
updateData.status === Task.STATUS.DONE &&
|
||||
!subtask.completed_at
|
||||
) {
|
||||
updateData.completed_at = new Date();
|
||||
} else if (updateData.status !== Task.STATUS.DONE) {
|
||||
updateData.completed_at = null;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (subtask.priority !== undefined) {
|
||||
updateData.priority = subtask.priority
|
||||
? typeof subtask.priority === 'string'
|
||||
? Task.getPriorityValue(subtask.priority)
|
||||
: subtask.priority
|
||||
: Task.PRIORITY.MEDIUM;
|
||||
}
|
||||
|
||||
return Task.update(updateData, {
|
||||
where: {
|
||||
id: subtask.id,
|
||||
user_id: req.currentUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
}
|
||||
|
|
@ -1296,9 +1381,24 @@ router.patch('/task/:id', async (req, res) => {
|
|||
name: subtask.name.trim(),
|
||||
parent_task_id: task.id,
|
||||
user_id: req.currentUser.id,
|
||||
priority: Task.PRIORITY.MEDIUM,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
today: false,
|
||||
priority: subtask.priority
|
||||
? typeof subtask.priority === 'string'
|
||||
? Task.getPriorityValue(subtask.priority)
|
||||
: subtask.priority
|
||||
: Task.PRIORITY.MEDIUM,
|
||||
status: subtask.status
|
||||
? typeof subtask.status === 'string'
|
||||
? Task.getStatusValue(subtask.status)
|
||||
: subtask.status
|
||||
: Task.STATUS.NOT_STARTED,
|
||||
completed_at:
|
||||
subtask.status === 'done' ||
|
||||
subtask.status === Task.STATUS.DONE
|
||||
? subtask.completed_at
|
||||
? new Date(subtask.completed_at)
|
||||
: new Date()
|
||||
: null,
|
||||
today: subtask.today || false,
|
||||
recurrence_type: 'none',
|
||||
completion_based: false,
|
||||
})
|
||||
|
|
@ -1472,15 +1572,10 @@ router.patch('/task/:id', async (req, res) => {
|
|||
],
|
||||
});
|
||||
|
||||
const taskJson = taskWithAssociations.toJSON();
|
||||
// Use serializeTask to include subtasks data
|
||||
const serializedTask = await serializeTask(taskWithAssociations);
|
||||
|
||||
res.json({
|
||||
...taskJson,
|
||||
tags: taskJson.Tags || [], // Normalize Tags to tags
|
||||
due_date: taskWithAssociations.due_date
|
||||
? taskWithAssociations.due_date.toISOString().split('T')[0]
|
||||
: null,
|
||||
});
|
||||
res.json(serializedTask);
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
res.status(400).json({
|
||||
|
|
@ -1497,12 +1592,34 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{ model: Project, attributes: ['name'], required: false },
|
||||
{
|
||||
model: Task,
|
||||
as: 'Subtasks',
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
|
||||
// Track if parent-child logic was executed
|
||||
let parentChildLogicExecuted = false;
|
||||
|
||||
const newStatus =
|
||||
task.status === Task.STATUS.DONE || task.status === 'done'
|
||||
? task.note
|
||||
|
|
@ -1520,29 +1637,63 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
|
||||
await task.update(updateData);
|
||||
|
||||
// Handle parent-child task completion logic
|
||||
// Check if subtasks exist in database directly to debug association issue
|
||||
const directSubtasksQuery = await Task.findAll({
|
||||
where: {
|
||||
parent_task_id: task.id,
|
||||
user_id: req.currentUser.id,
|
||||
},
|
||||
attributes: ['id', 'name', 'status', 'parent_task_id'],
|
||||
});
|
||||
|
||||
// If direct query finds subtasks but task.Subtasks is empty, there's an association issue
|
||||
if (
|
||||
directSubtasksQuery.length > 0 &&
|
||||
(!task.Subtasks || task.Subtasks.length === 0)
|
||||
) {
|
||||
task.Subtasks = directSubtasksQuery;
|
||||
}
|
||||
|
||||
if (task.parent_task_id) {
|
||||
if (newStatus === Task.STATUS.DONE) {
|
||||
if (newStatus === Task.STATUS.DONE || newStatus === 'done') {
|
||||
// When subtask is done, check if parent should be done
|
||||
await checkAndUpdateParentTaskCompletion(
|
||||
const parentUpdated = await checkAndUpdateParentTaskCompletion(
|
||||
task.parent_task_id,
|
||||
req.currentUser.id
|
||||
);
|
||||
if (parentUpdated) {
|
||||
parentChildLogicExecuted = true;
|
||||
}
|
||||
} else {
|
||||
// When subtask is undone, undone parent if it was done
|
||||
await undoneParentTaskIfNeeded(
|
||||
const parentUpdated = await undoneParentTaskIfNeeded(
|
||||
task.parent_task_id,
|
||||
req.currentUser.id
|
||||
);
|
||||
if (parentUpdated) {
|
||||
parentChildLogicExecuted = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a parent task
|
||||
} else if (task.Subtasks && task.Subtasks.length > 0) {
|
||||
// This is a parent task with subtasks
|
||||
if (newStatus === Task.STATUS.DONE) {
|
||||
// When parent is done, complete all subtasks
|
||||
await completeAllSubtasks(task.id, req.currentUser.id);
|
||||
const subtasksUpdated = await completeAllSubtasks(
|
||||
task.id,
|
||||
req.currentUser.id
|
||||
);
|
||||
if (subtasksUpdated) {
|
||||
parentChildLogicExecuted = true;
|
||||
}
|
||||
} else {
|
||||
// When parent is undone, undone all subtasks
|
||||
await undoneAllSubtasks(task.id, req.currentUser.id);
|
||||
const subtasksUpdated = await undoneAllSubtasks(
|
||||
task.id,
|
||||
req.currentUser.id
|
||||
);
|
||||
if (subtasksUpdated) {
|
||||
parentChildLogicExecuted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1552,12 +1703,11 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
nextTask = await RecurringTaskService.handleTaskCompletion(task);
|
||||
}
|
||||
|
||||
const response = {
|
||||
...task.toJSON(),
|
||||
due_date: task.due_date
|
||||
? task.due_date.toISOString().split('T')[0]
|
||||
: null,
|
||||
};
|
||||
// Use serializeTask to include subtasks data
|
||||
const response = await serializeTask(task);
|
||||
|
||||
// If parent-child logic was executed, we might need to reload data
|
||||
// For now, let the frontend handle the refresh to avoid complex reloading logic
|
||||
|
||||
if (nextTask) {
|
||||
response.next_task = {
|
||||
|
|
@ -1568,9 +1718,17 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
};
|
||||
}
|
||||
|
||||
// Add flag to response to indicate if parent-child logic was executed
|
||||
response.parent_child_logic_executed = parentChildLogicExecuted;
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(422).json({ error: 'Unable to update task' });
|
||||
console.error('Error in toggle completion endpoint:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
res.status(422).json({
|
||||
error: 'Unable to update task',
|
||||
details: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ const Layout: React.FC<LayoutProps> = ({
|
|||
task={{
|
||||
name: '',
|
||||
status: 'not_started',
|
||||
completed_at: null,
|
||||
}}
|
||||
onSave={handleSaveTask}
|
||||
onDelete={async () => {}}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
|||
priority: 'medium',
|
||||
tags: taskTags,
|
||||
project_id: projectId,
|
||||
completed_at: null,
|
||||
};
|
||||
|
||||
if (item.id !== undefined) {
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ const InboxItems: React.FC = () => {
|
|||
name: '',
|
||||
status: 'not_started',
|
||||
priority: 'medium',
|
||||
completed_at: null,
|
||||
}
|
||||
}
|
||||
onSave={handleSaveTask}
|
||||
|
|
|
|||
|
|
@ -989,6 +989,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
priority: 'medium',
|
||||
tags: taskTags,
|
||||
project_id: projectId,
|
||||
completed_at: null,
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -1127,8 +1128,8 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
const newTask: Task = {
|
||||
name: cleanedText,
|
||||
status: 'not_started',
|
||||
completed_at: null,
|
||||
};
|
||||
|
||||
try {
|
||||
await onSave(newTask);
|
||||
showSuccessToast(t('task.createSuccess'));
|
||||
|
|
|
|||
|
|
@ -122,9 +122,6 @@ const ProjectDetails: React.FC = () => {
|
|||
localStorage.getItem('project_order_by') ||
|
||||
'created_at:desc';
|
||||
|
||||
console.log(
|
||||
`Fetching ONLY project ${id} with fetchProjectById`
|
||||
);
|
||||
const projectData = await fetchProjectById(id, {
|
||||
sort: sortParam,
|
||||
// Remove completed parameter since backend filtering isn't working
|
||||
|
|
@ -171,6 +168,7 @@ const ProjectDetails: React.FC = () => {
|
|||
name: taskName,
|
||||
status: 'not_started',
|
||||
project_id: project.id,
|
||||
completed_at: null,
|
||||
});
|
||||
setTasks([...tasks, newTask]);
|
||||
|
||||
|
|
@ -318,6 +316,7 @@ const ProjectDetails: React.FC = () => {
|
|||
status: 'not_started',
|
||||
project_id: projectId,
|
||||
priority: 'medium',
|
||||
completed_at: null,
|
||||
});
|
||||
|
||||
// Update the tasks list to include the new task
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
const subtasksSectionRef = useRef<HTMLDivElement>(null);
|
||||
const addInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
if (subtasksSectionRef.current) {
|
||||
|
|
@ -51,6 +50,7 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
isNew: true,
|
||||
// Also keep for UI purposes
|
||||
_isNew: true,
|
||||
completed_at: null,
|
||||
} as Task;
|
||||
|
||||
onSubtasksChange([...subtasks, newSubtask]);
|
||||
|
|
@ -113,11 +113,21 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
const handleToggleNewSubtaskCompletion = (index: number) => {
|
||||
const updatedSubtasks = subtasks.map((subtask, i) => {
|
||||
if (i === index) {
|
||||
const isDone = subtask.status === 'done' || subtask.status === 2;
|
||||
const isDone =
|
||||
subtask.status === 'done' || subtask.status === 2;
|
||||
const newStatus = isDone
|
||||
? ('not_started' as const)
|
||||
: ('done' as const);
|
||||
const hasId =
|
||||
subtask.id &&
|
||||
!((subtask as any)._isNew || (subtask as any).isNew);
|
||||
|
||||
return {
|
||||
...subtask,
|
||||
status: isDone ? ('not_started' as const) : ('done' as const),
|
||||
completed_at: isDone ? undefined : new Date().toISOString(),
|
||||
status: newStatus,
|
||||
completed_at: isDone ? null : new Date().toISOString(),
|
||||
// Mark for backend update if it has an ID (existing subtask)
|
||||
_statusChanged: hasId,
|
||||
};
|
||||
}
|
||||
return subtask;
|
||||
|
|
@ -150,8 +160,16 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
subtask.status || 'not_started'
|
||||
}
|
||||
onToggleCompletion={async () => {
|
||||
if (subtask.id && onSubtaskUpdate) {
|
||||
// Existing subtask - use API
|
||||
if (
|
||||
subtask.id &&
|
||||
onSubtaskUpdate &&
|
||||
!(
|
||||
(subtask as any)
|
||||
._isNew ||
|
||||
(subtask as any).isNew
|
||||
)
|
||||
) {
|
||||
// Existing subtask - use API for immediate toggle, then update callback
|
||||
try {
|
||||
const updatedSubtask =
|
||||
await toggleTaskCompletion(
|
||||
|
|
@ -167,8 +185,10 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
);
|
||||
}
|
||||
} else {
|
||||
// New subtask - handle locally
|
||||
handleToggleNewSubtaskCompletion(index);
|
||||
// New subtask or no callback - handle locally
|
||||
handleToggleNewSubtaskCompletion(
|
||||
index
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -213,8 +233,17 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
'not_started'
|
||||
}
|
||||
onToggleCompletion={async () => {
|
||||
if (subtask.id && onSubtaskUpdate) {
|
||||
// Existing subtask - use API
|
||||
if (
|
||||
subtask.id &&
|
||||
onSubtaskUpdate &&
|
||||
!(
|
||||
(subtask as any)
|
||||
._isNew ||
|
||||
(subtask as any)
|
||||
.isNew
|
||||
)
|
||||
) {
|
||||
// Existing subtask - use API for immediate toggle, then update callback
|
||||
try {
|
||||
const updatedSubtask =
|
||||
await toggleTaskCompletion(
|
||||
|
|
@ -230,8 +259,10 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
);
|
||||
}
|
||||
} else {
|
||||
// New subtask - handle locally
|
||||
handleToggleNewSubtaskCompletion(index);
|
||||
// New subtask or no callback - handle locally
|
||||
handleToggleNewSubtaskCompletion(
|
||||
index
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,18 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
subtask.id
|
||||
);
|
||||
|
||||
// Check if parent-child logic was executed
|
||||
if (
|
||||
updatedSubtask.parent_child_logic_executed
|
||||
) {
|
||||
// For subtasks, we need a full page refresh because the parent task
|
||||
// might be displayed in multiple places (task list, today view, etc.)
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the subtask in local state immediately
|
||||
onSubtaskUpdate(
|
||||
updatedSubtask
|
||||
|
|
@ -125,6 +137,7 @@ import { useTranslation } from 'react-i18next';
|
|||
interface TaskItemProps {
|
||||
task: Task;
|
||||
onTaskUpdate: (task: Task) => Promise<void>;
|
||||
onTaskCompletionToggle?: (task: Task) => void;
|
||||
onTaskDelete: (taskId: number) => void;
|
||||
projects: Project[];
|
||||
hideProjectName?: boolean;
|
||||
|
|
@ -134,6 +147,7 @@ interface TaskItemProps {
|
|||
const TaskItem: React.FC<TaskItemProps> = ({
|
||||
task,
|
||||
onTaskUpdate,
|
||||
onTaskCompletionToggle,
|
||||
onTaskDelete,
|
||||
projects,
|
||||
hideProjectName = false,
|
||||
|
|
@ -286,8 +300,39 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
const handleToggleCompletion = async () => {
|
||||
if (task.id) {
|
||||
try {
|
||||
const updatedTask = await toggleTaskCompletion(task.id);
|
||||
await onTaskUpdate(updatedTask);
|
||||
const response = await toggleTaskCompletion(task.id);
|
||||
|
||||
// Handle the updated task
|
||||
if (onTaskCompletionToggle) {
|
||||
onTaskCompletionToggle(response);
|
||||
} else {
|
||||
await onTaskUpdate(response);
|
||||
}
|
||||
|
||||
// Only refresh if parent-child logic was executed (affecting other tasks)
|
||||
if (response.parent_child_logic_executed) {
|
||||
// Instead of refreshing, let's refetch and update the task data
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Refetch the current task with updated subtasks
|
||||
const updatedTaskResponse = await fetch(
|
||||
`/api/task/${task.id}`
|
||||
);
|
||||
if (updatedTaskResponse.ok) {
|
||||
const updatedTaskData =
|
||||
await updatedTaskResponse.json();
|
||||
await onTaskUpdate(updatedTaskData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error refetching task after parent-child logic:',
|
||||
error
|
||||
);
|
||||
// Fallback to refresh if API call fails
|
||||
window.location.reload();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling task completion:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,25 +6,41 @@ import { Task } from '../../entities/Task';
|
|||
interface TaskListProps {
|
||||
tasks: Task[];
|
||||
onTaskUpdate: (task: Task) => Promise<void>;
|
||||
onTaskCompletionToggle?: (task: Task) => void;
|
||||
onTaskCreate?: (task: Task) => void;
|
||||
onTaskDelete: (taskId: number) => void;
|
||||
projects: Project[];
|
||||
hideProjectName?: boolean;
|
||||
onToggleToday?: (taskId: number) => Promise<void>;
|
||||
showCompletedTasks?: boolean; // New prop
|
||||
}
|
||||
|
||||
const TaskList: React.FC<TaskListProps> = ({
|
||||
tasks,
|
||||
onTaskUpdate,
|
||||
onTaskCompletionToggle,
|
||||
onTaskDelete,
|
||||
projects,
|
||||
hideProjectName = false,
|
||||
onToggleToday,
|
||||
showCompletedTasks = false, // Default to false
|
||||
}) => {
|
||||
// Conditionally filter tasks based on showCompletedTasks prop
|
||||
const filteredTasks = showCompletedTasks
|
||||
? tasks
|
||||
: tasks.filter((task) => {
|
||||
const isCompleted =
|
||||
task.status === 'done' ||
|
||||
task.status === 'archived' ||
|
||||
task.status === 2 ||
|
||||
task.status === 3;
|
||||
return !isCompleted;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="task-list-container">
|
||||
{tasks.length > 0 ? (
|
||||
tasks.map((task) => (
|
||||
{filteredTasks.length > 0 ? (
|
||||
filteredTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="task-item-wrapper transition-all duration-200 ease-in-out"
|
||||
|
|
@ -32,6 +48,7 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||
<TaskItem
|
||||
task={task}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
onTaskCompletionToggle={onTaskCompletionToggle}
|
||||
onTaskDelete={onTaskDelete}
|
||||
projects={projects}
|
||||
hideProjectName={hideProjectName}
|
||||
|
|
|
|||
|
|
@ -51,8 +51,11 @@ const getLocale = (language: string) => {
|
|||
const TasksToday: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Get tasks from store at the top level to avoid conditional hook usage
|
||||
const storeTasks = useStore((state) => state.tasksStore.tasks);
|
||||
|
||||
// Temporarily use local state to debug infinite loop
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
|
|
@ -202,9 +205,13 @@ const TasksToday: React.FC = () => {
|
|||
const { tasks: fetchedTasks, metrics: fetchedMetrics } =
|
||||
await fetchTasks('?type=today');
|
||||
if (isMounted.current) {
|
||||
setTasks(fetchedTasks);
|
||||
setMetrics(fetchedMetrics);
|
||||
setIsError(false);
|
||||
// setTasks(fetchedTasks); // Removed local state
|
||||
if (isMounted.current) {
|
||||
// setTasks(fetchedTasks); // Removed local state
|
||||
setMetrics(fetchedMetrics);
|
||||
useStore.getState().tasksStore.setTasks(fetchedTasks);
|
||||
setIsError(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tasks:', error);
|
||||
|
|
@ -400,78 +407,171 @@ const TasksToday: React.FC = () => {
|
|||
async (updatedTask: Task): Promise<void> => {
|
||||
if (!updatedTask.id || !isMounted.current) return;
|
||||
|
||||
// Helper function to update a task in an array
|
||||
const updateTaskInArray = (tasks: Task[]) =>
|
||||
tasks.map((task) =>
|
||||
task.id === updatedTask.id
|
||||
? {
|
||||
...task,
|
||||
...updatedTask,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
: task
|
||||
// Optimistically update UI
|
||||
setMetrics((prevMetrics) => {
|
||||
const newMetrics = { ...prevMetrics };
|
||||
|
||||
// Helper to remove task from a list
|
||||
const removeTask = (list: Task[]) =>
|
||||
list.filter((task) => task.id !== updatedTask.id);
|
||||
|
||||
// Helper to add or update task in a list
|
||||
const updateOrAddTask = (list: Task[], taskToProcess: Task) => {
|
||||
const existingIndex = list.findIndex(
|
||||
(task) => task.id === taskToProcess.id
|
||||
);
|
||||
if (existingIndex > -1) {
|
||||
// Task exists, update it by creating a new object and a new array
|
||||
// Preserve subtasks data to prevent loss
|
||||
return list.map((task, index) =>
|
||||
index === existingIndex
|
||||
? {
|
||||
...task,
|
||||
...taskToProcess,
|
||||
// Explicitly preserve subtasks data
|
||||
subtasks:
|
||||
taskToProcess.subtasks ||
|
||||
taskToProcess.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
Subtasks:
|
||||
taskToProcess.subtasks ||
|
||||
taskToProcess.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
}
|
||||
: task
|
||||
);
|
||||
} else {
|
||||
// Task does not exist, add it by creating a new array
|
||||
return [...list, taskToProcess];
|
||||
}
|
||||
};
|
||||
|
||||
// Remove task from all potential "active" lists first
|
||||
newMetrics.today_plan_tasks = removeTask(
|
||||
newMetrics.today_plan_tasks || []
|
||||
);
|
||||
|
||||
// Check if this task exists in any of our task lists
|
||||
const taskExistsInLocal = tasks.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
);
|
||||
const taskExistsInMetrics =
|
||||
metrics.today_plan_tasks?.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
) ||
|
||||
metrics.suggested_tasks.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
) ||
|
||||
metrics.tasks_due_today.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
) ||
|
||||
metrics.tasks_in_progress.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
) ||
|
||||
metrics.tasks_completed_today.some(
|
||||
(task) => task.id === updatedTask.id
|
||||
newMetrics.suggested_tasks = removeTask(
|
||||
newMetrics.suggested_tasks || []
|
||||
);
|
||||
newMetrics.tasks_due_today = removeTask(
|
||||
newMetrics.tasks_due_today || []
|
||||
);
|
||||
newMetrics.tasks_in_progress = removeTask(
|
||||
newMetrics.tasks_in_progress || []
|
||||
);
|
||||
newMetrics.tasks_completed_today = removeTask(
|
||||
newMetrics.tasks_completed_today || []
|
||||
); // Always remove from completed first
|
||||
|
||||
// Update local task state
|
||||
if (taskExistsInLocal) {
|
||||
setTasks((prevTasks) => updateTaskInArray(prevTasks));
|
||||
}
|
||||
// Now, add the task to the appropriate list(s) based on its new status
|
||||
if (updatedTask.status === 'done' || updatedTask.status === 2) {
|
||||
// If completed, add to tasks_completed_today if it was completed today
|
||||
if (updatedTask.completed_at) {
|
||||
const completedDate = new Date(
|
||||
updatedTask.completed_at
|
||||
);
|
||||
const today = new Date();
|
||||
if (
|
||||
format(completedDate, 'yyyy-MM-dd') ===
|
||||
format(today, 'yyyy-MM-dd')
|
||||
) {
|
||||
newMetrics.tasks_completed_today = updateOrAddTask(
|
||||
newMetrics.tasks_completed_today,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If not completed, add to relevant active lists
|
||||
if (
|
||||
updatedTask.today &&
|
||||
updatedTask.status !== 'archived'
|
||||
) {
|
||||
newMetrics.today_plan_tasks = updateOrAddTask(
|
||||
newMetrics.today_plan_tasks,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
if (updatedTask.status === 'in_progress') {
|
||||
newMetrics.tasks_in_progress = updateOrAddTask(
|
||||
newMetrics.tasks_in_progress,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
// Check if due today (and not already in today_plan_tasks or in_progress)
|
||||
const isDueToday =
|
||||
updatedTask.due_date &&
|
||||
format(new Date(updatedTask.due_date), 'yyyy-MM-dd') ===
|
||||
format(new Date(), 'yyyy-MM-dd');
|
||||
if (
|
||||
isDueToday &&
|
||||
updatedTask.status !== 'archived' &&
|
||||
!newMetrics.today_plan_tasks.some(
|
||||
(t) => t.id === updatedTask.id
|
||||
) &&
|
||||
!newMetrics.tasks_in_progress.some(
|
||||
(t) => t.id === updatedTask.id
|
||||
)
|
||||
) {
|
||||
newMetrics.tasks_due_today = updateOrAddTask(
|
||||
newMetrics.tasks_due_today,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
// Check for suggested tasks (and not already in other active lists)
|
||||
const isSuggested =
|
||||
!updatedTask.today &&
|
||||
!updatedTask.project_id &&
|
||||
!updatedTask.due_date;
|
||||
if (
|
||||
isSuggested &&
|
||||
updatedTask.status !== 'archived' &&
|
||||
updatedTask.status !== 'done' &&
|
||||
updatedTask.status !== 2 &&
|
||||
!newMetrics.today_plan_tasks.some(
|
||||
(t) => t.id === updatedTask.id
|
||||
) &&
|
||||
!newMetrics.tasks_due_today.some(
|
||||
(t) => t.id === updatedTask.id
|
||||
) &&
|
||||
!newMetrics.tasks_in_progress.some(
|
||||
(t) => t.id === updatedTask.id
|
||||
)
|
||||
) {
|
||||
newMetrics.suggested_tasks = updateOrAddTask(
|
||||
newMetrics.suggested_tasks,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (taskExistsInMetrics) {
|
||||
setMetrics((prevMetrics) => ({
|
||||
...prevMetrics,
|
||||
today_plan_tasks: updateTaskInArray(
|
||||
prevMetrics.today_plan_tasks || []
|
||||
),
|
||||
suggested_tasks: updateTaskInArray(
|
||||
prevMetrics.suggested_tasks || []
|
||||
),
|
||||
tasks_due_today: updateTaskInArray(
|
||||
prevMetrics.tasks_due_today || []
|
||||
),
|
||||
tasks_in_progress: updateTaskInArray(
|
||||
prevMetrics.tasks_in_progress || []
|
||||
),
|
||||
tasks_completed_today: updateTaskInArray(
|
||||
prevMetrics.tasks_completed_today || []
|
||||
),
|
||||
}));
|
||||
}
|
||||
// Recalculate total_open_tasks based on the updated active lists
|
||||
newMetrics.total_open_tasks =
|
||||
newMetrics.today_plan_tasks.length +
|
||||
newMetrics.suggested_tasks.length +
|
||||
newMetrics.tasks_due_today.length +
|
||||
newMetrics.tasks_in_progress.length;
|
||||
|
||||
return newMetrics;
|
||||
});
|
||||
|
||||
// Update the store with the updated task
|
||||
useStore.getState().tasksStore.updateTaskInStore(updatedTask);
|
||||
|
||||
try {
|
||||
// Make API call if the task exists anywhere
|
||||
if (taskExistsInLocal || taskExistsInMetrics) {
|
||||
await updateTask(updatedTask.id, updatedTask);
|
||||
}
|
||||
// Make API call to persist the change
|
||||
await updateTask(updatedTask.id, updatedTask);
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
if (isMounted.current) {
|
||||
// Error handling is now managed by the store
|
||||
}
|
||||
// Revert UI on error if necessary, or re-fetch to sync
|
||||
// For now, just log the error
|
||||
}
|
||||
},
|
||||
[tasks, metrics]
|
||||
[] // Dependencies are now handled by direct state manipulation
|
||||
);
|
||||
|
||||
const handleTaskDelete = useCallback(
|
||||
|
|
@ -485,7 +585,7 @@ const TasksToday: React.FC = () => {
|
|||
const { tasks: updatedTasks, metrics: updatedMetrics } =
|
||||
await fetchTasks('?type=today');
|
||||
if (isMounted.current) {
|
||||
setTasks(updatedTasks);
|
||||
useStore.getState().tasksStore.setTasks(updatedTasks);
|
||||
setMetrics(updatedMetrics);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -506,7 +606,7 @@ const TasksToday: React.FC = () => {
|
|||
const { tasks: updatedTasks, metrics: updatedMetrics } =
|
||||
await fetchTasks('?type=today');
|
||||
if (isMounted.current) {
|
||||
setTasks(updatedTasks);
|
||||
useStore.getState().tasksStore.setTasks(updatedTasks);
|
||||
setMetrics(updatedMetrics);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -516,6 +616,21 @@ const TasksToday: React.FC = () => {
|
|||
[]
|
||||
);
|
||||
|
||||
const handleTaskCompletionToggle = useCallback(
|
||||
async (updatedTask: Task): Promise<void> => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
try {
|
||||
// The updatedTask is already the result of the API call from TaskItem
|
||||
// Use the centralized task update handler to update UI optimistically
|
||||
await handleTaskUpdate(updatedTask);
|
||||
} catch (error) {
|
||||
console.error('Error toggling task completion:', error);
|
||||
}
|
||||
},
|
||||
[handleTaskUpdate]
|
||||
);
|
||||
|
||||
// Calculate today's progress for the progress bar
|
||||
const getTodayProgress = () => {
|
||||
const todayTasks = metrics.today_plan_tasks || [];
|
||||
|
|
@ -551,7 +666,7 @@ const TasksToday: React.FC = () => {
|
|||
}
|
||||
|
||||
// Show error state
|
||||
if (isError && tasks.length === 0) {
|
||||
if (isError && storeTasks.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<p className="text-red-500">
|
||||
|
|
@ -814,7 +929,7 @@ const TasksToday: React.FC = () => {
|
|||
productivityAssistantEnabled &&
|
||||
profileSettings.productivity_assistant_enabled === true ? (
|
||||
<ProductivityAssistant
|
||||
tasks={tasks}
|
||||
tasks={storeTasks}
|
||||
projects={localProjects}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -854,6 +969,7 @@ const TasksToday: React.FC = () => {
|
|||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onToggleToday={handleToggleToday}
|
||||
onTaskCompletionToggle={handleTaskCompletionToggle}
|
||||
/>
|
||||
|
||||
{/* Suggested Tasks - Separate setting */}
|
||||
|
|
@ -890,6 +1006,9 @@ const TasksToday: React.FC = () => {
|
|||
<TaskList
|
||||
tasks={metrics.suggested_tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskCompletionToggle={
|
||||
handleTaskCompletionToggle
|
||||
}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={localProjects}
|
||||
onToggleToday={handleToggleToday}
|
||||
|
|
@ -912,6 +1031,9 @@ const TasksToday: React.FC = () => {
|
|||
onTaskDelete={handleTaskDelete}
|
||||
projects={localProjects}
|
||||
onToggleToday={handleToggleToday}
|
||||
onTaskCompletionToggle={
|
||||
handleTaskCompletionToggle
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -919,37 +1041,49 @@ const TasksToday: React.FC = () => {
|
|||
{/* Completed Tasks - Conditionally Rendered */}
|
||||
{isSettingsLoaded &&
|
||||
todaySettings.showCompleted &&
|
||||
metrics.tasks_completed_today.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
onClick={toggleCompletedCollapsed}
|
||||
>
|
||||
<h3 className="text-xl font-medium">
|
||||
{t('tasks.completedToday')}
|
||||
</h3>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-500 mr-2">
|
||||
{metrics.tasks_completed_today.length}
|
||||
</span>
|
||||
{isCompletedCollapsed ? (
|
||||
<ChevronRightIcon className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
(() => {
|
||||
const completedToday = metrics.tasks_completed_today; // Use the already filtered list from backend
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
onClick={toggleCompletedCollapsed}
|
||||
>
|
||||
<h3 className="text-xl font-medium">
|
||||
{t('tasks.completedToday')}
|
||||
</h3>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-500 mr-2">
|
||||
{completedToday.length}
|
||||
</span>
|
||||
{isCompletedCollapsed ? (
|
||||
<ChevronRightIcon className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isCompletedCollapsed &&
|
||||
(completedToday.length > 0 ? (
|
||||
<TaskList
|
||||
tasks={completedToday}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={localProjects}
|
||||
onToggleToday={handleToggleToday}
|
||||
showCompletedTasks={true}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center mt-4">
|
||||
{t(
|
||||
'tasks.noCompletedTasksToday',
|
||||
'No completed tasks today.'
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{!isCompletedCollapsed && (
|
||||
<TaskList
|
||||
tasks={metrics.tasks_completed_today}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={localProjects}
|
||||
onToggleToday={handleToggleToday}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
|
||||
{metrics.tasks_due_today.length === 0 &&
|
||||
metrics.tasks_in_progress.length === 0 &&
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface TodayPlanProps {
|
|||
onTaskUpdate: (task: Task) => Promise<void>;
|
||||
onTaskDelete: (taskId: number) => Promise<void>;
|
||||
onToggleToday?: (taskId: number) => Promise<void>;
|
||||
onTaskCompletionToggle?: (task: Task) => void; // New prop
|
||||
}
|
||||
|
||||
const TodayPlan: React.FC<TodayPlanProps> = ({
|
||||
|
|
@ -19,6 +20,7 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
|||
onTaskUpdate,
|
||||
onTaskDelete,
|
||||
onToggleToday,
|
||||
onTaskCompletionToggle, // Destructure new prop
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -83,6 +85,7 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
|||
onTaskDelete={onTaskDelete}
|
||||
projects={projects}
|
||||
onToggleToday={onToggleToday}
|
||||
onTaskCompletionToggle={onTaskCompletionToggle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TaskList from './Task/TaskList';
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
TagIcon,
|
||||
XMarkIcon,
|
||||
MagnifyingGlassIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
|
||||
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
|
@ -45,10 +46,49 @@ const Tasks: React.FC = () => {
|
|||
const [taskSearchQuery, setTaskSearchQuery] = useState<string>('');
|
||||
const [isInfoExpanded, setIsInfoExpanded] = useState(false); // Collapsed by default
|
||||
const [isSearchExpanded, setIsSearchExpanded] = useState(false); // Collapsed by default
|
||||
const [showCompleted, setShowCompleted] = useState(false); // Show completed tasks toggle
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Filter tasks based on completion status and search query
|
||||
const displayTasks = useMemo(() => {
|
||||
let filteredTasks;
|
||||
|
||||
// First filter by completion status
|
||||
if (showCompleted) {
|
||||
// Show only completed tasks (done=2 or archived=3)
|
||||
filteredTasks = tasks.filter(
|
||||
(task) =>
|
||||
task.status === 'done' ||
|
||||
task.status === 'archived' ||
|
||||
task.status === 2 ||
|
||||
task.status === 3
|
||||
);
|
||||
} else {
|
||||
// Show only non-completed tasks - exclude done(2) and archived(3)
|
||||
filteredTasks = tasks.filter(
|
||||
(task) =>
|
||||
task.status !== 'done' &&
|
||||
task.status !== 'archived' &&
|
||||
task.status !== 2 &&
|
||||
task.status !== 3
|
||||
);
|
||||
}
|
||||
|
||||
// Then filter by search query if provided
|
||||
if (taskSearchQuery.trim()) {
|
||||
const query = taskSearchQuery.toLowerCase();
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) =>
|
||||
task.name.toLowerCase().includes(query) ||
|
||||
task.note?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return filteredTasks;
|
||||
}, [tasks, showCompleted, taskSearchQuery]);
|
||||
const query = new URLSearchParams(location.search);
|
||||
const { title: stateTitle } = location.state || {};
|
||||
|
||||
|
|
@ -98,9 +138,15 @@ const Tasks: React.FC = () => {
|
|||
setError(null);
|
||||
try {
|
||||
const tagId = query.get('tag');
|
||||
// Fetch all tasks (both completed and non-completed) for client-side filtering
|
||||
const allTasksUrl = new URLSearchParams(location.search);
|
||||
// Add special parameter to get ALL tasks (completed and non-completed)
|
||||
allTasksUrl.set('client_side_filtering', 'true');
|
||||
const searchParams = allTasksUrl.toString();
|
||||
|
||||
const [tasksResponse, projectsResponse] = await Promise.all([
|
||||
fetch(
|
||||
`/api/tasks${location.search}${tagId ? `&tag=${tagId}` : ''}`
|
||||
`/api/tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}`
|
||||
),
|
||||
fetch('/api/projects'),
|
||||
]);
|
||||
|
|
@ -196,7 +242,25 @@ const Tasks: React.FC = () => {
|
|||
if (response.ok) {
|
||||
setTasks((prevTasks) =>
|
||||
prevTasks.map((task) =>
|
||||
task.id === updatedTask.id ? updatedTask : task
|
||||
task.id === updatedTask.id
|
||||
? {
|
||||
...task,
|
||||
...updatedTask,
|
||||
// Explicitly preserve subtasks data
|
||||
subtasks:
|
||||
updatedTask.subtasks ||
|
||||
updatedTask.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
Subtasks:
|
||||
updatedTask.subtasks ||
|
||||
updatedTask.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
}
|
||||
: task
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
|
@ -210,6 +274,15 @@ const Tasks: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Handler specifically for task completion toggles (no API call needed, just state update)
|
||||
const handleTaskCompletionToggle = (updatedTask: Task) => {
|
||||
setTasks((prevTasks) =>
|
||||
prevTasks.map((task) =>
|
||||
task.id === updatedTask.id ? updatedTask : task
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTaskDelete = async (taskId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/task/${taskId}`, {
|
||||
|
|
@ -290,10 +363,6 @@ const Tasks: React.FC = () => {
|
|||
return status !== 'done';
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter((task) =>
|
||||
task.name.toLowerCase().includes(taskSearchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
|
|
@ -316,7 +385,7 @@ const Tasks: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Info expand/collapse button, search button, and sort dropdown */}
|
||||
{/* Info expand/collapse button, search button, show completed toggle, and sort dropdown */}
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end mt-2 sm:mt-0">
|
||||
<button
|
||||
onClick={() => setIsInfoExpanded((v) => !v)}
|
||||
|
|
@ -372,6 +441,32 @@ const Tasks: React.FC = () => {
|
|||
: 'Search Tasks'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCompleted((v) => !v)}
|
||||
className={`flex items-center transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg p-2 ${
|
||||
showCompleted
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
aria-pressed={showCompleted}
|
||||
aria-label={
|
||||
showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'
|
||||
}
|
||||
title={
|
||||
showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'
|
||||
}
|
||||
>
|
||||
<CheckCircleIcon className="h-5 w-5" />
|
||||
<span className="sr-only">
|
||||
{showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'}
|
||||
</span>
|
||||
</button>
|
||||
<SortFilter
|
||||
sortOptions={sortOptions}
|
||||
sortValue={orderBy}
|
||||
|
|
@ -450,14 +545,18 @@ const Tasks: React.FC = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{filteredTasks.length > 0 ? (
|
||||
{displayTasks.length > 0 ? (
|
||||
<TaskList
|
||||
tasks={filteredTasks}
|
||||
tasks={displayTasks}
|
||||
onTaskCreate={handleTaskCreate}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskCompletionToggle={
|
||||
handleTaskCompletionToggle
|
||||
}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={projects}
|
||||
onToggleToday={handleToggleToday}
|
||||
showCompletedTasks={showCompleted}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
|
|
|
|||
|
|
@ -24,13 +24,18 @@ export interface Task {
|
|||
recurrence_week_of_month?: number;
|
||||
completion_based?: boolean;
|
||||
recurring_parent_id?: number;
|
||||
completed_at?: string;
|
||||
completed_at: string | null;
|
||||
parent_task_id?: number;
|
||||
subtasks?: Task[];
|
||||
Subtasks?: Task[]; // Handle API response case sensitivity (temporary)
|
||||
}
|
||||
|
||||
export type StatusType = 'not_started' | 'in_progress' | 'done' | 'archived';
|
||||
export type StatusType =
|
||||
| 'not_started'
|
||||
| 'in_progress'
|
||||
| 'done'
|
||||
| 'archived'
|
||||
| 'waiting';
|
||||
export type PriorityType = 'low' | 'medium' | 'high';
|
||||
export type RecurrenceType =
|
||||
| 'none'
|
||||
|
|
|
|||
|
|
@ -509,7 +509,25 @@ export const useStore = create<StoreState>((set) => ({
|
|||
tasksStore: {
|
||||
...state.tasksStore,
|
||||
tasks: state.tasksStore.tasks.map((task) =>
|
||||
task.id === updatedTask.id ? updatedTask : task
|
||||
task.id === updatedTask.id
|
||||
? {
|
||||
...task,
|
||||
...updatedTask,
|
||||
// Explicitly preserve subtasks data
|
||||
subtasks:
|
||||
updatedTask.subtasks ||
|
||||
updatedTask.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
Subtasks:
|
||||
updatedTask.subtasks ||
|
||||
updatedTask.Subtasks ||
|
||||
task.subtasks ||
|
||||
task.Subtasks ||
|
||||
[],
|
||||
}
|
||||
: task
|
||||
),
|
||||
},
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export const isTaskOverdue = (task: {
|
|||
created_at?: string;
|
||||
today_move_count?: number;
|
||||
status: string | number;
|
||||
completed_at?: string;
|
||||
completed_at: string | null;
|
||||
}): boolean => {
|
||||
// If task is not in today plan, it's not overdue
|
||||
if (!task.today) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue