import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { IGroupByOption, ILabelsChangeResponse, ITaskListColumn, ITaskListConfigV2, ITaskListGroup, ITaskListSortableColumn, } from '@/types/tasks/taskList.types'; import { tasksApiService } from '@/api/tasks/tasks.api.service'; import logger from '@/utils/errorLogger'; import { ITaskListMemberFilter } from '@/types/tasks/taskListFilters.types'; import { ITaskAssignee, ITaskFormViewModel } from '@/types/tasks/task.types'; import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { ITaskStatusViewModel } from '@/types/tasks/taskStatusGetResponse.types'; import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types'; import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types'; import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service'; import { ITaskLabel, ITaskLabelFilter } from '@/types/tasks/taskLabel.types'; import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response'; import { produce } from 'immer'; import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service'; import { SocketEvents } from '@/shared/socket-events'; import { ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule'; export enum IGroupBy { STATUS = 'status', PRIORITY = 'priority', PHASE = 'phase', MEMBERS = 'members', } export const GROUP_BY_STATUS_VALUE = IGroupBy.STATUS; export const GROUP_BY_PRIORITY_VALUE = IGroupBy.PRIORITY; export const GROUP_BY_PHASE_VALUE = IGroupBy.PHASE; export const GROUP_BY_OPTIONS: IGroupByOption[] = [ { label: 'Status', value: GROUP_BY_STATUS_VALUE }, { label: 'Priority', value: GROUP_BY_PRIORITY_VALUE }, { label: 'Phase', value: GROUP_BY_PHASE_VALUE }, ]; const LOCALSTORAGE_GROUP_KEY = 'worklenz.tasklist.group_by'; export const getCurrentGroup = (): IGroupByOption => { const key = localStorage.getItem(LOCALSTORAGE_GROUP_KEY); if (key) { const group = GROUP_BY_OPTIONS.find(option => option.value === key); if (group) return group; } setCurrentGroup(GROUP_BY_STATUS_VALUE); return GROUP_BY_OPTIONS[0]; }; export const setCurrentGroup = (groupBy: IGroupBy): void => { localStorage.setItem(LOCALSTORAGE_GROUP_KEY, groupBy); }; interface ITaskState { search: string | null; archived: boolean; groupBy: IGroupBy; isSubtasksInclude: boolean; fields: ITaskListSortableColumn[]; tasks: IProjectTask[]; taskGroups: ITaskListGroup[]; loadingColumns: boolean; columns: ITaskListColumn[]; loadingGroups: boolean; error: string | null; taskAssignees: ITaskListMemberFilter[]; loadingAssignees: boolean; statuses: ITaskStatusViewModel[]; loadingLabels: boolean; labels: ITaskLabelFilter[]; priorities: string[]; members: string[]; activeTimers: Record; convertToSubtaskDrawerOpen: boolean; customColumns: ITaskListColumn[]; customColumnValues: Record>; allTasks: IProjectTask[]; grouping: string; totalTasks: number; } const initialState: ITaskState = { search: null, archived: false, groupBy: getCurrentGroup().value as IGroupBy, isSubtasksInclude: false, fields: [], tasks: [], loadingColumns: false, columns: [], taskGroups: [], loadingGroups: false, error: null, taskAssignees: [], loadingAssignees: false, statuses: [], labels: [], loadingLabels: false, priorities: [], members: [], activeTimers: {}, convertToSubtaskDrawerOpen: false, customColumns: [], customColumnValues: {}, allTasks: [], grouping: '', totalTasks: 0, }; export const COLUMN_KEYS = { KEY: 'KEY', NAME: 'NAME', DESCRIPTION: 'DESCRIPTION', PROGRESS: 'PROGRESS', ASSIGNEES: 'ASSIGNEES', LABELS: 'LABELS', STATUS: 'STATUS', PRIORITY: 'PRIORITY', TIME_TRACKING: 'TIME_TRACKING', ESTIMATION: 'ESTIMATION', START_DATE: 'START_DATE', DUE_DATE: 'DUE_DATE', DUE_TIME: 'DUE_TIME', COMPLETED_DATE: 'COMPLETED_DATE', CREATED_DATE: 'CREATED_DATE', LAST_UPDATED: 'LAST_UPDATED', REPORTER: 'REPORTER', PHASE: 'PHASE', } as const; export const COLUMN_KEYS_LIST = Object.values(COLUMN_KEYS).map(key => ({ key, show: true, })); export const fetchTaskGroups = createAsyncThunk( 'tasks/fetchTaskGroups', async (projectId: string, { rejectWithValue, getState }) => { try { const state = getState() as { taskReducer: ITaskState }; const { taskReducer } = state; const selectedMembers = taskReducer.taskAssignees .filter(member => member.selected) .map(member => member.id) .join(' '); const selectedLabels = taskReducer.labels .filter(label => label.selected) .map(label => label.id) .join(' '); const config: ITaskListConfigV2 = { id: projectId, archived: taskReducer.archived, group: taskReducer.groupBy, field: taskReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','), order: '', search: taskReducer.search || '', statuses: '', members: selectedMembers, projects: '', isSubtasksInclude: false, labels: selectedLabels, priorities: taskReducer.priorities.join(' '), }; const response = await tasksApiService.getTaskListV3(config); return response.body; } catch (error) { logger.error('Fetch Task Groups', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch task groups'); } } ); export const fetchSubTasks = createAsyncThunk( 'tasks/fetchSubTasks', async ( { taskId, projectId }: { taskId: string; projectId: string }, { rejectWithValue, getState, dispatch } ) => { const state = getState() as { taskReducer: ITaskState }; const { taskReducer } = state; // Check if the task is already expanded const task = taskReducer.taskGroups.flatMap(group => group.tasks).find(t => t.id === taskId); if (task?.show_sub_tasks) { // If already expanded, just return without fetching return []; } // Request subtask progress data when expanding the task // This will trigger the socket to emit TASK_PROGRESS_UPDATED events for all subtasks try { // Get access to the socket from the state const socket = (getState() as any).socketReducer?.socket; if (socket?.connected) { // Request subtask count and progress information socket.emit(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), taskId); } } catch (error) { console.error('Error requesting subtask progress:', error); // Non-critical error, continue with fetching subtasks } const selectedMembers = taskReducer.taskAssignees .filter(member => member.selected) .map(member => member.id) .join(' '); const selectedLabels = taskReducer.labels .filter(label => label.selected) .map(label => label.id) .join(' '); const config: ITaskListConfigV2 = { id: projectId, archived: taskReducer.archived, group: taskReducer.groupBy, field: taskReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','), order: '', search: taskReducer.search || '', statuses: '', members: selectedMembers, projects: '', isSubtasksInclude: false, labels: selectedLabels, priorities: taskReducer.priorities.join(' '), parent_task: taskId, }; try { const response = await tasksApiService.getTaskListV3(config); // Only expand if we actually fetched subtasks if (response.body && response.body.groups && response.body.groups.length > 0) { dispatch(toggleTaskRowExpansion(taskId)); } return response.body; } catch (error) { logger.error('Fetch Sub Tasks', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch sub tasks'); } } ); export const fetchTaskListColumns = createAsyncThunk( 'tasks/fetTaskListColumns', async (projectId: string, { dispatch }) => { const [standardColumns, customColumns] = await Promise.all([ tasksApiService.fetchTaskListColumns(projectId), dispatch(fetchCustomColumns(projectId)), ]); return { standard: standardColumns.body, custom: customColumns.payload, }; } ); export const fetchTaskAssignees = createAsyncThunk( 'tasks/fetchTaskAssignees', async (projectId: string, { rejectWithValue }) => { try { const response = await tasksApiService.fetchTaskAssignees(projectId); return response.body; } catch (error) { logger.error('Fetch Task Assignees', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch task assignees'); } } ); export const fetchLabelsByProject = createAsyncThunk( 'taskLabel/fetchLabelsByProject', async (projectId: string, { rejectWithValue }) => { try { const response = await labelsApiService.getPriorityByProject(projectId); return response.body; } catch (error) { logger.error('Fetch Labels By Project', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch project labels'); } } ); export const fetchTask = createAsyncThunk( 'tasks/fetchTask', async ({ taskId, projectId }: { taskId: string; projectId: string }, { rejectWithValue }) => { try { const response = await tasksApiService.getFormViewModel(taskId, projectId); return response.body; } catch (error) { logger.error('Fetch Task', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch task'); } } ); export const updateColumnVisibility = createAsyncThunk( 'tasks/updateColumnVisibility', async ( { projectId, item }: { projectId: string; item: ITaskListColumn }, { rejectWithValue } ) => { try { const response = await tasksApiService.toggleColumnVisibility(projectId, item); return response.body; } catch (error) { logger.error('Update Column Visibility', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to update column visibility'); } } ); const getGroupIdByGroupedColumn = (task: IProjectTask): string | null => { const groupBy = getCurrentGroup().value; switch (groupBy) { case GROUP_BY_STATUS_VALUE: return task.status as string; case GROUP_BY_PRIORITY_VALUE: return task.priority as string; case GROUP_BY_PHASE_VALUE: return task.phase_id as string; default: return null; } }; const deleteTaskFromGroup = ( taskGroups: ITaskListGroup[], task: IProjectTask, groupId: string, index: number | null = null ): void => { const group = taskGroups.find(g => g.id === groupId); if (!group || !task.id) return; if (task.is_sub_task) { const parentTask = group.tasks.find(t => t.id === task.parent_task_id); if (parentTask) { const subTaskIndex = parentTask.sub_tasks?.findIndex(t => t.id === task.id); if (typeof subTaskIndex !== 'undefined' && subTaskIndex !== -1) { parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0); parentTask.sub_tasks?.splice(subTaskIndex, 1); } } } else { const taskIndex = index ?? group.tasks.findIndex(t => t.id === task.id); if (taskIndex !== -1) { group.tasks.splice(taskIndex, 1); } } }; const addTaskToGroup = ( taskGroups: ITaskListGroup[], task: IProjectTask, groupId: string, insert = false ): void => { const group = taskGroups.find(g => g.id === groupId); if (!group || !task.id) return; if (task.parent_task_id) { const parentTask = group.tasks.find(t => t.id === task.parent_task_id); if (parentTask) { parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1; if (!parentTask.sub_tasks) parentTask.sub_tasks = []; parentTask.sub_tasks.push({ ...task }); } } else { insert ? group.tasks.push(task) : group.tasks.unshift(task); } }; const updateTaskGroup = (taskGroups: ITaskListGroup[], task: IProjectTask, insert = true): void => { if (!task.id) return; const groupId = getGroupIdByGroupedColumn(task); if (groupId) { deleteTaskFromGroup(taskGroups, task, groupId); addTaskToGroup(taskGroups, { ...task }, groupId, insert); } }; const findTaskInGroups = ( taskGroups: ITaskListGroup[], taskId: string ): { task: IProjectTask; groupId: string; index: number } | null => { for (const group of taskGroups) { // Check main tasks const taskIndex = group.tasks.findIndex(t => t.id === taskId); if (taskIndex !== -1) { return { task: group.tasks[taskIndex], groupId: group.id, index: taskIndex }; } // Check subtasks for (const task of group.tasks) { if (task.sub_tasks) { const subTaskIndex = task.sub_tasks.findIndex(subtask => subtask.id === taskId); if (subTaskIndex !== -1) { return { task: task.sub_tasks[subTaskIndex], groupId: group.id, index: subTaskIndex }; } } } } return null; }; export const fetchCustomColumns = createAsyncThunk( 'tasks/fetchCustomColumns', async (projectId: string, { rejectWithValue }) => { try { const response = await tasksCustomColumnsService.getCustomColumns(projectId); return response.body; } catch (error) { logger.error('Fetch Custom Columns', error); if (error instanceof Error) { return rejectWithValue(error.message); } return rejectWithValue('Failed to fetch custom columns'); } } ); const taskSlice = createSlice({ name: 'taskReducer', initialState, reducers: { toggleArchived: state => { state.archived = !state.archived; }, setGroup: (state, action: PayloadAction) => { state.groupBy = action.payload; setCurrentGroup(action.payload); }, setLabels: (state, action: PayloadAction) => { state.labels = action.payload; }, setMembers: (state, action: PayloadAction) => { state.taskAssignees = action.payload; }, setPriorities: (state, action: PayloadAction) => { state.priorities = action.payload; }, setStatuses: (state, action: PayloadAction) => { state.statuses = action.payload; }, setFields: (state, action: PayloadAction) => { state.fields = action.payload; }, setSearch: (state, action: PayloadAction) => { state.search = action.payload; }, setConvertToSubtaskDrawerOpen: (state, action: PayloadAction) => { state.convertToSubtaskDrawerOpen = action.payload; }, addTask: ( state, action: PayloadAction<{ task: IProjectTask; groupId: string; insert?: boolean; }> ) => { const { task, groupId, insert = false } = action.payload; const group = state.taskGroups.find(g => g.id === groupId); if (!group || !task.id) return; // Handle subtask addition if (task.parent_task_id) { const parentTask = group.tasks.find(t => t.id === task.parent_task_id); // if (parentTask) { // if (!parentTask.sub_tasks) parentTask.sub_tasks = []; // parentTask.sub_tasks.push({ ...task }); // parentTask.sub_tasks_count = parentTask.sub_tasks.length; // Update the sub_tasks_count based on the actual length // Ensure sub-tasks are visible when adding a new one // parentTask.show_sub_tasks = true; // } } else { // Handle main task addition if (insert) { group.tasks.push(task); } else { group.tasks.unshift(task); } } }, deleteTask: ( state, action: PayloadAction<{ taskId: string; index?: number | null; }> ) => { const { taskId, index } = action.payload; for (const group of state.taskGroups) { // Try to find task in subtasks first let found = false; for (const parentTask of group.tasks) { if (parentTask.sub_tasks) { const subTaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId); if (subTaskIndex !== -1) { parentTask.sub_tasks.splice(subTaskIndex, 1); parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0); found = true; break; } } } if (found) break; // If not found in subtasks, try main tasks const taskIndex = index ?? group.tasks.findIndex(t => t.id === taskId); if (taskIndex !== -1) { group.tasks.splice(taskIndex, 1); break; } } }, updateTaskName: ( state, action: PayloadAction<{ id: string; parent_task: string; name: string }> ) => { const { id, name } = action.payload; for (const group of state.taskGroups) { // Check main tasks const task = group.tasks.find(task => task.id === id); if (task) { task.name = name; break; } // Check subtasks for (const task of group.tasks) { if (task.sub_tasks) { const subTask = task.sub_tasks.find(subtask => subtask.id === id); if (subTask) { subTask.name = name; break; } } } } }, updateTaskProgress: ( state, action: PayloadAction<{ taskId: string; progress: number; totalTasksCount: number; completedCount: number; }> ) => { const { taskId, progress, totalTasksCount, completedCount } = action.payload; // Helper function to find and update a task at any nesting level const findAndUpdateTask = (tasks: IProjectTask[]) => { for (const task of tasks) { if (task.id === taskId) { task.complete_ratio = progress; task.progress_value = progress; task.total_tasks_count = totalTasksCount; task.completed_count = completedCount; return true; } // Check subtasks if they exist if (task.sub_tasks && task.sub_tasks.length > 0) { const found = findAndUpdateTask(task.sub_tasks); if (found) return true; } } return false; }; // Try to find and update the task in any task group for (const group of state.taskGroups) { const found = findAndUpdateTask(group.tasks); if (found) break; } }, updateTaskAssignees: ( state, action: PayloadAction<{ groupId: string; taskId: string; assignees: ITeamMemberViewModel[]; }> ) => { const { groupId, taskId, assignees } = action.payload; const group = state.taskGroups.find(group => group.id === groupId); if (!group) return; // Try to find the task in main tasks first const mainTask = group.tasks.find(task => task.id === taskId); if (mainTask) { mainTask.assignees = assignees as ITaskAssignee[]; return; } // If not found in main tasks, look for it in subtasks for (const parentTask of group.tasks) { if (parentTask.sub_tasks) { const subTask = parentTask.sub_tasks.find(st => st.id === taskId); if (subTask) { subTask.assignees = assignees as ITaskAssignee[]; return; } } } }, updateTaskLabel: (state, action: PayloadAction) => { const label = action.payload; for (const group of state.taskGroups) { // Find the task or its subtask const task = group.tasks.find(task => task.id === label.id) || group.tasks .flatMap(task => task.sub_tasks || []) .find(subtask => subtask.id === label.id); if (task) { task.labels = label.labels || []; task.all_labels = label.all_labels || []; break; } } }, updateTaskStatus: (state, action: PayloadAction) => { const { id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload; // Find the task in any group const taskInfo = findTaskInGroups(state.taskGroups, id); if (!taskInfo || !status_id) return; const { task, groupId } = taskInfo; // Update the task properties task.status_color = color_code; task.status_color_dark = color_code_dark; task.complete_ratio = +complete_ratio; task.status = status_id; task.status_category = statusCategory; // If grouped by status and not a subtask, move the task to the new status group if (state.groupBy === GROUP_BY_STATUS_VALUE && !task.is_sub_task && groupId !== status_id) { // Remove from current group deleteTaskFromGroup(state.taskGroups, task, groupId); // Add to new status group addTaskToGroup(state.taskGroups, task, status_id, false); } }, updateTaskEndDate: ( state, action: PayloadAction<{ task: IProjectTask; }> ) => { const { task } = action.payload; for (const group of state.taskGroups) { const existingTask = group.tasks.find(t => t.id === task.id) || group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id); if (existingTask) { existingTask.end_date = task.end_date; break; } } }, updateTaskStartDate: ( state, action: PayloadAction<{ task: IProjectTask; }> ) => { const { task } = action.payload; for (const group of state.taskGroups) { const existingTask = group.tasks.find(t => t.id === task.id) || group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id); if (existingTask) { existingTask.start_date = task.start_date; break; } } }, updateTaskEstimation: ( state, action: PayloadAction<{ task: IProjectTask; }> ) => { const { task } = action.payload; for (const group of state.taskGroups) { const existingTask = group.tasks.find(t => t.id === task.id) || group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id); if (existingTask) { existingTask.total_time_string = task.total_time_string; break; } } }, updateTaskPhase: (state, action: PayloadAction) => { const { id: phase_id, task_id, color_code } = action.payload; if (!task_id || !phase_id) return; const taskInfo = findTaskInGroups(state.taskGroups, task_id); if (!taskInfo) return; const { task, groupId } = taskInfo; task.phase_id = phase_id; task.phase_color = color_code; if (state.groupBy === GROUP_BY_PHASE_VALUE && !task.is_sub_task && groupId !== phase_id) { deleteTaskFromGroup(state.taskGroups, task, groupId); addTaskToGroup(state.taskGroups, task, phase_id, false); } }, updateTaskGroupColor: ( state, action: PayloadAction<{ groupId: string; colorCode: string }> ) => { const { colorCode, groupId } = action.payload; if (groupId) { const group = state.taskGroups.find(g => g.id === groupId); if (group) { group.color_code = colorCode; } } }, updateTaskStatusColor: (state, action: PayloadAction<{ taskId: string; color: string }>) => { const { taskId, color } = action.payload; const task = state.tasks.find(t => t.id === taskId) || state.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === taskId); if (task) { task.status_color = color; } }, toggleColumnVisibility: (state, action: PayloadAction) => { const column = state.columns.find(col => col.key === action.payload); if (column) { column.pinned = !column.pinned; } }, updateTaskTimeTracking: ( state, action: PayloadAction<{ taskId: string; timeTracking: number | null; }> ) => { const { taskId, timeTracking } = action.payload; state.activeTimers[taskId] = timeTracking; }, updateTaskPriority: (state, action: PayloadAction) => { const { id, priority_id, color_code, color_code_dark } = action.payload; // Find the task in any group const taskInfo = findTaskInGroups(state.taskGroups, id); if (!taskInfo || !priority_id) return; const { task, groupId } = taskInfo; // Update the task properties task.priority = priority_id; task.priority_color = color_code; task.priority_color_dark = color_code_dark; // If grouped by priority and not a subtask, move the task to the new priority group if ( state.groupBy === GROUP_BY_PRIORITY_VALUE && !task.is_sub_task && groupId !== priority_id ) { // Remove from current group deleteTaskFromGroup(state.taskGroups, task, groupId); // Add to new priority group addTaskToGroup(state.taskGroups, task, priority_id, false); } }, updateTaskDescription: ( state, action: PayloadAction<{ id: string; parent_task: string; description: string; }> ) => { const { id: taskId, description, parent_task } = action.payload; for (const group of state.taskGroups) { const existingTask = group.tasks.find(t => t.id === taskId) || group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === taskId); if (existingTask) { existingTask.description = description; break; } } }, toggleTaskRowExpansion: (state, action: PayloadAction) => { const taskId = action.payload; for (const group of state.taskGroups) { const task = group.tasks.find(t => t.id === taskId); if (task) { task.show_sub_tasks = !task.show_sub_tasks; break; } } }, resetTaskListData: state => { return { ...initialState, groupBy: state.groupBy, // Preserve the current grouping }; }, reorderTasks: ( state, action: PayloadAction<{ activeGroupId: string; overGroupId: string; fromIndex: number; toIndex: number; task: IProjectTask; updatedSourceTasks: IProjectTask[]; updatedTargetTasks: IProjectTask[]; }> ) => { return produce(state, draft => { const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload; const sourceGroup = draft.taskGroups.find(g => g.id === activeGroupId); const targetGroup = draft.taskGroups.find(g => g.id === overGroupId); if (!sourceGroup || !targetGroup) return; // Simply replace the arrays with the updated ones sourceGroup.tasks = updatedSourceTasks; // Only update target if it's different from source if (activeGroupId !== overGroupId) { targetGroup.tasks = updatedTargetTasks; } }); }, addCustomColumn: (state, action: PayloadAction) => { state.customColumns.push(action.payload); // Also add to columns array to maintain visibility state.columns.push({ ...action.payload, pinned: true, // New columns are visible by default }); }, updateCustomColumn: ( state, action: PayloadAction<{ key: string; column: ITaskListColumn }> ) => { const { key, column } = action.payload; const index = state.customColumns.findIndex(col => col.key === key); if (index !== -1) { state.customColumns[index] = column; // Update in columns array as well const colIndex = state.columns.findIndex(col => col.key === key); if (colIndex !== -1) { state.columns[colIndex] = { ...column, pinned: state.columns[colIndex].pinned }; } } }, deleteCustomColumn: (state, action: PayloadAction) => { const key = action.payload; state.customColumns = state.customColumns.filter(col => col.key !== key); // Remove from columns array as well state.columns = state.columns.filter(col => col.key !== key); }, updateSubTasks: (state, action: PayloadAction) => { const { parent_task_id } = action.payload; for (const group of state.taskGroups) { const parentTask = group.tasks.find(t => t.id === parent_task_id); if (parentTask) { if (!parentTask.sub_tasks) { parentTask.sub_tasks = []; } parentTask.sub_tasks.push({ ...action.payload }); // Always update sub_tasks_count based on actual subtasks array length parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1; break; } } }, updateCustomColumnValue: ( state, action: PayloadAction<{ taskId: string; columnKey: string; value: string; }> ) => { const { taskId, columnKey, value } = action.payload; // Update in task groups for (const group of state.taskGroups) { // Check in main tasks const taskIndex = group.tasks.findIndex(t => t.id === taskId); if (taskIndex !== -1) { if (!group.tasks[taskIndex].custom_column_values) { group.tasks[taskIndex].custom_column_values = {}; } group.tasks[taskIndex].custom_column_values[columnKey] = value; break; } // Check in subtasks for (const parentTask of group.tasks) { if (parentTask.sub_tasks) { const subtaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId); if (subtaskIndex !== -1) { if (!parentTask.sub_tasks[subtaskIndex].custom_column_values) { parentTask.sub_tasks[subtaskIndex].custom_column_values = {}; } parentTask.sub_tasks[subtaskIndex].custom_column_values[columnKey] = value; break; } } } } // Also update in the customColumnValues state if needed if (!state.customColumnValues[taskId]) { state.customColumnValues[taskId] = {}; } state.customColumnValues[taskId][columnKey] = value; }, updateCustomColumnPinned: ( state, action: PayloadAction<{ columnId: string; isVisible: boolean }> ) => { const { columnId, isVisible } = action.payload; const customColumn = state.customColumns.find(col => col.id === columnId); const column = state.columns.find(col => col.id === columnId); if (customColumn) { customColumn.pinned = isVisible; } if (column) { column.pinned = isVisible; } }, updateRecurringChange: (state, action: PayloadAction) => { const { id, schedule_type, task_id } = action.payload; const taskInfo = findTaskInGroups(state.taskGroups, task_id as string); if (!taskInfo) return; const { task } = taskInfo; task.schedule_id = id; }, }, extraReducers: builder => { builder .addCase(fetchTaskGroups.pending, state => { state.loadingGroups = true; state.error = null; }) .addCase(fetchTaskGroups.fulfilled, (state, action) => { state.loadingGroups = false; state.taskGroups = action.payload && action.payload.groups ? action.payload.groups : []; state.allTasks = action.payload && action.payload.allTasks ? action.payload.allTasks : []; state.grouping = action.payload && action.payload.grouping ? action.payload.grouping : ''; state.totalTasks = action.payload && action.payload.totalTasks ? action.payload.totalTasks : 0; }) .addCase(fetchTaskGroups.rejected, (state, action) => { state.loadingGroups = false; state.error = action.error.message || 'Failed to fetch task groups'; }) .addCase(fetchSubTasks.pending, state => { state.error = null; }) .addCase(fetchSubTasks.fulfilled, (state, action) => { if (action.payload && action.payload.groups && action.payload.groups.length > 0) { // Assuming subtasks are in the first group for this context const subtasks = action.payload.groups[0].tasks; const taskId = subtasks.length > 0 ? subtasks[0].parent_task_id : null; if (taskId) { for (const group of state.taskGroups) { const task = group.tasks.find(t => t.id === taskId); if (task) { task.sub_tasks = subtasks; task.show_sub_tasks = true; break; } } } } }) .addCase(fetchSubTasks.rejected, (state, action) => { state.error = action.error.message || 'Failed to fetch sub tasks'; }) .addCase(fetchTaskAssignees.pending, state => { state.loadingAssignees = true; state.error = null; }) .addCase(fetchTaskAssignees.fulfilled, (state, action) => { state.loadingAssignees = false; state.taskAssignees = action.payload; }) .addCase(fetchTaskAssignees.rejected, (state, action) => { state.loadingAssignees = false; state.error = action.error.message || 'Failed to fetch task assignees'; }) .addCase(fetchTaskListColumns.pending, state => { state.loadingColumns = true; state.error = null; }) .addCase(fetchTaskListColumns.fulfilled, (state, action) => { state.loadingColumns = false; // Process standard columns const standardColumns = action.payload.standard; standardColumns.splice(1, 0, { key: 'TASK', name: 'Task', index: 1, pinned: true, }); // Process custom columns const customColumns = (action.payload as { custom: any[] }).custom.map((col: any) => ({ ...col, isCustom: true, })); // Merge columns state.columns = [...standardColumns, ...customColumns]; state.customColumns = customColumns; }) .addCase(fetchTaskListColumns.rejected, (state, action) => { state.loadingColumns = false; state.error = action.error.message || 'Failed to fetch task list columns'; }) // Fetch Labels By Project .addCase(fetchLabelsByProject.pending, state => { state.loadingLabels = true; state.error = null; }) .addCase(fetchLabelsByProject.fulfilled, (state, action: PayloadAction) => { const newLabels = action.payload.map(label => ({ ...label, selected: false })); state.labels = newLabels; state.loadingLabels = false; }) .addCase(fetchLabelsByProject.rejected, (state, action) => { state.loadingLabels = false; state.error = action.payload as string; }) .addCase(updateColumnVisibility.fulfilled, (state, action) => { const column = state.columns.find(col => col.key === action.payload.key); if (column) { column.pinned = action.payload.pinned; } }) .addCase(updateColumnVisibility.rejected, (state, action) => { state.error = action.payload as string; }) .addCase(updateColumnVisibility.pending, state => { state.loadingColumns = true; state.error = null; }) .addCase(fetchCustomColumns.pending, state => { state.loadingColumns = true; state.error = null; }) .addCase(fetchCustomColumns.fulfilled, (state, action) => { state.loadingColumns = false; state.customColumns = action.payload; // Add custom columns to the columns array const customColumnsForVisibility = action.payload; state.columns = [...state.columns, ...customColumnsForVisibility]; }) .addCase(fetchCustomColumns.rejected, (state, action) => { state.loadingColumns = false; state.error = action.error.message || 'Failed to fetch custom columns'; }); }, }); export const { setGroup, addTask, deleteTask, updateTaskName, updateTaskProgress, updateTaskAssignees, updateTaskLabel, toggleArchived, setMembers, setLabels, setPriorities, setStatuses, setFields, setSearch, toggleColumnVisibility, updateTaskStatus, updateTaskPhase, updateTaskPriority, updateTaskEndDate, updateTaskStartDate, updateTaskEstimation, updateTaskTimeTracking, toggleTaskRowExpansion, resetTaskListData, updateTaskStatusColor, updateTaskGroupColor, setConvertToSubtaskDrawerOpen, reorderTasks, updateTaskDescription, addCustomColumn, updateCustomColumn, deleteCustomColumn, updateSubTasks, updateCustomColumnValue, updateCustomColumnPinned, updateRecurringChange, } = taskSlice.actions; export default taskSlice.reducer;