- Added custom column values to task responses in the API for better task management flexibility. - Implemented custom column components in the frontend, including dropdowns and date pickers, to improve user interaction. - Updated TaskListV2 and TaskRow components to handle custom columns, ensuring proper rendering and functionality. - Introduced a new PeopleDropdown component for selecting team members in custom columns, enhancing usability. - Enhanced styling for custom column components to support both light and dark modes, improving visual consistency.
837 lines
28 KiB
TypeScript
837 lines
28 KiB
TypeScript
import { useCallback, useEffect } from 'react';
|
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
|
import { useSocket } from '@/socket/socketContext';
|
|
import { useAuthService } from '@/hooks/useAuth';
|
|
import { SocketEvents } from '@/shared/socket-events';
|
|
import logger from '@/utils/errorLogger';
|
|
import alertService from '@/services/alerts/alertService';
|
|
import { store } from '@/app/store';
|
|
|
|
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
|
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
|
|
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
|
|
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
|
import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response';
|
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
|
import { Task } from '@/types/task-management.types';
|
|
|
|
import {
|
|
fetchTaskAssignees,
|
|
updateTaskAssignees,
|
|
fetchLabelsByProject,
|
|
updateTaskLabel,
|
|
updateTaskStatus,
|
|
updateTaskPriority,
|
|
updateTaskEndDate,
|
|
updateTaskEstimation,
|
|
updateTaskName,
|
|
updateTaskPhase,
|
|
updateTaskStartDate,
|
|
updateTaskDescription,
|
|
updateSubTasks,
|
|
updateTaskProgress,
|
|
} from '@/features/tasks/tasks.slice';
|
|
import {
|
|
addTask,
|
|
addTaskToGroup,
|
|
updateTask,
|
|
moveTaskToGroup,
|
|
moveTaskBetweenGroups,
|
|
selectCurrentGroupingV3,
|
|
fetchTasksV3,
|
|
addSubtaskToParent,
|
|
} from '@/features/task-management/task-management.slice';
|
|
import {
|
|
updateEnhancedKanbanSubtask,
|
|
addTaskToGroup as addEnhancedKanbanTaskToGroup,
|
|
updateEnhancedKanbanTaskStatus,
|
|
updateEnhancedKanbanTaskPriority,
|
|
updateEnhancedKanbanTaskAssignees,
|
|
updateEnhancedKanbanTaskLabels,
|
|
updateEnhancedKanbanTaskProgress,
|
|
updateEnhancedKanbanTaskName,
|
|
updateEnhancedKanbanTaskEndDate,
|
|
updateEnhancedKanbanTaskStartDate,
|
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
|
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
|
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
|
import {
|
|
setStartDate,
|
|
setTaskAssignee,
|
|
setTaskEndDate,
|
|
setTaskLabels,
|
|
setTaskPriority,
|
|
setTaskStatus,
|
|
setTaskSubscribers,
|
|
} from '@/features/task-drawer/task-drawer.slice';
|
|
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
|
|
|
export const useTaskSocketHandlers = () => {
|
|
const dispatch = useAppDispatch();
|
|
const { socket } = useSocket();
|
|
const currentSession = useAuthService().getCurrentSession();
|
|
|
|
const { loadingAssignees, taskGroups } = useAppSelector((state: any) => state.taskReducer);
|
|
const { projectId } = useAppSelector((state: any) => state.projectReducer);
|
|
const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3);
|
|
|
|
// Memoize socket event handlers
|
|
const handleAssigneesUpdate = useCallback(
|
|
(data: ITaskAssigneesUpdateResponse) => {
|
|
if (!data) return;
|
|
|
|
const updatedAssignees =
|
|
data.assignees?.map(assignee => ({
|
|
...assignee,
|
|
selected: true,
|
|
})) || [];
|
|
|
|
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
|
|
if (data.id) {
|
|
dispatch(
|
|
updateTask({
|
|
id: data.id,
|
|
changes: {
|
|
assignees: data.assignees?.map(a => a.team_member_id) || [],
|
|
assignee_names: data.names || [],
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
const groupId = taskGroups?.find((group: ITaskListGroup) =>
|
|
group.tasks?.some(
|
|
(task: IProjectTask) =>
|
|
task.id === data.id ||
|
|
(task.sub_tasks &&
|
|
task.sub_tasks.some((subtask: IProjectTask) => subtask.id === data.id))
|
|
)
|
|
)?.id;
|
|
|
|
if (groupId) {
|
|
dispatch(
|
|
updateTaskAssignees({
|
|
groupId,
|
|
taskId: data.id,
|
|
assignees: updatedAssignees,
|
|
})
|
|
);
|
|
|
|
dispatch(
|
|
setTaskAssignee({
|
|
...data,
|
|
manual_progress: false,
|
|
} as IProjectTask)
|
|
);
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
|
|
|
// Remove unnecessary refetch - real-time updates handle this
|
|
// if (currentSession?.team_id && !loadingAssignees) {
|
|
// dispatch(fetchTaskAssignees(currentSession.team_id));
|
|
// }
|
|
}
|
|
},
|
|
[taskGroups, dispatch, currentSession?.team_id, loadingAssignees]
|
|
);
|
|
|
|
const handleLabelsChange = useCallback(
|
|
async (labels: ILabelsChangeResponse) => {
|
|
if (!labels) return;
|
|
|
|
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
|
|
if (labels.id) {
|
|
dispatch(
|
|
updateTask({
|
|
id: labels.id,
|
|
changes: {
|
|
labels:
|
|
labels.labels?.map(l => ({
|
|
id: l.id || '',
|
|
name: l.name || '',
|
|
color: l.color_code || '#1890ff',
|
|
end: l.end,
|
|
names: l.names,
|
|
})) || [],
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update the old task slice and other related slices (for backward compatibility)
|
|
// Only update existing data, don't refetch from server
|
|
await Promise.all([
|
|
dispatch(updateTaskLabel(labels)),
|
|
dispatch(setTaskLabels(labels)),
|
|
// Remove unnecessary refetches - real-time updates handle this
|
|
// dispatch(fetchLabels()),
|
|
// projectId && dispatch(fetchLabelsByProject(projectId)),
|
|
]);
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskLabels(labels));
|
|
},
|
|
[dispatch, projectId]
|
|
);
|
|
|
|
const handleTaskStatusChange = useCallback(
|
|
(response: ITaskListStatusChangeResponse) => {
|
|
if (!response) return;
|
|
|
|
if (response.completed_deps === false) {
|
|
alertService.error(
|
|
'Task is not completed',
|
|
'Please complete the task dependencies before proceeding'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
dispatch(updateTaskStatus(response));
|
|
dispatch(deselectAll());
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskStatus(response));
|
|
|
|
// For the task management slice, update the task entity and handle group movement
|
|
const state = store.getState();
|
|
const groups = state.taskManagement.groups;
|
|
const currentTask = state.taskManagement.entities[response.id];
|
|
const currentGrouping = state.taskManagement.grouping;
|
|
|
|
if (currentTask) {
|
|
// Determine the new status value based on status category
|
|
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
|
|
if (response.statusCategory) {
|
|
if (response.statusCategory.is_done) {
|
|
newStatusValue = 'done';
|
|
} else if (response.statusCategory.is_doing) {
|
|
newStatusValue = 'doing';
|
|
} else {
|
|
newStatusValue = 'todo';
|
|
}
|
|
}
|
|
|
|
// Update the task entity first
|
|
dispatch(
|
|
updateTask({
|
|
...currentTask,
|
|
status: newStatusValue,
|
|
progress: response.complete_ratio || currentTask.progress,
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
);
|
|
|
|
// Handle group movement ONLY if grouping by status
|
|
if (groups && groups.length > 0 && currentGrouping === 'status') {
|
|
// Find current group containing the task
|
|
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
|
|
|
// Find target group based on new status value (not UUID)
|
|
const targetGroup = groups.find(group => group.groupValue === newStatusValue);
|
|
|
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
|
// Use the action to move task between groups
|
|
dispatch(
|
|
moveTaskBetweenGroups({
|
|
taskId: response.id,
|
|
sourceGroupId: currentGroup.id,
|
|
targetGroupId: targetGroup.id,
|
|
})
|
|
);
|
|
} else {
|
|
console.log('🔧 No group movement needed for status change');
|
|
}
|
|
} else {
|
|
console.log('🔧 Not grouped by status, skipping group movement');
|
|
}
|
|
}
|
|
},
|
|
[dispatch, currentGroupingV3, projectId]
|
|
);
|
|
|
|
const handleTaskProgress = useCallback(
|
|
(data: {
|
|
id: string;
|
|
status: string;
|
|
complete_ratio: number;
|
|
completed_count: number;
|
|
total_tasks_count: number;
|
|
parent_task: string;
|
|
}) => {
|
|
if (!data) return;
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
dispatch(
|
|
updateTaskProgress({
|
|
taskId: data.parent_task || data.id,
|
|
progress: data.complete_ratio,
|
|
totalTasksCount: data.total_tasks_count,
|
|
completedCount: data.completed_count,
|
|
})
|
|
);
|
|
|
|
// For the task management slice, update task progress
|
|
const taskId = data.parent_task || data.id;
|
|
if (taskId) {
|
|
dispatch(
|
|
updateTask({
|
|
id: taskId,
|
|
changes: {
|
|
progress: data.complete_ratio,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(
|
|
updateEnhancedKanbanTaskProgress({
|
|
id: data.id,
|
|
complete_ratio: data.complete_ratio,
|
|
completed_count: data.completed_count,
|
|
total_tasks_count: data.total_tasks_count,
|
|
parent_task: data.parent_task,
|
|
})
|
|
);
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handlePriorityChange = useCallback(
|
|
(response: ITaskListPriorityChangeResponse) => {
|
|
if (!response) return;
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
dispatch(updateTaskPriority(response));
|
|
dispatch(setTaskPriority(response));
|
|
dispatch(deselectAll());
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskPriority(response));
|
|
|
|
// For the task management slice, update the task entity and handle group movement
|
|
const state = store.getState();
|
|
const currentTask = state.taskManagement.entities[response.id];
|
|
const currentGrouping = state.taskManagement.grouping;
|
|
|
|
if (currentTask) {
|
|
// Get priority list to map priority_id to priority name
|
|
const priorityList = state.priorityReducer?.priorities || [];
|
|
const priority = priorityList.find(p => p.id === response.priority_id);
|
|
|
|
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
|
|
if (priority?.name) {
|
|
const priorityName = priority.name.toLowerCase();
|
|
if (['critical', 'high', 'medium', 'low'].includes(priorityName)) {
|
|
newPriorityValue = priorityName as 'critical' | 'high' | 'medium' | 'low';
|
|
}
|
|
}
|
|
|
|
// Update the task entity first
|
|
dispatch(
|
|
updateTask({
|
|
...currentTask,
|
|
priority: newPriorityValue,
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
);
|
|
|
|
// Handle group movement ONLY if grouping by priority
|
|
const groups = state.taskManagement.groups;
|
|
|
|
if (groups && groups.length > 0 && currentGrouping === 'priority') {
|
|
// Find current group containing the task
|
|
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
|
|
|
// Find target group based on new priority value
|
|
const targetGroup = groups.find(
|
|
group => group.groupValue?.toLowerCase() === newPriorityValue.toLowerCase()
|
|
);
|
|
|
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
|
dispatch(
|
|
moveTaskBetweenGroups({
|
|
taskId: response.id,
|
|
sourceGroupId: currentGroup.id,
|
|
targetGroupId: targetGroup.id,
|
|
})
|
|
);
|
|
} else {
|
|
console.log('🔧 No group movement needed for priority change');
|
|
}
|
|
} else {
|
|
console.log('🔧 Not grouped by priority, skipping group movement');
|
|
}
|
|
}
|
|
},
|
|
[dispatch, currentGroupingV3]
|
|
);
|
|
|
|
const handleEndDateChange = useCallback(
|
|
(task: { id: string; parent_task: string | null; end_date: string }) => {
|
|
if (!task) return;
|
|
|
|
const taskWithProgress = {
|
|
...task,
|
|
manual_progress: false,
|
|
} as IProjectTask;
|
|
|
|
dispatch(updateTaskEndDate({ task: taskWithProgress }));
|
|
dispatch(setTaskEndDate(taskWithProgress));
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress }));
|
|
|
|
// Update task-management slice for task-list-v2 components
|
|
const currentTask = store.getState().taskManagement.entities[task.id];
|
|
if (currentTask) {
|
|
dispatch(updateTask({
|
|
...currentTask,
|
|
dueDate: task.end_date,
|
|
updatedAt: new Date().toISOString(),
|
|
}));
|
|
}
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleTaskNameChange = useCallback(
|
|
(data: { id: string; parent_task: string; name: string }) => {
|
|
if (!data) return;
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
dispatch(updateTaskName(data));
|
|
|
|
// For the task management slice, update task name
|
|
if (data.id) {
|
|
dispatch(
|
|
updateTask({
|
|
id: data.id,
|
|
changes: {
|
|
title: data.name,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update enhanced kanban slice
|
|
dispatch(updateEnhancedKanbanTaskName({ task: data }));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handlePhaseChange = useCallback(
|
|
(data: ITaskPhaseChangeResponse) => {
|
|
if (!data) return;
|
|
|
|
// Update the old task slice (for backward compatibility)
|
|
dispatch(updateTaskPhase(data));
|
|
dispatch(deselectAll());
|
|
|
|
// For the task management slice, always update the task entity first
|
|
const state = store.getState();
|
|
const taskId = data.task_id;
|
|
|
|
if (taskId) {
|
|
const currentTask = state.taskManagement.entities[taskId];
|
|
|
|
if (currentTask) {
|
|
// Get phase list to map phase_id to phase name
|
|
const phaseList = state.phaseReducer?.phaseList || [];
|
|
let newPhaseValue = '';
|
|
|
|
if (data.id) {
|
|
// data.id is the phase_id
|
|
const phase = phaseList.find(p => p.id === data.id);
|
|
newPhaseValue = phase?.name || '';
|
|
} else {
|
|
// No phase selected (cleared)
|
|
newPhaseValue = '';
|
|
}
|
|
|
|
// Update the task entity
|
|
dispatch(
|
|
updateTask({
|
|
id: taskId,
|
|
changes: {
|
|
phase: newPhaseValue,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
|
|
// Handle group movement ONLY if grouping by phase
|
|
const groups = state.taskManagement.groups;
|
|
const currentGrouping = state.taskManagement.grouping;
|
|
|
|
if (groups && groups.length > 0 && currentGrouping === 'phase') {
|
|
// Find current group containing the task
|
|
const currentGroup = groups.find(group => group.taskIds.includes(taskId));
|
|
|
|
// Find target group based on new phase value
|
|
let targetGroup: any = null;
|
|
|
|
if (newPhaseValue && newPhaseValue.trim() !== '') {
|
|
// Find group by phase name
|
|
targetGroup = groups.find(
|
|
group => group.groupValue === newPhaseValue || group.title === newPhaseValue
|
|
);
|
|
} else {
|
|
// Find "Unmapped" group for tasks without a phase or with default phase
|
|
targetGroup = groups.find(
|
|
group =>
|
|
group.groupValue === 'Unmapped' ||
|
|
group.title === 'Unmapped' ||
|
|
group.title.toLowerCase().includes('unmapped')
|
|
);
|
|
}
|
|
|
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
|
dispatch(
|
|
moveTaskBetweenGroups({
|
|
taskId: taskId,
|
|
fromGroupId: currentGroup.id,
|
|
toGroupId: targetGroup.id,
|
|
taskUpdate: {
|
|
phase: newPhaseValue,
|
|
},
|
|
})
|
|
);
|
|
} else {
|
|
console.log('🔧 No group movement needed for phase change');
|
|
}
|
|
} else {
|
|
console.log('🔧 Not grouped by phase, skipping group movement');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[dispatch, currentGroupingV3, projectId]
|
|
);
|
|
|
|
const handleStartDateChange = useCallback(
|
|
(task: { id: string; parent_task: string | null; start_date: string }) => {
|
|
if (!task) return;
|
|
|
|
const taskWithProgress = {
|
|
...task,
|
|
manual_progress: false,
|
|
} as IProjectTask;
|
|
|
|
dispatch(updateTaskStartDate({ task: taskWithProgress }));
|
|
dispatch(setStartDate(taskWithProgress));
|
|
|
|
// Update task-management slice for task-list-v2 components
|
|
const currentTask = store.getState().taskManagement.entities[task.id];
|
|
if (currentTask) {
|
|
dispatch(updateTask({
|
|
...currentTask,
|
|
startDate: task.start_date,
|
|
updatedAt: new Date().toISOString(),
|
|
}));
|
|
}
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleTaskSubscribersChange = useCallback(
|
|
(data: InlineMember[]) => {
|
|
if (!data) return;
|
|
dispatch(setTaskSubscribers(data));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleEstimationChange = useCallback(
|
|
(task: { id: string; parent_task: string | null; estimation: number }) => {
|
|
if (!task) return;
|
|
|
|
const taskWithProgress = {
|
|
...task,
|
|
manual_progress: false,
|
|
} as IProjectTask;
|
|
|
|
dispatch(updateTaskEstimation({ task: taskWithProgress }));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleTaskDescriptionChange = useCallback(
|
|
(data: { id: string; parent_task: string; description: string }) => {
|
|
if (!data) return;
|
|
dispatch(updateTaskDescription(data));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleNewTaskReceived = useCallback(
|
|
(response: any) => {
|
|
// Handle array format response [index, taskData]
|
|
const data = Array.isArray(response) ? response[1] : response;
|
|
if (!data) return;
|
|
if (data.parent_task_id) {
|
|
// Handle subtask creation
|
|
const subtask: Task = {
|
|
id: data.id || '',
|
|
task_key: data.task_key || '',
|
|
title: data.name || '',
|
|
description: data.description || '',
|
|
status: (data.status_category?.is_todo
|
|
? 'todo'
|
|
: data.status_category?.is_doing
|
|
? 'doing'
|
|
: data.status_category?.is_done
|
|
? 'done'
|
|
: 'todo') as 'todo' | 'doing' | 'done',
|
|
priority: (data.priority_value === 3
|
|
? 'critical'
|
|
: data.priority_value === 2
|
|
? 'high'
|
|
: data.priority_value === 1
|
|
? 'medium'
|
|
: 'low') as 'critical' | 'high' | 'medium' | 'low',
|
|
phase: data.phase_name || 'Development',
|
|
progress: data.complete_ratio || 0,
|
|
assignees: data.assignees?.map((a: any) => a.team_member_id) || [],
|
|
assignee_names: data.names || [],
|
|
labels:
|
|
data.labels?.map((l: any) => ({
|
|
id: l.id || '',
|
|
name: l.name || '',
|
|
color: l.color_code || '#1890ff',
|
|
end: l.end,
|
|
names: l.names,
|
|
})) || [],
|
|
dueDate: data.end_date,
|
|
timeTracking: {
|
|
estimated: (data.total_hours || 0) + (data.total_minutes || 0) / 60,
|
|
logged: (data.time_spent?.hours || 0) + (data.time_spent?.minutes || 0) / 60,
|
|
},
|
|
created_at: data.created_at || new Date().toISOString(),
|
|
updated_at: data.updated_at || new Date().toISOString(),
|
|
order: data.sort_order || 0,
|
|
parent_task_id: data.parent_task_id,
|
|
is_sub_task: true,
|
|
};
|
|
dispatch(addSubtaskToParent({ parentId: data.parent_task_id, subtask }));
|
|
|
|
// Also update enhanced kanban slice for subtask creation
|
|
dispatch(
|
|
updateEnhancedKanbanSubtask({
|
|
sectionId: '',
|
|
subtask: data,
|
|
mode: 'add',
|
|
})
|
|
);
|
|
} else {
|
|
// Handle regular task creation - transform to Task format and add
|
|
const task: Task = {
|
|
id: data.id || '',
|
|
task_key: data.task_key || '',
|
|
title: data.name || '',
|
|
description: data.description || '',
|
|
status: (data.status_category?.is_todo
|
|
? 'todo'
|
|
: data.status_category?.is_doing
|
|
? 'doing'
|
|
: data.status_category?.is_done
|
|
? 'done'
|
|
: 'todo') as 'todo' | 'doing' | 'done',
|
|
priority: (data.priority_value === 3
|
|
? 'critical'
|
|
: data.priority_value === 2
|
|
? 'high'
|
|
: data.priority_value === 1
|
|
? 'medium'
|
|
: 'low') as 'critical' | 'high' | 'medium' | 'low',
|
|
phase: data.phase_name || 'Development',
|
|
progress: data.complete_ratio || 0,
|
|
assignees: data.assignees?.map((a: any) => a.team_member_id) || [],
|
|
assignee_names: data.names || [],
|
|
labels:
|
|
data.labels?.map((l: any) => ({
|
|
id: l.id || '',
|
|
name: l.name || '',
|
|
color: l.color_code || '#1890ff',
|
|
end: l.end,
|
|
names: l.names,
|
|
})) || [],
|
|
dueDate: data.end_date,
|
|
startDate: data.start_date,
|
|
timeTracking: {
|
|
estimated: (data.total_hours || 0) + (data.total_minutes || 0) / 60,
|
|
logged: (data.time_spent?.hours || 0) + (data.time_spent?.minutes || 0) / 60,
|
|
},
|
|
created_at: data.created_at || new Date().toISOString(),
|
|
updated_at: data.updated_at || new Date().toISOString(),
|
|
order: data.sort_order || 0,
|
|
sub_tasks: [],
|
|
sub_tasks_count: 0,
|
|
show_sub_tasks: false,
|
|
};
|
|
|
|
// Extract the group UUID from the backend response based on current grouping
|
|
let groupId: string | undefined;
|
|
|
|
// Select the correct UUID based on current grouping
|
|
// If currentGroupingV3 is null, default to 'status' since that's the most common grouping
|
|
const grouping = currentGroupingV3 || 'status';
|
|
|
|
if (grouping === 'status') {
|
|
// For status grouping, use status field (which contains the status UUID)
|
|
groupId = data.status;
|
|
} else if (grouping === 'priority') {
|
|
// For priority grouping, use priority field (which contains the priority UUID)
|
|
groupId = data.priority;
|
|
} else if (grouping === 'phase') {
|
|
// For phase grouping, use phase_id
|
|
groupId = data.phase_id;
|
|
}
|
|
|
|
// Use addTaskToGroup with the actual group UUID
|
|
dispatch(addTaskToGroup({ task, groupId: groupId || '' }));
|
|
|
|
// Also update enhanced kanban slice for regular task creation
|
|
dispatch(
|
|
addEnhancedKanbanTaskToGroup({
|
|
sectionId: groupId || '',
|
|
task: data,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleTaskProgressUpdated = useCallback(
|
|
(data: { task_id: string; progress_value?: number; weight?: number }) => {
|
|
if (!data || !taskGroups) return;
|
|
|
|
if (data.progress_value !== undefined) {
|
|
for (const group of taskGroups) {
|
|
const task = group.tasks?.find((task: IProjectTask) => task.id === data.task_id);
|
|
if (task) {
|
|
dispatch(
|
|
updateTaskProgress({
|
|
taskId: data.task_id,
|
|
progress: data.progress_value,
|
|
totalTasksCount: task.total_tasks_count || 0,
|
|
completedCount: task.completed_count || 0,
|
|
})
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[dispatch, taskGroups]
|
|
);
|
|
|
|
const handleCustomColumnUpdate = useCallback(
|
|
(data: { task_id: string; column_key: string; value: string }) => {
|
|
if (!data || !data.task_id || !data.column_key) return;
|
|
|
|
// Update the task-management slice for task-list-v2 components
|
|
const currentTask = store.getState().taskManagement.entities[data.task_id];
|
|
if (currentTask) {
|
|
const updatedCustomColumnValues = {
|
|
...currentTask.custom_column_values,
|
|
[data.column_key]: data.value,
|
|
};
|
|
|
|
const updatedTask: Task = {
|
|
...currentTask,
|
|
custom_column_values: updatedCustomColumnValues,
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
|
|
dispatch(updateTask(updatedTask));
|
|
}
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
// Handler for TASK_ASSIGNEES_CHANGE (fallback event with limited data)
|
|
const handleTaskAssigneesChange = useCallback((data: { assigneeIds: string[] }) => {
|
|
if (!data || !data.assigneeIds) return;
|
|
|
|
// This event only provides assignee IDs, so we update what we can
|
|
// The full assignee data will come from QUICK_ASSIGNEES_UPDATE
|
|
// console.log('🔄 Task assignees change (limited data):', data);
|
|
}, []);
|
|
|
|
// Register socket event listeners
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
|
|
const eventHandlers = [
|
|
{ event: SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handler: handleAssigneesUpdate },
|
|
{ event: SocketEvents.TASK_ASSIGNEES_CHANGE.toString(), handler: handleTaskAssigneesChange },
|
|
{ event: SocketEvents.TASK_LABELS_CHANGE.toString(), handler: handleLabelsChange },
|
|
{ event: SocketEvents.CREATE_LABEL.toString(), handler: handleLabelsChange },
|
|
{ event: SocketEvents.TASK_STATUS_CHANGE.toString(), handler: handleTaskStatusChange },
|
|
{ event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgress },
|
|
{ event: SocketEvents.TASK_PRIORITY_CHANGE.toString(), handler: handlePriorityChange },
|
|
{ event: SocketEvents.TASK_END_DATE_CHANGE.toString(), handler: handleEndDateChange },
|
|
{ event: SocketEvents.TASK_NAME_CHANGE.toString(), handler: handleTaskNameChange },
|
|
{ event: SocketEvents.TASK_PHASE_CHANGE.toString(), handler: handlePhaseChange },
|
|
{ event: SocketEvents.TASK_START_DATE_CHANGE.toString(), handler: handleStartDateChange },
|
|
{
|
|
event: SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(),
|
|
handler: handleTaskSubscribersChange,
|
|
},
|
|
{
|
|
event: SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(),
|
|
handler: handleEstimationChange,
|
|
},
|
|
{
|
|
event: SocketEvents.TASK_DESCRIPTION_CHANGE.toString(),
|
|
handler: handleTaskDescriptionChange,
|
|
},
|
|
{ event: SocketEvents.QUICK_TASK.toString(), handler: handleNewTaskReceived },
|
|
{ event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdated },
|
|
{ event: SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), handler: handleCustomColumnUpdate },
|
|
];
|
|
|
|
// Register all event listeners
|
|
eventHandlers.forEach(({ event, handler }) => {
|
|
socket.on(event, handler);
|
|
});
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
eventHandlers.forEach(({ event, handler }) => {
|
|
socket.off(event, handler);
|
|
});
|
|
};
|
|
}, [
|
|
socket,
|
|
handleAssigneesUpdate,
|
|
handleTaskAssigneesChange,
|
|
handleLabelsChange,
|
|
handleTaskStatusChange,
|
|
handleTaskProgress,
|
|
handlePriorityChange,
|
|
handleEndDateChange,
|
|
handleTaskNameChange,
|
|
handlePhaseChange,
|
|
handleStartDateChange,
|
|
handleTaskSubscribersChange,
|
|
handleEstimationChange,
|
|
handleTaskDescriptionChange,
|
|
handleNewTaskReceived,
|
|
handleTaskProgressUpdated,
|
|
handleCustomColumnUpdate,
|
|
]);
|
|
};
|