import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Task } from '../../entities/Task'; import { Project } from '../../entities/Project'; import TaskHeader from './TaskHeader'; import { useToast } from '../Shared/ToastContext'; import TaskPriorityIcon from './TaskPriorityIcon'; import { isTaskCompleted } from '../../constants/taskStatus'; // Import SubtasksDisplay component from TaskHeader interface SubtasksDisplayProps { loadingSubtasks: boolean; subtasks: Task[]; onTaskClick: (e: React.MouseEvent, task: Task) => void; loadSubtasks: () => Promise; onSubtaskUpdate: (updatedSubtask: Task) => void; } const getPriorityBorderClassName = ( priority?: Task['priority'] | number ): string => { let normalizedPriority = priority; if (typeof normalizedPriority === 'number') { const priorityNames: Array<'low' | 'medium' | 'high'> = [ 'low', 'medium', 'high', ]; normalizedPriority = priorityNames[normalizedPriority] || undefined; } switch (normalizedPriority) { case 'high': return 'border-l-4 border-l-red-500'; case 'medium': return 'border-l-4 border-l-yellow-400'; case 'low': return 'border-l-4 border-l-blue-400'; default: return 'border-l-4 border-l-transparent'; } }; const SubtasksDisplay: React.FC = ({ loadingSubtasks, subtasks, onTaskClick, loadSubtasks, onSubtaskUpdate, }) => { const { t } = useTranslation(); if (loadingSubtasks) { return (
{t('loading.subtasks', 'Loading subtasks...')}
); } if (subtasks.length === 0) { return (
{t('subtasks.noSubtasks', 'No subtasks found')}
); } return (
{subtasks.map((subtask) => { const borderClass = isTaskCompleted(subtask.status) ? 'border-l-4 border-l-green-500' : getPriorityBorderClassName(subtask.priority); return (
{ e.stopPropagation(); onTaskClick(e, subtask); }} >
{ if (subtask.uid) { try { const updatedSubtask = await toggleTaskCompletion( subtask.uid, subtask ); if ( updatedSubtask.parent_child_logic_executed ) { setTimeout(() => { window.location.reload(); }, 200); return; } onSubtaskUpdate( updatedSubtask ); } catch (error) { console.error( 'Error toggling subtask completion:', error ); await loadSubtasks(); } } }} /> {subtask.original_name || subtask.name}
{isTaskCompleted(subtask.status) && ( )}
); })}
); }; import TaskModal from './TaskModal'; import { toggleTaskCompletion, fetchSubtasks } from '../../utils/tasksService'; import { isTaskOverdue } from '../../utils/dateUtils'; import { useTranslation } from 'react-i18next'; import ConfirmDialog from '../Shared/ConfirmDialog'; import { useStore } from '../../store/useStore'; import { getApiPath } from '../../config/paths'; interface TaskItemProps { task: Task; onTaskUpdate: (task: Task) => Promise; onTaskCompletionToggle?: (task: Task) => void; onTaskDelete: (taskUid: string) => void; projects: Project[]; hideProjectName?: boolean; onToggleToday?: (taskId: number, task?: Task) => Promise; isUpcomingView?: boolean; showCompletedTasks?: boolean; } const TaskItem: React.FC = ({ task, onTaskUpdate, onTaskCompletionToggle, onTaskDelete, projects, hideProjectName = false, onToggleToday, isUpcomingView = false, showCompletedTasks = false, }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { modalStore } = useStore(); const isModalOpen = modalStore.isTaskModalOpen(task.id); const [subtaskModalOpen, setSubtaskModalOpen] = useState(false); const [selectedSubtask, setSelectedSubtask] = useState(null); const [projectList, setProjectList] = useState(projects); const [parentTaskModalOpen, setParentTaskModalOpen] = useState(false); const [parentTask, setParentTask] = useState(null); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const { showErrorToast } = useToast(); const [isAnimatingOut, setIsAnimatingOut] = useState(false); // Subtasks state const [subtasks, setSubtasks] = useState([]); const [loadingSubtasks, setLoadingSubtasks] = useState(false); const [showSubtasks, setShowSubtasks] = useState(false); // Update projectList when projects prop changes useEffect(() => { setProjectList(projects); }, [projects]); const loadSubtasks = useCallback(async () => { if (!task.id) return; setLoadingSubtasks(true); try { const subtasksData = await fetchSubtasks(task.id); setSubtasks(subtasksData); } catch (error) { console.error('Failed to load subtasks:', error); setSubtasks([]); } finally { setLoadingSubtasks(false); } }, [task.id]); // Calculate completion percentage const calculateCompletionPercentage = () => { if (subtasks.length === 0) return 0; const completedCount = subtasks.filter( (subtask) => subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3 ).length; return Math.round((completedCount / subtasks.length) * 100); }; const completionPercentage = calculateCompletionPercentage(); const hasInitialSubtasks = (task.subtasks && task.subtasks.length > 0) || (task.Subtasks && task.Subtasks.length > 0); const shouldShowSubtasksIcon = hasInitialSubtasks || subtasks.length > 0 || loadingSubtasks; // Check if task has subtasks using the included subtasks data useEffect(() => { // Handle both 'subtasks' and 'Subtasks' property names (case sensitivity) const subtasksData = task.subtasks || task.Subtasks || []; setSubtasks(subtasksData); }, [task.id, task.subtasks, task.Subtasks]); useEffect(() => { setShowSubtasks(false); }, [task.id]); const handleTaskClick = () => { if (task.uid) { if (task.habit_mode) { navigate(`/habit/${task.uid}`); } else { navigate(`/task/${task.uid}`); } } }; const handleSubtaskClick = async () => { // Navigate to the parent task URL (not the subtask URL) if (task.uid) { navigate(`/task/${task.uid}`); } }; const handleSubtaskSave = async (updatedSubtask: Task) => { await onTaskUpdate(updatedSubtask); setSubtaskModalOpen(false); setSelectedSubtask(null); }; const handleSubtasksToggle = async (e: React.MouseEvent) => { e.stopPropagation(); if (!showSubtasks) { if (subtasks.length === 0) { await loadSubtasks(); } setShowSubtasks(true); } else { setShowSubtasks(false); } }; const handleSubtaskDelete = async () => { if (selectedSubtask && selectedSubtask.uid) { await onTaskDelete(selectedSubtask.uid); setSubtaskModalOpen(false); setSelectedSubtask(null); } }; const handleParentTaskSave = async (updatedParentTask: Task) => { await onTaskUpdate(updatedParentTask); setParentTaskModalOpen(false); setParentTask(null); }; const handleParentTaskDelete = async () => { if (parentTask && parentTask.uid) { await onTaskDelete(parentTask.uid); setParentTaskModalOpen(false); setParentTask(null); } }; const handleSave = async (updatedTask: Task) => { try { await onTaskUpdate(updatedTask); // Let TaskModal invoke onClose so unsaved-change checks remain consistent } catch (error: any) { console.error('Task update failed:', error); showErrorToast(t('errors.permissionDenied', 'Permission denied')); } }; const handleEdit = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); modalStore.openTaskModal(task.id); }; const handleDeleteClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setIsConfirmDialogOpen(true); }; const handleConfirmDelete = () => { setIsConfirmDialogOpen(false); handleDelete(); }; const handleDelete = async () => { if (task.uid) { try { await onTaskDelete(task.uid); } catch (error: any) { console.error('Task delete failed:', error); showErrorToast( t('errors.permissionDenied', 'Permission denied') ); } } }; const handleToggleCompletion = async () => { if (task.id) { try { // Check if task is being completed (not uncompleted) const isCompletingTask = task.status !== 'done' && task.status !== 2 && task.status !== 'archived' && task.status !== 3; // If completing the task in upcoming view and not showing completed tasks, trigger animation if (isCompletingTask && isUpcomingView && !showCompletedTasks) { setIsAnimatingOut(true); // Wait for animation to complete before updating state await new Promise((resolve) => setTimeout(resolve, 300)); } const response = await toggleTaskCompletion(task.uid!, task); // Handle the updated task if (onTaskCompletionToggle) { onTaskCompletionToggle(response); } else { // Merge the response with existing task data to preserve subtasks const mergedTask = { ...task, ...response, // Explicitly preserve subtasks data from original task subtasks: response.subtasks || response.Subtasks || task.subtasks || task.Subtasks || [], Subtasks: response.subtasks || response.Subtasks || task.subtasks || task.Subtasks || [], }; await onTaskUpdate(mergedTask); } // 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( getApiPath(`task/${task.uid}`) ); 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); setIsAnimatingOut(false); // Reset animation state on error } } }; const handleCreateProject = async (name: string): Promise => { try { const response = await fetch(getApiPath('project'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, active: true }), }); if (!response.ok) { throw new Error('Failed to create project'); } const newProject = await response.json(); setProjectList((prevProjects) => [...prevProjects, newProject]); return newProject; } catch (error) { console.error('Error creating project:', error); throw error; } }; // Use the project from the task's included data if available, otherwise find from projectList let project = task.Project || projectList.find((p) => p.id === task.project_id); // If project exists but doesn't have an ID, add the ID from task.project_id if (project && !project.id && task.project_id) { project = { ...project, id: task.project_id }; } // Check if task is in progress to apply pulsing border animation 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 priorityBorderClass = isTaskCompleted(task.status) ? 'border-l-4 border-l-green-500' : getPriorityBorderClassName(task.priority); return ( <>
{/* Progress bar at bottom of parent task */} {subtasks.length > 0 && (
)}
{/* Hide subtasks display for archived tasks */} {showSubtasks && (subtasks.length > 0 || loadingSubtasks) && !(task.status === 'archived' || task.status === 3) && ( { e.stopPropagation(); handleSubtaskClick(); }} loadSubtasks={loadSubtasks} onSubtaskUpdate={(updatedSubtask) => { setSubtasks((prev) => prev.map((st) => st.id === updatedSubtask.id ? updatedSubtask : st ) ); }} /> )} modalStore.closeTaskModal()} task={task} onSave={handleSave} onDelete={handleDelete} projects={projectList} onCreateProject={handleCreateProject} initialSubtasks={task.subtasks || task.Subtasks || []} /> {selectedSubtask && ( { setSubtaskModalOpen(false); setSelectedSubtask(null); }} task={selectedSubtask} onSave={handleSubtaskSave} onDelete={handleSubtaskDelete} projects={projectList} onCreateProject={handleCreateProject} /> )} {parentTask && ( { setParentTaskModalOpen(false); setParentTask(null); }} task={parentTask} onSave={handleParentTaskSave} onDelete={handleParentTaskDelete} projects={projectList} onCreateProject={handleCreateProject} autoFocusSubtasks={true} /> )} {/* Confirm Delete Dialog */} {isConfirmDialogOpen && ( setIsConfirmDialogOpen(false)} /> )} ); }; export default React.memo(TaskItem);