import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { CalendarDaysIcon, CalendarIcon, ArrowPathIcon, ListBulletIcon, PencilIcon, TrashIcon, EllipsisVerticalIcon, ChevronDownIcon, PlayIcon, PauseCircleIcon, CheckIcon, } from '@heroicons/react/24/outline'; import { TagIcon, FolderIcon, FireIcon } from '@heroicons/react/24/solid'; import { useTranslation } from 'react-i18next'; import TaskPriorityIcon from './TaskPriorityIcon'; import { Project } from '../../entities/Project'; import { Task, StatusType } from '../../entities/Task'; import { fetchSubtasks } from '../../utils/tasksService'; interface TaskHeaderProps { task: Task; project?: Project; onTaskClick: (e: React.MouseEvent) => void; onToggleCompletion?: () => void; hideProjectName?: boolean; onToggleToday?: (taskId: number, task?: Task) => Promise; onTaskUpdate?: (task: Task) => Promise; isOverdue?: boolean; // Props for subtasks functionality showSubtasks?: boolean; hasSubtasks?: boolean; onSubtasksToggle?: (e: React.MouseEvent) => void; // Props for edit and delete functionality onEdit?: (e: React.MouseEvent) => void; onDelete?: (e: React.MouseEvent) => void; isUpcomingView?: boolean; } const TaskHeader: React.FC = ({ task, project, onTaskClick, onToggleCompletion, hideProjectName = false, onToggleToday, onTaskUpdate, // Props for subtasks functionality showSubtasks, hasSubtasks, onSubtasksToggle, // Props for edit and delete functionality onEdit, onDelete, isUpcomingView = false, }) => { const { t } = useTranslation(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const buttonRef = useRef(null); const dropdownId = useRef( `dropdown-${Math.random().toString(36).substr(2, 9)}` ).current; const desktopCompletionMenuRef = useRef(null); const mobileCompletionMenuRef = useRef(null); const [completionMenuOpen, setCompletionMenuOpen] = useState< 'desktop' | 'mobile' | null >(null); const [isCompletingTask, setIsCompletingTask] = useState(false); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (isDropdownOpen && buttonRef.current) { const target = event.target as Node; const isOutsideButton = !buttonRef.current.contains(target); const currentDropdown = document.querySelector( `[data-dropdown-id="${dropdownId}"]` ); const isOutsideDropdown = !currentDropdown?.contains(target); if (isOutsideButton && isOutsideDropdown) { setIsDropdownOpen(false); } } }; // Listen for custom event to close this dropdown when another opens const handleCloseOtherDropdowns = (event: CustomEvent) => { if (event.detail.dropdownId !== dropdownId && isDropdownOpen) { setIsDropdownOpen(false); } }; if (isDropdownOpen) { document.addEventListener('click', handleClickOutside); document.addEventListener( 'closeOtherDropdowns', handleCloseOtherDropdowns as EventListener ); } return () => { document.removeEventListener('click', handleClickOutside); document.removeEventListener( 'closeOtherDropdowns', handleCloseOtherDropdowns as EventListener ); }; }, [isDropdownOpen, dropdownId]); useEffect(() => { if (!completionMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; const activeRef = completionMenuOpen === 'desktop' ? desktopCompletionMenuRef.current : mobileCompletionMenuRef.current; if (activeRef && activeRef.contains(target)) { return; } setCompletionMenuOpen(null); }; document.addEventListener('click', handleClickOutside); return () => { document.removeEventListener('click', handleClickOutside); }; }, [completionMenuOpen]); 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'); } }; const handleTodayToggle = async (e: React.MouseEvent) => { e.stopPropagation(); // Prevent opening task modal if (onToggleToday && task.id) { try { await onToggleToday(task.id, task); } catch (error) { console.error('Failed to toggle today status:', error); } } }; // Check if task has metadata (project, tags, due_date, recurrence_type, or recurring_parent_id) const hasMetadata = (project && !hideProjectName) || (task.tags && task.tags.length > 0) || task.due_date || (task.recurrence_type && task.recurrence_type !== 'none') || task.recurring_parent_id; const isTaskCompleted = task.status === 'done' || task.status === 2 || task.status === 'archived' || task.status === 3; const isTaskInProgress = task.status === 'in_progress' || task.status === 1; const completionButtonBorderClass = isTaskCompleted ? 'border-green-200 dark:border-green-900' : isTaskInProgress ? 'border-blue-200 dark:border-blue-900' : 'border-gray-200 dark:border-gray-700'; const completionButtonTextClass = isTaskCompleted ? 'text-green-600 dark:text-green-400' : isTaskInProgress ? 'text-blue-600 dark:text-blue-400' : 'text-gray-600 dark:text-gray-400'; const completionButtonHoverClass = isTaskCompleted ? 'hover:bg-green-50 dark:hover:bg-green-900/40' : isTaskInProgress ? 'hover:bg-blue-50 dark:hover:bg-blue-900/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800'; // Highlighted background for the active status button part const completionButtonMainBgClass = isTaskCompleted ? 'bg-green-100 dark:bg-green-900/50' : isTaskInProgress ? 'bg-blue-100 dark:bg-blue-900/50' : 'bg-gray-200 dark:bg-gray-700'; const completionButtonMainTextClass = isTaskCompleted ? 'text-green-900 dark:text-green-100 font-semibold' : isTaskInProgress ? 'text-blue-900 dark:text-blue-100 font-semibold' : 'text-gray-900 dark:text-gray-100 font-semibold'; const completionButtonMainClasses = `inline-flex items-center gap-2 text-sm transition ${completionButtonMainTextClass} ${completionButtonMainBgClass} ${completionButtonHoverClass} focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500`; const completionButtonChevronClasses = `inline-flex items-center justify-center transition ${completionButtonTextClass} ${completionButtonHoverClass} focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500`; const CompletionIcon = isTaskCompleted ? CheckIcon : isTaskInProgress ? PlayIcon : CheckIcon; const handleCompletionClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setCompletionMenuOpen(null); if (onToggleCompletion) { // Add animation delay when marking as done (not when undoing) if (!isTaskCompleted) { setIsCompletingTask(true); // Wait for green animation to complete (1200ms) await new Promise((resolve) => setTimeout(resolve, 1200)); } onToggleCompletion(); // Reset animation state after completion setTimeout(() => { setIsCompletingTask(false); }, 100); } }; return (
{ e.preventDefault(); e.stopPropagation(); onTaskClick(e); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onTaskClick(e as any); } }} > {/* Full view (md and larger) */}
{isUpcomingView ? (
{/* Full width title that wraps */}
{task.habit_mode && ( )} {task.original_name || task.name}
{/* Show project and tags info in upcoming view */} {project && !hideProjectName && (
{ // Prevent navigation if we're already on this project's page if ( window.location.pathname === `/project/${project.id}` ) { e.preventDefault(); } e.stopPropagation(); }} > {project.name}
)} {task.tags && task.tags.length > 0 && (
{task.tags.map((tag, index) => ( e.stopPropagation() } > {tag.name} {index < task.tags!.length - 1 && ', '} ))}
)}
) : (
{task.habit_mode && ( )} {task.original_name || task.name}
)} {/* Project, tags, due date, and recurrence in same row, with spacing when they exist */} {!isUpcomingView && (
{project && !hideProjectName && (
{ // Prevent navigation if we're already on this project's page if ( window.location.pathname === `/project/${project.id}` ) { e.preventDefault(); } e.stopPropagation(); }} > {project.name}
)} {task.tags && task.tags.length > 0 && (
{task.tags.map((tag, index) => ( e.stopPropagation() } > {tag.name} {index < task.tags!.length - 1 && ', '} ))}
)} {task.due_date && (
{formatDueDate(task.due_date)}
)} {task.recurrence_type && task.recurrence_type !== 'none' && (
{formatRecurrence( task.recurrence_type )}
)} {task.recurring_parent_id && (
{t( 'recurrence.instance', 'Recurring task instance' )}
)}
)}
{!isUpcomingView && !task.habit_mode && (
{/* Today Plan Controls */} {onToggleToday && ( )} {/* Show Subtasks Controls */} {hasSubtasks && !( task.status === 'archived' || task.status === 3 ) && ( )} {/* Three Dots Menu for Edit and Delete */} {(onEdit || onDelete) && (
{/* Dropdown Menu */} {isDropdownOpen && (
e.stopPropagation() } >
{/* Edit Button */} {onEdit && ( )} {/* Delete Button */} {onDelete && ( )}
)}
)}
{onToggleCompletion && (
{!isTaskCompleted && (task.status === 'not_started' || task.status === 0) && ( )} {isTaskInProgress && ( )}
{completionMenuOpen === 'desktop' && (
)}
)}
)}
{/* Mobile view (below md breakpoint) */}
{/* Priority Icon - Centered vertically with entire card */}
{/* Task content - full width */}
{/* Task Title */}
{task.habit_mode && ( )} {task.original_name || task.name}
{/* Project, tags, due date, and recurrence */}
{project && !hideProjectName && (
{ // Prevent navigation if we're already on this project's page if ( window.location.pathname === `/project/${project.id}` ) { e.preventDefault(); } e.stopPropagation(); }} > {project.name}
)} {task.tags && task.tags.length > 0 && (
{task.tags.map((tag, index) => ( e.stopPropagation() } > {tag.name} {index < task.tags!.length - 1 && ', '} ))}
)} {!isUpcomingView && task.due_date && (
{formatDueDate(task.due_date)}
)} {task.recurrence_type && task.recurrence_type !== 'none' && (
{formatRecurrence( task.recurrence_type )}
)} {task.recurring_parent_id && (
{t( 'recurrence.instance', 'Recurring task instance' )}
)}
{onToggleCompletion && (
{!isTaskCompleted && (task.status === 'not_started' || task.status === 0) && ( )} {isTaskInProgress && ( )}
{completionMenuOpen === 'mobile' && (
)}
)}
{/* Mobile 3-dot dropdown menu */} {!task.habit_mode && (
{/* Dropdown Menu - Positioned Relatively */} {isDropdownOpen && (
window.innerHeight ? 'translateY(-100%) translateY(-8px)' : 'none', }} onClick={(e) => e.stopPropagation()} >
{/* Today Plan Controls */} {onToggleToday && ( )} {/* Show Subtasks Controls */} {hasSubtasks && !( task.status === 'archived' || task.status === 3 ) && ( )} {/* Edit Button */} {onEdit && ( )} {/* Delete Button */} {onDelete && ( )}
)}
)}
); }; // Subtasks Display Component interface SubtasksDisplayProps { showSubtasks: boolean; loadingSubtasks: boolean; subtasks: Task[]; onTaskClick: (e: React.MouseEvent, task: Task) => void; } const SubtasksDisplay: React.FC = ({ showSubtasks, loadingSubtasks, subtasks, onTaskClick, }) => { const { t } = useTranslation(); if (!showSubtasks) return null; return (
{loadingSubtasks ? (
{t('loading.subtasks', 'Loading subtasks...')}
) : subtasks.length > 0 ? ( subtasks.map((subtask) => (
{ e.stopPropagation(); onTaskClick(e, subtask); }} >
{/* Left side - Task info */}
{subtask.original_name || subtask.name}
{/* Right side - Status indicator */}
{subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3 ? ( ) : null}
)) ) : (
{t('subtasks.noSubtasks', 'No subtasks found')}
)}
); }; // TaskWithSubtasks Component that combines both interface TaskWithSubtasksProps extends TaskHeaderProps { onSubtaskClick?: (subtask: Task) => void; } const TaskWithSubtasks: React.FC = (props) => { const [showSubtasks, setShowSubtasks] = useState(false); const [subtasks, setSubtasks] = useState([]); const [loadingSubtasks, setLoadingSubtasks] = useState(false); const [hasSubtasks, setHasSubtasks] = useState(false); // Check if task has subtasks using included data useEffect(() => { const hasSubtasksFromData = props.task.subtasks && props.task.subtasks.length > 0; setHasSubtasks(!!hasSubtasksFromData); // Set initial subtasks state if they are already loaded if (hasSubtasksFromData && props.task.subtasks) { setSubtasks(props.task.subtasks); } }, [props.task.id, props.task.subtasks]); const loadSubtasks = async () => { if (!props.task.id) return; // If subtasks are already included in the task data, use them if (props.task.subtasks && props.task.subtasks.length > 0) { setSubtasks(props.task.subtasks); return; } // Only fetch if not already included (fallback for older API responses) setLoadingSubtasks(true); try { const subtasksData = await fetchSubtasks(props.task.id); setSubtasks(subtasksData); } catch (error) { console.error('Failed to load subtasks:', error); setSubtasks([]); } finally { setLoadingSubtasks(false); } }; const handleSubtasksToggle = async (e: React.MouseEvent) => { e.stopPropagation(); // Prevent opening task modal if (!showSubtasks && subtasks.length === 0) { await loadSubtasks(); } setShowSubtasks(!showSubtasks); }; return ( <> { e.stopPropagation(); // Call the parent's onSubtaskClick handler if provided if (props.onSubtaskClick) { props.onSubtaskClick(task); } }} /> ); }; export { TaskWithSubtasks }; export default React.memo(TaskHeader);