import React, { useState, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { PriorityType, StatusType, Task } from '../../entities/Task'; import ConfirmDialog from '../Shared/ConfirmDialog'; import { useToast } from '../Shared/ToastContext'; import TimelinePanel from './TimelinePanel'; import { Project } from '../../entities/Project'; import { useStore } from '../../store/useStore'; import { fetchTaskById, fetchSubtasks } from '../../utils/tasksService'; import { getTaskIntelligenceEnabled } from '../../utils/profileService'; import { analyzeTaskName, TaskAnalysis, } from '../../utils/taskIntelligenceService'; import { useTranslation } from 'react-i18next'; import { ClockIcon, TagIcon, FolderIcon, Cog6ToothIcon, ArrowPathIcon, TrashIcon, Squares2X2Icon, } from '@heroicons/react/24/outline'; // Import form sections import TaskTitleSection from './TaskForm/TaskTitleSection'; import TaskContentSection from './TaskForm/TaskContentSection'; import TaskTagsSection from './TaskForm/TaskTagsSection'; import TaskProjectSection from './TaskForm/TaskProjectSection'; import TaskMetadataSection from './TaskForm/TaskMetadataSection'; import TaskRecurrenceSection from './TaskForm/TaskRecurrenceSection'; import TaskSubtasksSection from './TaskForm/TaskSubtasksSection'; interface TaskModalProps { isOpen: boolean; onClose: () => void; task: Task; onSave: (task: Task) => void; onDelete: (taskId: number) => Promise; projects: Project[]; onCreateProject: (name: string) => Promise; onEditParentTask?: (parentTask: Task) => void; } const TaskModal: React.FC = ({ isOpen, onClose, task, onSave, onDelete, projects, onCreateProject, onEditParentTask, }) => { const { tagsStore: { tags: availableTags }, } = useStore(); const [formData, setFormData] = useState(task); const [tags, setTags] = useState( task.tags?.map((tag) => tag.name) || [] ); const [filteredProjects, setFilteredProjects] = useState( projects || [] ); const [newProjectName, setNewProjectName] = useState(''); const [isCreatingProject, setIsCreatingProject] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); const modalRef = useRef(null); const [isClosing, setIsClosing] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [parentTask, setParentTask] = useState(null); const [parentTaskLoading, setParentTaskLoading] = useState(false); const [taskAnalysis, setTaskAnalysis] = useState(null); const [taskIntelligenceEnabled, setTaskIntelligenceEnabled] = useState(true); const [isTimelineExpanded, setIsTimelineExpanded] = useState(false); const [subtasks, setSubtasks] = useState>([]); // Collapsible section states const [expandedSections, setExpandedSections] = useState({ tags: false, project: false, metadata: false, recurrence: false, subtasks: false, }); const { showSuccessToast, showErrorToast } = useToast(); const { t } = useTranslation(); const scrollToSubtasksSection = () => { setTimeout(() => { const subtasksSection = document.querySelector('[data-section="subtasks"]'); if (subtasksSection) { subtasksSection.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }, 300); // Give time for section to expand }; const toggleSection = useCallback( (section: keyof typeof expandedSections) => { setExpandedSections((prev) => { const newExpanded = { ...prev, [section]: !prev[section], }; // Auto-scroll to show the expanded section if (newExpanded[section]) { // Special handling for subtasks section if (section === 'subtasks') { scrollToSubtasksSection(); } else { setTimeout(() => { const scrollContainer = document.querySelector( '.absolute.inset-0.overflow-y-auto' ); if (scrollContainer) { scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth', }); } }, 100); // Small delay to ensure DOM is updated } } return newExpanded; }); }, [] ); useEffect(() => { setFormData(task); setTags(task.tags?.map((tag) => tag.name) || []); // Analyze task name and show helper when modal opens (only if intelligence is enabled) if (isOpen && task.name && taskIntelligenceEnabled) { const analysis = analyzeTaskName(task.name); setTaskAnalysis(analysis); } else { setTaskAnalysis(null); } // Safely find the current project, handling the case where projects might be undefined const currentProject = projects?.find( (project) => project.id === task.project_id ); setNewProjectName(currentProject ? currentProject.name : ''); // Fetch parent task if this is a child task const fetchParentTask = async () => { if (task.recurring_parent_id && isOpen) { setParentTaskLoading(true); try { const parent = await fetchTaskById( task.recurring_parent_id ); setParentTask(parent); } catch (error) { console.error('Error fetching parent task:', error); setParentTask(null); } finally { setParentTaskLoading(false); } } else { setParentTask(null); } }; fetchParentTask(); }, [task, projects, isOpen, taskIntelligenceEnabled]); // Fetch task intelligence setting when modal opens useEffect(() => { const fetchTaskIntelligenceSetting = async () => { if (isOpen) { try { const enabled = await getTaskIntelligenceEnabled(); setTaskIntelligenceEnabled(enabled); } catch (error) { console.error( 'Error fetching task intelligence setting:', error ); setTaskIntelligenceEnabled(true); // Default to enabled } } }; fetchTaskIntelligenceSetting(); }, [isOpen]); const handleEditParent = () => { if (parentTask && onEditParentTask) { onEditParentTask(parentTask); onClose(); // Close current modal } }; const handleParentRecurrenceChange = (field: string, value: any) => { // Update the parent task data in local state if (parentTask) { setParentTask({ ...parentTask, [field]: value }); } // Also update the form data to reflect the change setFormData((prev) => ({ ...prev, [field]: value, update_parent_recurrence: true, })); }; // Note: Tags loading removed to prevent modal closing issues // Tags will be loaded by other components or on app startup const getPriorityString = ( priority: PriorityType | number | undefined ): PriorityType => { if (typeof priority === 'number') { const priorityNames: PriorityType[] = ['low', 'medium', 'high']; return priorityNames[priority] || 'medium'; } return priority || 'medium'; }; const handleChange = ( e: React.ChangeEvent< HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); // Analyze task name in real-time (only if intelligence is enabled) if (name === 'name' && taskIntelligenceEnabled) { const analysis = analyzeTaskName(value); setTaskAnalysis(analysis); } }; const handleRecurrenceChange = (field: string, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleTagsChange = useCallback((newTags: string[]) => { setTags(newTags); setFormData((prev) => ({ ...prev, tags: newTags.map((name) => ({ name })), })); }, []); const handleProjectSearch = (e: React.ChangeEvent) => { const query = e.target.value.toLowerCase(); setNewProjectName(query); setDropdownOpen(true); setFilteredProjects( projects.filter((project) => project.name.toLowerCase().includes(query) ) ); }; const handleProjectSelection = (project: Project) => { setFormData({ ...formData, project_id: project.id }); setNewProjectName(project.name); setDropdownOpen(false); }; const handleShowAllProjects = () => { setNewProjectName(''); setFilteredProjects(projects); setDropdownOpen(!dropdownOpen); }; const handleCreateProject = async () => { if (newProjectName.trim() !== '') { setIsCreatingProject(true); try { const newProject = await onCreateProject(newProjectName); setFormData({ ...formData, project_id: newProject.id }); setFilteredProjects([...filteredProjects, newProject]); setNewProjectName(newProject.name); setDropdownOpen(false); showSuccessToast(t('success.projectCreated')); } catch (error) { showErrorToast(t('errors.projectCreationFailed')); console.error('Error creating project:', error); } finally { setIsCreatingProject(false); } } }; const handleSubmit = () => { onSave({ ...formData, tags: tags.map((tag) => ({ name: tag })), subtasks: subtasks } as any); const taskLink = ( {t('task.updated', 'Task')}{' '} {formData.name} {' '} {t('task.updatedSuccessfully', 'updated successfully!')} ); showSuccessToast(taskLink); handleClose(); }; const handleDeleteClick = () => { setShowConfirmDialog(true); }; const handleDeleteConfirm = async () => { if (formData.id) { try { await onDelete(formData.id); const taskLink = ( {t('task.deleted', 'Task')}{' '} {formData.name} {' '} {t('task.deletedSuccessfully', 'deleted successfully!')} ); showSuccessToast(taskLink); setShowConfirmDialog(false); handleClose(); } catch (error) { console.error('Failed to delete task:', error); showErrorToast(t('task.deleteError', 'Failed to delete task')); } } }; const handleClose = () => { setIsClosing(true); setTimeout(() => { onClose(); setIsClosing(false); }, 300); }; useEffect(() => { setFilteredProjects(projects || []); }, [projects]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; // Ignore clicks on dropdown menus rendered via portal if ( target && (target.closest('.recurrence-dropdown-menu') || target.closest('.number-dropdown-menu') || target.closest('.date-picker-menu') || target.closest('[class*="fixed z-50"]') || target.closest('[class*="z-50"]')) ) { return; } if ( modalRef.current && !modalRef.current.contains(event.target as Node) ) { handleClose(); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); // Disable body scroll when modal is open document.body.style.overflow = 'hidden'; } else { // Re-enable body scroll when modal is closed document.body.style.overflow = 'unset'; } return () => { document.removeEventListener('mousedown', handleClickOutside); // Clean up: re-enable body scroll document.body.style.overflow = 'unset'; }; }, [isOpen]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleClose(); } }; if (isOpen) { document.addEventListener('keydown', handleKeyDown); } return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen]); // Load existing subtasks when modal opens useEffect(() => { if (isOpen && task.id) { const loadExistingSubtasks = async () => { try { const existingSubtasks = await fetchSubtasks(task.id!); const subtaskData = existingSubtasks.map(subtask => ({ id: subtask.id, name: subtask.name, isNew: false, })); setSubtasks(subtaskData); } catch (error) { // Handle silently - don't show error for this setSubtasks([]); } }; loadExistingSubtasks(); } else if (!isOpen) { // Reset subtasks when modal closes setSubtasks([]); } }, [isOpen, task.id]); if (!isOpen) return null; return createPortal( <>
{/* Main Form Section */}
{/* Task Title Section - Always Visible */} {/* Content Section - Always Visible */} {/* Expandable Sections - Only show when expanded */} {expandedSections.tags && (

{t( 'forms.task.labels.tags', 'Tags' )}

tag.name ) || [] } onTagsChange={ handleTagsChange } availableTags={ availableTags } />
)} {expandedSections.project && (

{t( 'forms.task.labels.project', 'Project' )}

)} {expandedSections.metadata && (

{t( 'forms.task.statusAndOptions', 'Status & Options' )}

{ // Universal rule: when setting status to in_progress, also add to today const updatedData = { ...formData, status: value, }; if ( value === 'in_progress' ) { updatedData.today = true; } setFormData( updatedData ); }} onPriorityChange={( value: PriorityType ) => setFormData({ ...formData, priority: value, }) } onDueDateChange={ handleChange } />
)} {expandedSections.recurrence && (

{t( 'forms.task.recurrence', 'Recurrence' )}

)} {expandedSections.subtasks && (

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

)}
{/* Timeline Panel - Show when expanded on mobile only */} {isTimelineExpanded && (
setIsTimelineExpanded( !isTimelineExpanded ) } />
)} {/* Section Icons - Above border, split layout */}
{/* Left side: Section icons */}
{/* Tags Toggle */} {/* Project Toggle */} {/* Status & Options Toggle */} {/* Recurrence Toggle */} {/* Subtasks Toggle */}
{/* Right side: Timeline Toggle Button */}
{/* Action Buttons - Below border with custom layout */}
{/* Left side: Delete and Cancel */}
{task.id && ( )}
{/* Right side: Save */}
{/* Timeline Panel - Desktop Sidebar */} setIsTimelineExpanded(!isTimelineExpanded) } />
{showConfirmDialog && ( setShowConfirmDialog(false)} /> )} , document.body ); }; export default TaskModal;