diff --git a/frontend/components/Task/TaskDetails.tsx b/frontend/components/Task/TaskDetails.tsx index 70e50ed..0cbbcd6 100644 --- a/frontend/components/Task/TaskDetails.tsx +++ b/frontend/components/Task/TaskDetails.tsx @@ -10,6 +10,7 @@ import { deleteTask, fetchTaskByUid, fetchTaskNextIterations, + fetchSubtasks, TaskIteration, toggleTaskCompletion, } from '../../utils/tasksService'; @@ -57,8 +58,7 @@ const TaskDetails: React.FC = () => { const [loadingIterations, setLoadingIterations] = useState(false); const [parentTask, setParentTask] = useState(null); const [loadingParent, setLoadingParent] = useState(false); - const [isEditingSubtasks, setIsEditingSubtasks] = useState(false); - const [editedSubtasks, setEditedSubtasks] = useState([]); + const [pendingSubtasks, setPendingSubtasks] = useState([]); const [actionsMenuOpen, setActionsMenuOpen] = useState(false); const actionsMenuRef = useRef(null); useEffect(() => { @@ -116,6 +116,7 @@ const TaskDetails: React.FC = () => { }); const [activePill, setActivePill] = useState('overview'); const [attachmentCount, setAttachmentCount] = useState(0); + const [hasLoadedSubtasks, setHasLoadedSubtasks] = useState(false); useEffect(() => { setEditedDueDate(task?.due_date || ''); @@ -187,7 +188,6 @@ const TaskDetails: React.FC = () => { setRecurrenceForm((prev) => { const updated = { ...prev, [field]: value }; - // Set default values when switching to monthly recurrence if ( field === 'recurrence_type' && value === 'monthly' && @@ -307,7 +307,6 @@ const TaskDetails: React.FC = () => { } } - // Check if due date is in the past if (editedDueDate) { const dueDate = new Date(editedDueDate); const today = new Date(); @@ -463,7 +462,6 @@ const TaskDetails: React.FC = () => { fetchTaskData(); }, [uid, task, tasksStore]); - // Load attachment count when task is loaded useEffect(() => { const loadAttachmentCount = async () => { if (task?.uid) { @@ -479,6 +477,49 @@ const TaskDetails: React.FC = () => { loadAttachmentCount(); }, [task?.uid]); + useEffect(() => { + setHasLoadedSubtasks(false); + }, [uid]); + + useEffect(() => { + const loadSubtasks = async () => { + const subtasksAlreadyLoaded = task?.subtasks && task.subtasks.length > 0; + + if ( + activePill === 'subtasks' && + task?.uid && + !hasLoadedSubtasks && + !subtasksAlreadyLoaded + ) { + try { + const fetchedSubtasks = await fetchSubtasks(task.uid); + setHasLoadedSubtasks(true); + + const existingIndex = tasksStore.tasks.findIndex( + (t: Task) => t.uid === task.uid + ); + if (existingIndex >= 0) { + const updatedTasks = [...tasksStore.tasks]; + updatedTasks[existingIndex] = { + ...task, + subtasks: fetchedSubtasks, + }; + tasksStore.setTasks(updatedTasks); + } + } catch (error) { + console.error('Error loading subtasks:', error); + setHasLoadedSubtasks(true); + } + } + }; + + loadSubtasks(); + }, [activePill, task?.uid, task?.subtasks, hasLoadedSubtasks, tasksStore]); + + useEffect(() => { + setPendingSubtasks(subtasks); + }, [subtasks]); + useEffect(() => { const loadNextIterations = async () => { if ( @@ -488,7 +529,6 @@ const TaskDetails: React.FC = () => { ) { try { setLoadingIterations(true); - // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( task.uid! ); @@ -508,7 +548,6 @@ const TaskDetails: React.FC = () => { try { setLoadingIterations(true); - // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( parentTask.uid ); @@ -559,20 +598,29 @@ const TaskDetails: React.FC = () => { loadParentTask(); }, [task?.recurring_parent_uid]); - const handleStartSubtasksEdit = () => { - setIsEditingSubtasks(true); - setEditedSubtasks([...subtasks]); - }; - - const handleSaveSubtasks = async () => { + const handleSaveSubtasks = async (subtasksToSave: Task[]) => { if (!task?.uid) { - setIsEditingSubtasks(false); - setEditedSubtasks([]); + return; + } + + const hasChanges = + subtasksToSave.length !== subtasks.length || + subtasksToSave.some( + (ps, i) => + !subtasks[i] || + ps.name !== subtasks[i].name || + ps.status !== subtasks[i].status || + (ps as any)._isNew || + (ps as any)._isEdited || + (ps as any)._statusChanged + ); + + if (!hasChanges) { return; } try { - await updateTask(task.uid, { ...task, subtasks: editedSubtasks }); + await updateTask(task.uid, { ...task, subtasks: subtasksToSave }); if (uid) { const updatedTask = await fetchTaskByUid(uid); @@ -586,45 +634,13 @@ const TaskDetails: React.FC = () => { } } - showSuccessToast( - t('task.subtasksUpdated', 'Subtasks updated successfully') - ); - setIsEditingSubtasks(false); - setTimelineRefreshKey((prev) => prev + 1); } catch (error) { console.error('Error updating subtasks:', error); showErrorToast( t('task.subtasksUpdateError', 'Failed to update subtasks') ); - setEditedSubtasks([...subtasks]); - setIsEditingSubtasks(false); - } - }; - - const handleCancelSubtasksEdit = () => { - setIsEditingSubtasks(false); - setEditedSubtasks([]); - }; - - const handleToggleSubtaskCompletion = async (subtask: Task) => { - if (!subtask.uid) return; - try { - await toggleTaskCompletion(subtask.uid, subtask); - 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); - } - } - setTimelineRefreshKey((prev) => prev + 1); - } catch (error) { - console.error('Error toggling subtask completion:', error); + setPendingSubtasks([...subtasks]); } }; @@ -715,13 +731,11 @@ const TaskDetails: React.FC = () => { try { setLoadingIterations(true); if (isTemplateTask) { - // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( latestTask.uid! ); setNextIterations(iterations); } else if (canUseParentIterations && parentTask?.uid) { - // Don't pass startFromDate - let backend default to today const iterations = await fetchTaskNextIterations( parentTask.uid ); @@ -1071,7 +1085,6 @@ const TaskDetails: React.FC = () => { return (
- {/* Header Section with Title and Action Buttons */} { subtasksCount={subtasks.length} /> - {/* Content - Full width layout */}
- {/* Overview Pill */} {activePill === 'overview' && (
- {/* Left Column - Main Content */}
{ />
- {/* Right Column - Project and Tags */}
{
)} - {/* Recurrence Pill */} {activePill === 'recurrence' && (
{
)} - {/* Subtasks Pill */} {activePill === 'subtasks' && (
)} - {/* Attachments Pill */} {activePill === 'attachments' && (
{
)} - {/* Activity Pill */} {activePill === 'activity' && (
{
)}
- {/* End of main content sections */} - {/* Confirm Delete Dialog */} {isConfirmDialogOpen && taskToDelete && ( void; - onStartEdit: () => void; - onSave: () => void; - onCancel: () => void; - onToggleSubtaskCompletion: (subtask: Task) => Promise; + onSave: (subtasks: Task[]) => void; } const TaskSubtasksCard: React.FC = ({ task, subtasks, - isEditing, - editedSubtasks, onSubtasksChange, - onStartEdit, onSave, - onCancel, - onToggleSubtaskCompletion, }) => { - const { t } = useTranslation(); - return ( -
- {isEditing ? ( -
- -
-
- - -
-
-
- ) : subtasks.length > 0 ? ( -
- {subtasks.map((subtask: Task) => ( -
-
- - onToggleSubtaskCompletion(subtask) - } - /> - - {subtask.name} - -
-
- ))} -
- ) : ( -
-
- - - {t('task.noSubtasksClickToAdd', 'Add subtasks')} - -
-
- )} +
+
); }; diff --git a/frontend/components/Task/TaskForm/TaskSubtasksSection.tsx b/frontend/components/Task/TaskForm/TaskSubtasksSection.tsx index 4e7f561..ac379c8 100644 --- a/frontend/components/Task/TaskForm/TaskSubtasksSection.tsx +++ b/frontend/components/Task/TaskForm/TaskSubtasksSection.tsx @@ -10,6 +10,7 @@ interface TaskSubtasksSectionProps { subtasks: Task[]; onSubtasksChange: (subtasks: Task[]) => void; onSubtaskUpdate?: (subtask: Task) => Promise; + onSave?: (subtasks: Task[]) => void; } const TaskSubtasksSection: React.FC = ({ @@ -17,6 +18,7 @@ const TaskSubtasksSection: React.FC = ({ subtasks, onSubtasksChange, onSubtaskUpdate, + onSave, }) => { const [newSubtaskName, setNewSubtaskName] = useState(''); const [isLoading] = useState(false); @@ -28,7 +30,6 @@ const TaskSubtasksSection: React.FC = ({ const scrollToBottom = () => { setTimeout(() => { - // Find the modal's scrollable container const modalScrollContainer = document.querySelector( '.absolute.inset-0.overflow-y-auto' ); @@ -49,24 +50,24 @@ const TaskSubtasksSection: React.FC = ({ status: 'not_started', priority: 'low', today: false, - parent_task_id: parentTaskId, // Set the parent task ID immediately - // Mark as new for backend processing + parent_task_id: parentTaskId, isNew: true, - // Also keep for UI purposes _isNew: true, completed_at: null, } as Task; - onSubtasksChange([...subtasks, newSubtask]); + const updatedSubtasks = [...subtasks, newSubtask]; + onSubtasksChange(updatedSubtasks); setNewSubtaskName(''); - - // Only scroll when adding new subtask, not when toggling completion scrollToBottom(); + + onSave?.(updatedSubtasks); }; const handleDeleteSubtask = (index: number) => { const updatedSubtasks = subtasks.filter((_, i) => i !== index); onSubtasksChange(updatedSubtasks); + onSave?.(updatedSubtasks); }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -93,10 +94,8 @@ const TaskSubtasksSection: React.FC = ({ return { ...subtask, name: editingName.trim(), - // Backend flags isNew: isNew, isEdited: isEdited, - // UI flags _isNew: isNew, _isEdited: isEdited, }; @@ -107,6 +106,7 @@ const TaskSubtasksSection: React.FC = ({ onSubtasksChange(updatedSubtasks); setEditingIndex(null); setEditingName(''); + onSave?.(updatedSubtasks); }; const handleCancelEdit = () => { @@ -130,7 +130,6 @@ const TaskSubtasksSection: React.FC = ({ ...subtask, status: newStatus, completed_at: isDone ? null : new Date().toISOString(), - // Mark for backend update if it has an ID (existing subtask) _statusChanged: hasId, }; } @@ -141,7 +140,6 @@ const TaskSubtasksSection: React.FC = ({ return (
- {/* Existing Subtasks */} {isLoading ? (
{t('loading.subtasks', 'Loading subtasks...')} @@ -171,7 +169,6 @@ const TaskSubtasksSection: React.FC = ({ (subtask as any).isNew ) ) { - // Existing subtask - use API for immediate toggle, then update callback try { const updatedSubtask = await toggleTaskCompletion( @@ -187,7 +184,6 @@ const TaskSubtasksSection: React.FC = ({ ); } } else { - // New subtask or no callback - handle locally handleToggleNewSubtaskCompletion( index ); @@ -245,7 +241,6 @@ const TaskSubtasksSection: React.FC = ({ .isNew ) ) { - // Existing subtask - use API for immediate toggle, then update callback try { const updatedSubtask = await toggleTaskCompletion( @@ -261,7 +256,6 @@ const TaskSubtasksSection: React.FC = ({ ); } } else { - // New subtask or no callback - handle locally handleToggleNewSubtaskCompletion( index ); @@ -287,16 +281,6 @@ const TaskSubtasksSection: React.FC = ({ )} > {subtask.name} - {(subtask as any)._isNew && ( - - (new) - - )} - {(subtask as any)._isEdited && ( - - (edited) - - )}
)} - {/* Add New Subtask */}