Files
worklenz/worklenz-frontend/src/features/tasks/tasks.slice.ts
2025-07-03 01:31:05 +05:30

1201 lines
37 KiB
TypeScript

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<string, number | null>;
convertToSubtaskDrawerOpen: boolean;
customColumns: ITaskListColumn[];
customColumnValues: Record<string, Record<string, any>>;
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<IGroupBy>) => {
state.groupBy = action.payload;
setCurrentGroup(action.payload);
},
setLabels: (state, action: PayloadAction<ITaskLabel[]>) => {
state.labels = action.payload;
},
setMembers: (state, action: PayloadAction<ITaskListMemberFilter[]>) => {
state.taskAssignees = action.payload;
},
setPriorities: (state, action: PayloadAction<string[]>) => {
state.priorities = action.payload;
},
setStatuses: (state, action: PayloadAction<ITaskStatusViewModel[]>) => {
state.statuses = action.payload;
},
setFields: (state, action: PayloadAction<ITaskListSortableColumn[]>) => {
state.fields = action.payload;
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload;
},
setConvertToSubtaskDrawerOpen: (state, action: PayloadAction<boolean>) => {
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<ILabelsChangeResponse>) => {
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<ITaskListStatusChangeResponse>) => {
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<ITaskPhaseChangeResponse>) => {
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<string>) => {
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<ITaskListPriorityChangeResponse>) => {
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<string>) => {
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<ITaskListColumn>) => {
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<string>) => {
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<IProjectTask>) => {
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<ITaskRecurringScheduleData>) => {
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<ITaskLabel[]>) => {
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;