diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx index a23d106e..b597a6c1 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx @@ -547,17 +547,17 @@ const TaskListV2: React.FC = () => { totalSelected={selectedTaskIds.length} projectId={urlProjectId} onClearSelection={bulkActions.handleClearSelection} - onBulkStatusChange={bulkActions.handleBulkStatusChange} - onBulkPriorityChange={bulkActions.handleBulkPriorityChange} - onBulkPhaseChange={bulkActions.handleBulkPhaseChange} - onBulkAssignToMe={bulkActions.handleBulkAssignToMe} - onBulkAssignMembers={bulkActions.handleBulkAssignMembers} - onBulkAddLabels={bulkActions.handleBulkAddLabels} - onBulkArchive={bulkActions.handleBulkArchive} - onBulkDelete={bulkActions.handleBulkDelete} - onBulkDuplicate={bulkActions.handleBulkDuplicate} - onBulkExport={bulkActions.handleBulkExport} - onBulkSetDueDate={bulkActions.handleBulkSetDueDate} + onBulkStatusChange={(statusId) => bulkActions.handleBulkStatusChange(statusId, selectedTaskIds)} + onBulkPriorityChange={(priorityId) => bulkActions.handleBulkPriorityChange(priorityId, selectedTaskIds)} + onBulkPhaseChange={(phaseId) => bulkActions.handleBulkPhaseChange(phaseId, selectedTaskIds)} + onBulkAssignToMe={() => bulkActions.handleBulkAssignToMe(selectedTaskIds)} + onBulkAssignMembers={(memberIds) => bulkActions.handleBulkAssignMembers(memberIds, selectedTaskIds)} + onBulkAddLabels={(labelIds) => bulkActions.handleBulkAddLabels(labelIds, selectedTaskIds)} + onBulkArchive={() => bulkActions.handleBulkArchive(selectedTaskIds)} + onBulkDelete={() => bulkActions.handleBulkDelete(selectedTaskIds)} + onBulkDuplicate={() => bulkActions.handleBulkDuplicate(selectedTaskIds)} + onBulkExport={() => bulkActions.handleBulkExport(selectedTaskIds)} + onBulkSetDueDate={(date) => bulkActions.handleBulkSetDueDate(date, selectedTaskIds)} /> )} diff --git a/worklenz-frontend/src/components/task-list-v2/hooks/useBulkActions.ts b/worklenz-frontend/src/components/task-list-v2/hooks/useBulkActions.ts index 8fde5d28..fa3377ea 100644 --- a/worklenz-frontend/src/components/task-list-v2/hooks/useBulkActions.ts +++ b/worklenz-frontend/src/components/task-list-v2/hooks/useBulkActions.ts @@ -1,68 +1,342 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { useAppSelector } from '@/hooks/useAppSelector'; import { clearSelection } from '@/features/task-management/selection.slice'; +import { fetchTasksV3 } from '@/features/task-management/task-management.slice'; +import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; +import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service'; +import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice'; +import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; +import alertService from '@/services/alerts/alertService'; +import logger from '@/utils/errorLogger'; +import { + evt_project_task_list_bulk_archive, + evt_project_task_list_bulk_assign_me, + evt_project_task_list_bulk_assign_members, + evt_project_task_list_bulk_change_phase, + evt_project_task_list_bulk_change_priority, + evt_project_task_list_bulk_change_status, + evt_project_task_list_bulk_delete, + evt_project_task_list_bulk_update_labels, +} from '@/shared/worklenz-analytics-events'; +import { + IBulkTasksLabelsRequest, + IBulkTasksPhaseChangeRequest, + IBulkTasksPriorityChangeRequest, + IBulkTasksStatusChangeRequest, +} from '@/types/tasks/bulk-action-bar.types'; +import { ITaskLabel } from '@/types/tasks/taskLabel.types'; +import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; +import { ITaskAssignee } from '@/types/tasks/task.types'; export const useBulkActions = () => { const dispatch = useAppDispatch(); + const { projectId } = useParams(); + const { trackMixpanelEvent } = useMixpanelTracking(); + const archived = useAppSelector(state => state.taskReducer.archived); + + // Loading states for individual actions + const [loadingStates, setLoadingStates] = useState({ + status: false, + priority: false, + phase: false, + assignToMe: false, + assignMembers: false, + labels: false, + archive: false, + delete: false, + duplicate: false, + export: false, + dueDate: false, + }); + + // Helper function to update loading state + const updateLoadingState = useCallback((action: keyof typeof loadingStates, loading: boolean) => { + setLoadingStates(prev => ({ ...prev, [action]: loading })); + }, []); + + // Helper function to refetch tasks after bulk action + const refetchTasks = useCallback(() => { + if (projectId) { + dispatch(fetchTasksV3(projectId)); + } + }, [dispatch, projectId]); const handleClearSelection = useCallback(() => { dispatch(clearSelection()); }, [dispatch]); - const handleBulkStatusChange = useCallback(async (statusId: string) => { - // TODO: Implement bulk status change - console.log('Bulk status change:', statusId); - }, []); + const handleBulkStatusChange = useCallback(async (statusId: string, selectedTaskIds: string[]) => { + if (!statusId || !projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('status', true); - const handleBulkPriorityChange = useCallback(async (priorityId: string) => { - // TODO: Implement bulk priority change - console.log('Bulk priority change:', priorityId); - }, []); + // Check task dependencies before proceeding + for (const taskId of selectedTaskIds) { + const canContinue = await checkTaskDependencyStatus(taskId, statusId); + if (!canContinue) { + if (selectedTaskIds.length > 1) { + alertService.warning( + 'Incomplete Dependencies!', + 'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.' + ); + } else { + alertService.error( + 'Task is not completed', + 'Please complete the task dependencies before proceeding' + ); + } + return; + } + } - const handleBulkPhaseChange = useCallback(async (phaseId: string) => { - // TODO: Implement bulk phase change - console.log('Bulk phase change:', phaseId); - }, []); + const body: IBulkTasksStatusChangeRequest = { + tasks: selectedTaskIds, + status_id: statusId, + }; - const handleBulkAssignToMe = useCallback(async () => { - // TODO: Implement bulk assign to me - console.log('Bulk assign to me'); - }, []); + const res = await taskListBulkActionsApiService.changeStatus(body, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_change_status); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error changing status:', error); + } finally { + updateLoadingState('status', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); - const handleBulkAssignMembers = useCallback(async (memberIds: string[]) => { - // TODO: Implement bulk assign members - console.log('Bulk assign members:', memberIds); - }, []); + const handleBulkPriorityChange = useCallback(async (priorityId: string, selectedTaskIds: string[]) => { + if (!priorityId || !projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('priority', true); + + const body: IBulkTasksPriorityChangeRequest = { + tasks: selectedTaskIds, + priority_id: priorityId, + }; - const handleBulkAddLabels = useCallback(async (labelIds: string[]) => { - // TODO: Implement bulk add labels - console.log('Bulk add labels:', labelIds); - }, []); + const res = await taskListBulkActionsApiService.changePriority(body, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_change_priority); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error changing priority:', error); + } finally { + updateLoadingState('priority', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); - const handleBulkArchive = useCallback(async () => { - // TODO: Implement bulk archive - console.log('Bulk archive'); - }, []); + const handleBulkPhaseChange = useCallback(async (phaseId: string, selectedTaskIds: string[]) => { + if (!phaseId || !projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('phase', true); + + const body: IBulkTasksPhaseChangeRequest = { + tasks: selectedTaskIds, + phase_id: phaseId, + }; - const handleBulkDelete = useCallback(async () => { - // TODO: Implement bulk delete - console.log('Bulk delete'); - }, []); + const res = await taskListBulkActionsApiService.changePhase(body, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_change_phase); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error changing phase:', error); + } finally { + updateLoadingState('phase', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); - const handleBulkDuplicate = useCallback(async () => { - // TODO: Implement bulk duplicate - console.log('Bulk duplicate'); - }, []); + const handleBulkAssignToMe = useCallback(async (selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('assignToMe', true); + + const body = { + tasks: selectedTaskIds, + project_id: projectId, + }; - const handleBulkExport = useCallback(async () => { - // TODO: Implement bulk export - console.log('Bulk export'); - }, []); + const res = await taskListBulkActionsApiService.assignToMe(body); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_assign_me); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error assigning to me:', error); + } finally { + updateLoadingState('assignToMe', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); - const handleBulkSetDueDate = useCallback(async (date: string) => { - // TODO: Implement bulk set due date - console.log('Bulk set due date:', date); - }, []); + const handleBulkAssignMembers = useCallback(async (memberIds: string[], selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('assignMembers', true); + + // Convert memberIds to member objects - this would need to be handled by the component + // For now, we'll just pass the IDs and let the API handle it + const body = { + tasks: selectedTaskIds, + project_id: projectId, + members: memberIds.map(id => ({ + id: id, + name: '', + team_member_id: id, + project_member_id: id, + })) as ITaskAssignee[], + }; + + const res = await taskListBulkActionsApiService.assignTasks(body); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_assign_members); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error assigning tasks:', error); + } finally { + updateLoadingState('assignMembers', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); + + const handleBulkAddLabels = useCallback(async (labelIds: string[], selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('labels', true); + + // Convert labelIds to label objects - this would need to be handled by the component + // For now, we'll just pass the IDs and let the API handle it + const body: IBulkTasksLabelsRequest = { + tasks: selectedTaskIds, + labels: labelIds.map(id => ({ id, name: '', color: '' })) as ITaskLabel[], + text: null, + }; + + const res = await taskListBulkActionsApiService.assignLabels(body, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_update_labels); + dispatch(clearSelection()); + dispatch(fetchLabels()); // Refetch labels in case new ones were created + refetchTasks(); + } + } catch (error) { + logger.error('Error updating labels:', error); + } finally { + updateLoadingState('labels', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); + + const handleBulkArchive = useCallback(async (selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('archive', true); + + const body = { + tasks: selectedTaskIds, + project_id: projectId, + }; + + const res = await taskListBulkActionsApiService.archiveTasks(body, archived); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_archive); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error archiving tasks:', error); + } finally { + updateLoadingState('archive', false); + } + }, [projectId, archived, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); + + const handleBulkDelete = useCallback(async (selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('delete', true); + + const body = { + tasks: selectedTaskIds, + project_id: projectId, + }; + + const res = await taskListBulkActionsApiService.deleteTasks(body, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_bulk_delete); + dispatch(clearSelection()); + refetchTasks(); + } + } catch (error) { + logger.error('Error deleting tasks:', error); + } finally { + updateLoadingState('delete', false); + } + }, [projectId, trackMixpanelEvent, dispatch, refetchTasks, updateLoadingState]); + + const handleBulkDuplicate = useCallback(async (selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('duplicate', true); + // TODO: Implement bulk duplicate API call when available + console.log('Bulk duplicate:', selectedTaskIds); + // For now, just clear selection and refetch + dispatch(clearSelection()); + refetchTasks(); + } catch (error) { + logger.error('Error duplicating tasks:', error); + } finally { + updateLoadingState('duplicate', false); + } + }, [projectId, dispatch, refetchTasks, updateLoadingState]); + + const handleBulkExport = useCallback(async (selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('export', true); + // TODO: Implement bulk export API call when available + console.log('Bulk export:', selectedTaskIds); + } catch (error) { + logger.error('Error exporting tasks:', error); + } finally { + updateLoadingState('export', false); + } + }, [projectId, updateLoadingState]); + + const handleBulkSetDueDate = useCallback(async (date: string, selectedTaskIds: string[]) => { + if (!projectId || !selectedTaskIds.length) return; + + try { + updateLoadingState('dueDate', true); + // TODO: Implement bulk set due date API call when available + console.log('Bulk set due date:', date, selectedTaskIds); + // For now, just clear selection and refetch + dispatch(clearSelection()); + refetchTasks(); + } catch (error) { + logger.error('Error setting due date:', error); + } finally { + updateLoadingState('dueDate', false); + } + }, [projectId, dispatch, refetchTasks, updateLoadingState]); return { handleClearSelection, @@ -77,5 +351,6 @@ export const useBulkActions = () => { handleBulkDuplicate, handleBulkExport, handleBulkSetDueDate, + loadingStates, }; }; \ No newline at end of file