import React, { useEffect, useState, useCallback } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { PencilSquareIcon, TrashIcon, TagIcon, FolderIcon, CalendarIcon, ExclamationTriangleIcon, ArrowPathIcon, ListBulletIcon, XMarkIcon, } from '@heroicons/react/24/outline'; import ConfirmDialog from '../Shared/ConfirmDialog'; import TaskModal from './TaskModal'; import { Task } from '../../entities/Task'; import { Project } from '../../entities/Project'; import { updateTask, deleteTask, toggleTaskCompletion, fetchTaskByUid, } from '../../utils/tasksService'; import { createProject } from '../../utils/projectsService'; import { useStore } from '../../store/useStore'; import { useToast } from '../Shared/ToastContext'; import TaskPriorityIcon from './TaskPriorityIcon'; import LoadingScreen from '../Shared/LoadingScreen'; import MarkdownRenderer from '../Shared/MarkdownRenderer'; import TaskTimeline from './TaskTimeline'; import { isTaskOverdue } from '../../utils/dateUtils'; const TaskDetails: React.FC = () => { const { uid } = useParams<{ uid: string }>(); const navigate = useNavigate(); const { t } = useTranslation(); const { showSuccessToast, showErrorToast } = useToast(); const projects = useStore((state: any) => state.projectsStore.projects); const tagsStore = useStore((state: any) => state.tagsStore); const tasksStore = useStore((state: any) => state.tasksStore); const task = useStore((state: any) => state.tasksStore.tasks.find((t: Task) => t.uid === uid) ); // Get subtasks from the task data (already loaded in global store) const subtasks = task?.subtasks || task?.Subtasks || []; // Local state const [loading, setLoading] = useState(!task); // Only show loading if task not in store const [error, setError] = useState(null); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [taskToDelete, setTaskToDelete] = useState(null); const [focusSubtasks, setFocusSubtasks] = useState(false); const [timelineRefreshKey, setTimelineRefreshKey] = useState(0); const [isOverdueAlertDismissed, setIsOverdueAlertDismissed] = useState(false); // Load tags early and check for pending modal state on mount useEffect(() => { // Preload tags if not already loaded if (!tagsStore.hasLoaded && !tagsStore.isLoading) { tagsStore.loadTags(); } try { // Check for subtasks modal state const pendingStateStr = sessionStorage.getItem('pendingModalState'); if (pendingStateStr) { const pendingState = JSON.parse(pendingStateStr); const isRecent = Date.now() - pendingState.timestamp < 2000; // Within 2 seconds const isCorrectTask = pendingState.taskId === uid; if (isRecent && isCorrectTask && pendingState.isOpen) { // Use microtask to avoid lifecycle method warning queueMicrotask(() => { setIsTaskModalOpen(true); setFocusSubtasks(pendingState.focusSubtasks); }); sessionStorage.removeItem('pendingModalState'); } } // Check for edit modal state const pendingEditStateStr = sessionStorage.getItem( 'pendingTaskEditModalState' ); if (pendingEditStateStr) { const pendingEditState = JSON.parse(pendingEditStateStr); const isRecent = Date.now() - pendingEditState.timestamp < 5000; // Within 5 seconds const isCorrectTask = pendingEditState.taskId === uid; if (isRecent && isCorrectTask && pendingEditState.isOpen) { // Use microtask to avoid lifecycle method warning queueMicrotask(() => { setIsTaskModalOpen(true); setFocusSubtasks(false); }); sessionStorage.removeItem('pendingTaskEditModalState'); } } } catch { sessionStorage.removeItem('pendingModalState'); sessionStorage.removeItem('pendingTaskEditModalState'); } }, [uid, tagsStore]); // Date and recurrence formatting functions (from TaskHeader) const formatDueDate = (dueDate: string) => { const today = new Date().toISOString().split('T')[0]; const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000) .toISOString() .split('T')[0]; const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000) .toISOString() .split('T')[0]; if (dueDate === today) return t('dateIndicators.today', 'TODAY'); if (dueDate === tomorrow) return t('dateIndicators.tomorrow', 'TOMORROW'); if (dueDate === yesterday) return t('dateIndicators.yesterday', 'YESTERDAY'); return new Date(dueDate).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); }; const formatRecurrence = (recurrenceType: string) => { switch (recurrenceType) { case 'daily': return t('recurrence.daily', 'Daily'); case 'weekly': return t('recurrence.weekly', 'Weekly'); case 'monthly': return t('recurrence.monthly', 'Monthly'); case 'monthly_weekday': return t('recurrence.monthlyWeekday', 'Monthly'); case 'monthly_last_day': return t('recurrence.monthlyLastDay', 'Monthly'); default: return t('recurrence.recurring', 'Recurring'); } }; useEffect(() => { const fetchTaskData = async () => { if (!uid) { setError('No task uid provided'); setLoading(false); return; } // If task is not in store, load it if (!task) { try { setLoading(true); const fetchedTask = await fetchTaskByUid(uid); // Add the task to the store tasksStore.setTasks([...tasksStore.tasks, fetchedTask]); } catch (fetchError) { setError('Task not found'); console.error('Error fetching task:', fetchError); } finally { setLoading(false); } } // Subtasks are already loaded as part of the task data from the global store }; fetchTaskData(); }, [uid, task, tasksStore]); const handleEdit = (e?: React.MouseEvent) => { if (e) { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); } // Store modal state in sessionStorage to persist across re-mounts const modalState = { isOpen: true, taskId: uid, timestamp: Date.now(), }; sessionStorage.setItem( 'pendingTaskEditModalState', JSON.stringify(modalState) ); setFocusSubtasks(false); setIsTaskModalOpen(true); }; const handleToggleCompletion = async () => { if (!task?.id) return; try { const updatedTask = await toggleTaskCompletion(task.id); // Update the task in the global store if (uid) { const updatedTask = await fetchTaskByUid(uid); const existingIndex = tasksStore.tasks.findIndex( (t: Task) => t.uid === uid ); if (existingIndex >= 0) { const updatedTasks = [...tasksStore.tasks]; updatedTasks[existingIndex] = updatedTask; tasksStore.setTasks(updatedTasks); } } const statusMessage = updatedTask.status === 'done' || updatedTask.status === 2 ? t('task.completedSuccess', 'Task marked as completed') : t('task.reopenedSuccess', 'Task reopened'); showSuccessToast(statusMessage); // Refresh timeline to show status change activity setTimelineRefreshKey((prev) => prev + 1); } catch (error) { console.error('Error toggling task completion:', error); showErrorToast( t('task.toggleError', 'Failed to update task status') ); } }; const handleTaskUpdate = async (updatedTask: Task) => { try { if (task?.id) { await updateTask(task.id, updatedTask); // Update the task in the global store if (uid) { const updatedTask = await fetchTaskByUid(uid); const existingIndex = tasksStore.tasks.findIndex( (t: Task) => t.uid === uid ); if (existingIndex >= 0) { const updatedTasks = [...tasksStore.tasks]; updatedTasks[existingIndex] = updatedTask; tasksStore.setTasks(updatedTasks); } } showSuccessToast( t('task.updateSuccess', 'Task updated successfully') ); // Subtasks will be automatically updated when the task is reloaded from the global store // Refresh timeline to show new activity setTimelineRefreshKey((prev) => prev + 1); } setIsTaskModalOpen(false); } catch (error) { console.error('Error updating task:', error); showErrorToast(t('task.updateError', 'Failed to update task')); } }; const handleDeleteClick = () => { if (task) { setTaskToDelete(task); setIsConfirmDialogOpen(true); } }; const handleDeleteConfirm = async () => { if (taskToDelete?.id) { try { await deleteTask(taskToDelete.id); showSuccessToast( t('task.deleteSuccess', 'Task deleted successfully') ); navigate('/today'); // Navigate back to today view after deletion } catch (error) { console.error('Error deleting task:', error); showErrorToast(t('task.deleteError', 'Failed to delete task')); } } setIsConfirmDialogOpen(false); setTaskToDelete(null); }; const handleCreateProject = async (name: string): Promise => { try { return await createProject({ name }); } catch (error) { console.error('Error creating project:', error); throw error; } }; const handleSubtaskClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); // Store the intent to open modal in sessionStorage (survives re-mounts) const modalState = { isOpen: true, focusSubtasks: true, taskId: uid, timestamp: Date.now(), }; sessionStorage.setItem( 'pendingModalState', JSON.stringify(modalState) ); // Set state immediately setFocusSubtasks(true); setIsTaskModalOpen(true); }, [uid] ); if (loading) { return ; } if (error || !task) { return (

{error || t('task.notFound', 'Task Not Found')}

{t( 'task.notFoundDescription', 'The task you are looking for does not exist or has been deleted.' )}

); } return (
{/* Header Section with Title and Action Buttons */}

{task.name}

{/* Project, tags, due date, and recurrence under title */} {(task.Project || (task.tags && task.tags.length > 0) || task.due_date || (task.recurrence_type && task.recurrence_type !== 'none')) && (
{task.Project && (
{task.Project.name}
)} {task.Project && task.tags && task.tags.length > 0 && ( )} {task.tags && task.tags.length > 0 && (
{task.tags.map( ( tag: any, index: number ) => ( {tag.name} {index < task.tags! .length - 1 && ', '} ) )}
)} {(task.Project || (task.tags && task.tags.length > 0)) && task.due_date && ( )} {task.due_date && (
{formatDueDate(task.due_date)}
)} {(task.Project || (task.tags && task.tags.length > 0) || task.due_date) && task.recurrence_type && task.recurrence_type !== 'none' && ( )} {task.recurrence_type && task.recurrence_type !== 'none' && (
{formatRecurrence( task.recurrence_type )}
)}
)}
{/* Overdue Alert */} {isTaskOverdue(task) && !isOverdueAlertDismissed && (

{t( 'task.overdueAlert', "This task was in your plan yesterday and wasn't completed." )}

{task.today_move_count && task.today_move_count > 1 ? t( 'task.overdueMultipleDays', `This task has been postponed ${task.today_move_count} times.` ) : t( 'task.overdueYesterday', 'Consider prioritizing this task or breaking it into smaller steps.' )}

)} {/* Content - Two column layout */}
{/* Left Column - Notes and Subtasks */}
{/* Notes Section - Always Visible */}

{t('task.content', 'Content')}

{task.note ? (
) : (
{t( 'task.noNotes', 'No content added yet' )}
)}
{/* Subtasks Section - Always Visible */}

{t('task.subtasks', 'Subtasks')}

{subtasks.length > 0 ? (
{subtasks.map((subtask: Task) => (
e.stopPropagation() } > { e?.stopPropagation(); if ( subtask.id ) { try { await toggleTaskCompletion( subtask.id ); // Reload subtasks after toggling completion if ( task?.id ) { // Refresh task data which includes updated subtasks if ( uid ) { const updatedTask = await fetchTaskByUid( uid ); const existingIndex = tasksStore.tasks.findIndex( ( t: Task ) => t.uid === uid ); if ( existingIndex >= 0 ) { const updatedTasks = [ ...tasksStore.tasks, ]; updatedTasks[ existingIndex ] = updatedTask; tasksStore.setTasks( updatedTasks ); } } // Refresh timeline to show subtask completion activity setTimelineRefreshKey( ( prev ) => prev + 1 ); } } catch (error) { console.error( 'Error toggling subtask completion:', error ); } } }} />
{subtask.name}
))}
) : (
{t( 'task.noSubtasks', 'No subtasks yet' )}
)}
{/* Right Column - Recent Activity */}

{t('task.recentActivity', 'Recent Activity')}

{/* End of main content sections */} {/* Task Modal for Editing - Only render when we have task data */} {task && ( { setIsTaskModalOpen(false); setFocusSubtasks(false); // Clear pending state when modal is closed sessionStorage.removeItem('pendingModalState'); sessionStorage.removeItem( 'pendingTaskEditModalState' ); }} onSave={handleTaskUpdate} onDelete={async (taskId: number) => { await deleteTask(taskId); navigate('/today'); }} projects={projects} onCreateProject={handleCreateProject} showToast={false} initialSubtasks={subtasks} autoFocusSubtasks={focusSubtasks} /> )} {/* Confirm Delete Dialog */} {isConfirmDialogOpen && taskToDelete && ( { setIsConfirmDialogOpen(false); setTaskToDelete(null); }} /> )}
); }; export default TaskDetails;