expand sub tasks

This commit is contained in:
chamiakJ
2025-07-03 01:31:05 +05:30
parent 3bef18901a
commit ecd4d29a38
435 changed files with 13150 additions and 11087 deletions

View File

@@ -11,7 +11,7 @@ const useDragCursor = (isDragging: boolean) => {
// Save the original cursor style
const originalCursor = document.body.style.cursor;
// Apply grabbing cursor to the entire document when dragging
document.body.style.cursor = 'grabbing';
@@ -22,4 +22,4 @@ const useDragCursor = (isDragging: boolean) => {
}, [isDragging]);
};
export default useDragCursor;
export default useDragCursor;

View File

@@ -2,10 +2,7 @@ import { useEffect, useCallback, useMemo } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
import {
fetchLabelsByProject,
fetchTaskAssignees,
} from '@/features/tasks/tasks.slice';
import { fetchLabelsByProject, fetchTaskAssignees } from '@/features/tasks/tasks.slice';
import { getTeamMembers } from '@/features/team-members/team-members.slice';
/**
@@ -14,10 +11,10 @@ import { getTeamMembers } from '@/features/team-members/team-members.slice';
*/
export const useFilterDataLoader = () => {
const dispatch = useAppDispatch();
// Memoize the priorities selector to prevent unnecessary re-renders
const priorities = useAppSelector(state => state.priorityReducer.priorities);
// Memoize the projectId selector to prevent unnecessary re-renders
const projectId = useAppSelector(state => state.projectReducer.projectId);
@@ -38,14 +35,16 @@ export const useFilterDataLoader = () => {
}
// Load team members for member filters
dispatch(getTeamMembers({
index: 0,
size: 100,
field: null,
order: null,
search: null,
all: true
}));
dispatch(
getTeamMembers({
index: 0,
size: 100,
field: null,
order: null,
search: null,
all: true,
})
);
} catch (error) {
console.error('Error loading filter data:', error);
// Don't throw - filter loading errors shouldn't break the main UI
@@ -57,11 +56,11 @@ export const useFilterDataLoader = () => {
// Use setTimeout to ensure this runs after the main component render
// This prevents filter loading from blocking the initial render
const timeoutId = setTimeout(loadFilterData, 0);
return () => clearTimeout(timeoutId);
}, [loadFilterData]);
return {
loadFilterData,
};
};
};

View File

@@ -5,12 +5,14 @@ const useIsProjectManager = () => {
const currentSession = useAuthService().getCurrentSession();
const { project: currentProject } = useAppSelector(state => state.projectReducer);
const { project: drawerProject } = useAppSelector(state => state.projectDrawerReducer);
// Check if user is project manager for either the current project or drawer project
const isManagerOfCurrentProject = currentSession?.team_member_id === currentProject?.project_manager?.id;
const isManagerOfDrawerProject = currentSession?.team_member_id === drawerProject?.project_manager?.id;
const isManagerOfCurrentProject =
currentSession?.team_member_id === currentProject?.project_manager?.id;
const isManagerOfDrawerProject =
currentSession?.team_member_id === drawerProject?.project_manager?.id;
return isManagerOfCurrentProject || isManagerOfDrawerProject;
};
export default useIsProjectManager;
export default useIsProjectManager;

View File

@@ -3,4 +3,4 @@ import { useLayoutEffect, useEffect } from 'react';
// Use useLayoutEffect in browser environments and useEffect in SSR environments
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
export default useIsomorphicLayoutEffect;

View File

@@ -15,29 +15,32 @@ export const usePerformanceOptimization = () => {
lastRenderTimeRef.current = now;
if (process.env.NODE_ENV === 'development') {
console.log(`[${componentName}] Render #${renderCountRef.current}, Time since last: ${timeSinceLastRender.toFixed(2)}ms`);
if (timeSinceLastRender < 16) { // Less than 60fps
console.log(
`[${componentName}] Render #${renderCountRef.current}, Time since last: ${timeSinceLastRender.toFixed(2)}ms`
);
if (timeSinceLastRender < 16) {
// Less than 60fps
console.warn(`[${componentName}] Potential over-rendering detected`);
}
}
}, []);
// Debounced callback creator
const createDebouncedCallback = useCallback(<T extends (...args: any[]) => any>(
callback: T,
delay: number = 300
) => {
return debounce(callback, delay);
}, []);
const createDebouncedCallback = useCallback(
<T extends (...args: any[]) => any>(callback: T, delay: number = 300) => {
return debounce(callback, delay);
},
[]
);
// Throttled callback creator
const createThrottledCallback = useCallback(<T extends (...args: any[]) => any>(
callback: T,
delay: number = 100
) => {
return throttle(callback, delay);
}, []);
const createThrottledCallback = useCallback(
<T extends (...args: any[]) => any>(callback: T, delay: number = 100) => {
return throttle(callback, delay);
},
[]
);
return {
trackRender,
@@ -73,11 +76,11 @@ export const useOptimizedEventHandlers = <T extends Record<string, (...args: any
) => {
return useMemo(() => {
const optimizedHandlers = {} as any;
Object.entries(handlers).forEach(([key, handler]) => {
optimizedHandlers[key] = useCallback(handler, [handler]);
});
return optimizedHandlers as T;
}, [handlers]);
};
@@ -90,11 +93,8 @@ export const useVirtualScrolling = (
) => {
const visibleRange = useMemo(() => {
const startIndex = Math.floor(window.scrollY / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
itemCount
);
const endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight) + 1, itemCount);
return { startIndex: Math.max(0, startIndex), endIndex };
}, [itemCount, itemHeight, containerHeight]);
@@ -106,22 +106,25 @@ export const useLazyLoading = (threshold: number = 0.1) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isVisible, setIsVisible] = useState(false);
const targetRef = useCallback((node: HTMLElement | null) => {
if (observerRef.current) observerRef.current.disconnect();
if (node) {
observerRef.current = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observerRef.current?.disconnect();
}
},
{ threshold }
);
observerRef.current.observe(node);
}
}, [threshold]);
const targetRef = useCallback(
(node: HTMLElement | null) => {
if (observerRef.current) observerRef.current.disconnect();
if (node) {
observerRef.current = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observerRef.current?.disconnect();
}
},
{ threshold }
);
observerRef.current.observe(node);
}
},
[threshold]
);
useEffect(() => {
return () => {
@@ -133,10 +136,7 @@ export const useLazyLoading = (threshold: number = 0.1) => {
};
// Memory optimization for large datasets
export const useMemoryOptimization = <T>(
data: T[],
maxCacheSize: number = 1000
) => {
export const useMemoryOptimization = <T>(data: T[], maxCacheSize: number = 1000) => {
const cacheRef = useRef(new Map<string, T>());
const optimizedData = useMemo(() => {
@@ -147,7 +147,7 @@ export const useMemoryOptimization = <T>(
// Keep only the most recently accessed items
const cache = cacheRef.current;
const recentData = data.slice(0, maxCacheSize);
// Clear old cache entries
cache.clear();
recentData.forEach((item, index) => {
@@ -160,4 +160,4 @@ export const useMemoryOptimization = <T>(
return optimizedData;
};
export default usePerformanceOptimization;
export default usePerformanceOptimization;

View File

@@ -4,8 +4,8 @@ const useTabSearchParam = () => {
const [searchParams] = useSearchParams();
const tab = searchParams.get('tab');
const projectView = tab === 'tasks-list' ? 'list' : 'kanban';
return { tab, projectView };
};
export default useTabSearchParam;
export default useTabSearchParam;

View File

@@ -2,7 +2,11 @@ import { useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAppSelector } from './useAppSelector';
import { useAppDispatch } from './useAppDispatch';
import { fetchTask, setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
import {
fetchTask,
setSelectedTaskId,
setShowTaskDrawer,
} from '@/features/task-drawer/task-drawer.slice';
/**
* A custom hook that synchronizes the task drawer state with the URL.
@@ -15,7 +19,7 @@ const useTaskDrawerUrlSync = () => {
const dispatch = useAppDispatch();
const { showTaskDrawer, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
const { projectId } = useAppSelector(state => state.projectReducer);
// Use a ref to track whether we're in the process of closing the drawer
const isClosingDrawer = useRef(false);
// Use a ref to track the last task ID we processed
@@ -29,14 +33,14 @@ const useTaskDrawerUrlSync = () => {
// Set the flag to indicate we're closing the drawer
isClosingDrawer.current = true;
shouldIgnoreUrlChange.current = true;
// Create a new URLSearchParams object to avoid modifying the current one
const newParams = new URLSearchParams(searchParams);
newParams.delete('task');
// Update the URL without triggering a navigation
setSearchParams(newParams, { replace: true });
// Reset the flags after a short delay
setTimeout(() => {
isClosingDrawer.current = false;
@@ -49,24 +53,26 @@ const useTaskDrawerUrlSync = () => {
useEffect(() => {
// Skip if we're programmatically updating the URL or closing the drawer
if (shouldIgnoreUrlChange.current || isClosingDrawer.current) return;
const taskIdFromUrl = searchParams.get('task');
// Only process URL changes if:
// 1. There's a task ID in the URL
// 2. The drawer is not currently open
// 3. We have a project ID
// 4. It's a different task ID than what we last processed
// 5. The selected task ID is different from URL (to avoid reopening same task)
if (taskIdFromUrl &&
!showTaskDrawer &&
projectId &&
taskIdFromUrl !== lastProcessedTaskId.current &&
taskIdFromUrl !== selectedTaskId) {
if (
taskIdFromUrl &&
!showTaskDrawer &&
projectId &&
taskIdFromUrl !== lastProcessedTaskId.current &&
taskIdFromUrl !== selectedTaskId
) {
lastProcessedTaskId.current = taskIdFromUrl;
dispatch(setSelectedTaskId(taskIdFromUrl));
dispatch(setShowTaskDrawer(true));
// Fetch task data
dispatch(fetchTask({ taskId: taskIdFromUrl, projectId }));
}
@@ -76,22 +82,22 @@ const useTaskDrawerUrlSync = () => {
useEffect(() => {
// Don't update URL if we're in the process of closing or ignoring changes
if (isClosingDrawer.current || shouldIgnoreUrlChange.current) return;
if (showTaskDrawer && selectedTaskId) {
// Don't update if it's the same task ID we already processed
if (lastProcessedTaskId.current === selectedTaskId) return;
// Add task ID to URL when drawer is opened
shouldIgnoreUrlChange.current = true;
lastProcessedTaskId.current = selectedTaskId;
// Create a new URLSearchParams object to avoid modifying the current one
const newParams = new URLSearchParams(searchParams);
newParams.set('task', selectedTaskId);
// Update the URL without triggering a navigation
setSearchParams(newParams, { replace: true });
// Reset the flag after a short delay
setTimeout(() => {
shouldIgnoreUrlChange.current = false;
@@ -103,11 +109,14 @@ const useTaskDrawerUrlSync = () => {
useEffect(() => {
// Only clear URL when drawer is closed and we have a task in URL
// Also ensure we're not in the middle of processing other URL changes
if (!showTaskDrawer &&
searchParams.has('task') &&
!isClosingDrawer.current &&
!shouldIgnoreUrlChange.current &&
!selectedTaskId) { // Only clear if selectedTaskId is also null/cleared
if (
!showTaskDrawer &&
searchParams.has('task') &&
!isClosingDrawer.current &&
!shouldIgnoreUrlChange.current &&
!selectedTaskId
) {
// Only clear if selectedTaskId is also null/cleared
clearTaskFromUrl();
lastProcessedTaskId.current = null;
}
@@ -116,4 +125,4 @@ const useTaskDrawerUrlSync = () => {
return { clearTaskFromUrl };
};
export default useTaskDrawerUrlSync;
export default useTaskDrawerUrlSync;

View File

@@ -34,13 +34,13 @@ import {
updateTaskProgress,
} from '@/features/tasks/tasks.slice';
import {
addTask,
addTask,
addTaskToGroup,
updateTask,
updateTask,
moveTaskToGroup,
moveTaskBetweenGroups,
selectCurrentGroupingV3,
fetchTasksV3
fetchTasksV3,
} from '@/features/task-management/task-management.slice';
import {
updateEnhancedKanbanSubtask,
@@ -52,7 +52,7 @@ import {
updateEnhancedKanbanTaskProgress,
updateEnhancedKanbanTaskName,
updateEnhancedKanbanTaskEndDate,
updateEnhancedKanbanTaskStartDate
updateEnhancedKanbanTaskStartDate,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
@@ -71,7 +71,7 @@ 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);
@@ -81,21 +81,24 @@ export const useTaskSocketHandlers = () => {
(data: ITaskAssigneesUpdateResponse) => {
if (!data) return;
const updatedAssignees = data.assignees?.map(assignee => ({
...assignee,
selected: true,
})) || [];
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(),
}
}));
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)
@@ -103,7 +106,8 @@ export const useTaskSocketHandlers = () => {
group.tasks?.some(
(task: IProjectTask) =>
task.id === data.id ||
(task.sub_tasks && task.sub_tasks.some((subtask: IProjectTask) => subtask.id === data.id))
(task.sub_tasks &&
task.sub_tasks.some((subtask: IProjectTask) => subtask.id === data.id))
)
)?.id;
@@ -122,7 +126,7 @@ export const useTaskSocketHandlers = () => {
manual_progress: false,
} as IProjectTask)
);
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskAssignees(data));
@@ -138,24 +142,27 @@ export const useTaskSocketHandlers = () => {
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(),
}
}));
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([
@@ -165,7 +172,7 @@ export const useTaskSocketHandlers = () => {
// dispatch(fetchLabels()),
// projectId && dispatch(fetchLabelsByProject(projectId)),
]);
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskLabels(labels));
},
@@ -187,7 +194,7 @@ export const useTaskSocketHandlers = () => {
// Update the old task slice (for backward compatibility)
dispatch(updateTaskStatus(response));
dispatch(deselectAll());
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskStatus(response));
@@ -195,18 +202,16 @@ export const useTaskSocketHandlers = () => {
const state = store.getState();
const groups = state.taskManagement.groups;
const currentTask = state.taskManagement.entities[response.id];
if (groups && groups.length > 0 && currentTask && response.status_id) {
// Find current group containing the task
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
// Find target group based on new status ID
// The status_id from response is the UUID of the new status
const targetGroup = groups.find(group => group.id === response.status_id);
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
// Determine the new status value based on status category
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
if (response.statusCategory) {
@@ -218,17 +223,19 @@ export const useTaskSocketHandlers = () => {
newStatusValue = 'todo';
}
}
// Use the new action to move task between groups
dispatch(moveTaskBetweenGroups({
taskId: response.id,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
status: newStatusValue,
progress: response.complete_ratio || currentTask.progress,
}
}));
dispatch(
moveTaskBetweenGroups({
taskId: response.id,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
status: newStatusValue,
progress: response.complete_ratio || currentTask.progress,
},
})
);
} else if (!currentGroup || !targetGroup) {
// Remove unnecessary refetch that causes data thrashing
// if (projectId) {
@@ -264,23 +271,27 @@ export const useTaskSocketHandlers = () => {
// 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(),
}
}));
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(
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]
);
@@ -293,19 +304,19 @@ export const useTaskSocketHandlers = () => {
dispatch(updateTaskPriority(response));
dispatch(setTaskPriority(response));
dispatch(deselectAll());
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskPriority(response));
// For the task management slice, always update the task entity first
const state = store.getState();
const currentTask = state.taskManagement.entities[response.id];
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();
@@ -315,37 +326,40 @@ export const useTaskSocketHandlers = () => {
}
// Update the task entity
dispatch(updateTask({
id: response.id,
changes: {
priority: newPriorityValue,
updatedAt: new Date().toISOString(),
}
}));
dispatch(
updateTask({
id: response.id,
changes: {
priority: newPriorityValue,
updatedAt: new Date().toISOString(),
},
})
);
// Handle group movement ONLY if grouping by priority
const groups = state.taskManagement.groups;
const currentGrouping = state.taskManagement.grouping;
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()
const targetGroup = groups.find(
group => group.groupValue.toLowerCase() === newPriorityValue.toLowerCase()
);
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
dispatch(moveTaskBetweenGroups({
taskId: response.id,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
priority: newPriorityValue,
}
}));
dispatch(
moveTaskBetweenGroups({
taskId: response.id,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
priority: newPriorityValue,
},
})
);
} else {
console.log('🔧 No group movement needed for priority change');
}
@@ -358,11 +372,7 @@ export const useTaskSocketHandlers = () => {
);
const handleEndDateChange = useCallback(
(task: {
id: string;
parent_task: string | null;
end_date: string;
}) => {
(task: { id: string; parent_task: string | null; end_date: string }) => {
if (!task) return;
const taskWithProgress = {
@@ -372,7 +382,7 @@ export const useTaskSocketHandlers = () => {
dispatch(updateTaskEndDate({ task: taskWithProgress }));
dispatch(setTaskEndDate(taskWithProgress));
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress }));
},
@@ -382,21 +392,23 @@ export const useTaskSocketHandlers = () => {
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(),
}
}));
dispatch(
updateTask({
id: data.id,
changes: {
title: data.name,
updatedAt: new Date().toISOString(),
},
})
);
}
// Update enhanced kanban slice
dispatch(updateEnhancedKanbanTaskName({ task: data }));
},
@@ -406,7 +418,7 @@ export const useTaskSocketHandlers = () => {
const handlePhaseChange = useCallback(
(data: ITaskPhaseChangeResponse) => {
if (!data) return;
// Update the old task slice (for backward compatibility)
dispatch(updateTaskPhase(data));
dispatch(deselectAll());
@@ -414,72 +426,78 @@ export const useTaskSocketHandlers = () => {
// 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 = '';
}
// Get phase list to map phase_id to phase name
const phaseList = state.phaseReducer?.phaseList || [];
let 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) {
// Find group by phase name
targetGroup = groups.find(group =>
group.groupValue === newPhaseValue || group.title === newPhaseValue
);
if (data.id) {
// data.id is the phase_id
const phase = phaseList.find(p => p.id === data.id);
newPhaseValue = phase?.name || '';
} else {
// Find "No Phase" or similar group
targetGroup = groups.find(group =>
group.groupValue === '' || group.title.toLowerCase().includes('no phase') || group.title.toLowerCase().includes('unassigned')
);
// No phase selected (cleared)
newPhaseValue = '';
}
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
dispatch(moveTaskBetweenGroups({
taskId: taskId,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
// 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) {
// Find group by phase name
targetGroup = groups.find(
group => group.groupValue === newPhaseValue || group.title === newPhaseValue
);
} else {
// Find "No Phase" or similar group
targetGroup = groups.find(
group =>
group.groupValue === '' ||
group.title.toLowerCase().includes('no phase') ||
group.title.toLowerCase().includes('unassigned')
);
}
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('🔧 No group movement needed for phase change');
console.log('🔧 Not grouped by phase, skipping group movement');
}
} else {
console.log('🔧 Not grouped by phase, skipping group movement');
}
}
}
},
@@ -487,11 +505,7 @@ export const useTaskSocketHandlers = () => {
);
const handleStartDateChange = useCallback(
(task: {
id: string;
parent_task: string | null;
start_date: string;
}) => {
(task: { id: string; parent_task: string | null; start_date: string }) => {
if (!task) return;
const taskWithProgress = {
@@ -514,11 +528,7 @@ export const useTaskSocketHandlers = () => {
);
const handleEstimationChange = useCallback(
(task: {
id: string;
parent_task: string | null;
estimation: number;
}) => {
(task: { id: string; parent_task: string | null; estimation: number }) => {
if (!task) return;
const taskWithProgress = {
@@ -532,11 +542,7 @@ export const useTaskSocketHandlers = () => {
);
const handleTaskDescriptionChange = useCallback(
(data: {
id: string;
parent_task: string;
description: string;
}) => {
(data: { id: string; parent_task: string; description: string }) => {
if (!data) return;
dispatch(updateTaskDescription(data));
},
@@ -548,14 +554,59 @@ export const useTaskSocketHandlers = () => {
if (!data) return;
if (data.parent_task_id) {
// Handle subtask creation
dispatch(updateSubTasks(data));
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 => a.team_member_id) || [],
assignee_names: data.names || [],
labels:
data.labels?.map(l => ({
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,
},
customFields: {},
createdAt: data.created_at || new Date().toISOString(),
updatedAt: data.updated_at || new Date().toISOString(),
order: data.sort_order || 0,
parent_task_id: data.parent_task_id,
is_sub_task: true,
};
dispatch(addSubtaskToParent({ subtask, parentTaskId: data.parent_task_id }));
// Also update enhanced kanban slice for subtask creation
dispatch(updateEnhancedKanbanSubtask({
sectionId: '',
subtask: data,
mode: 'add'
}));
dispatch(
updateEnhancedKanbanSubtask({
sectionId: '',
subtask: data,
mode: 'add',
})
);
} else {
// Handle regular task creation - transform to Task format and add
const task = {
@@ -563,41 +614,50 @@ export const useTaskSocketHandlers = () => {
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',
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 => a.team_member_id) || [],
assignee_names: data.names || [],
labels: data.labels?.map(l => ({
id: l.id || '',
name: l.name || '',
color: l.color_code || '#1890ff',
end: l.end,
names: l.names
})) || [],
labels:
data.labels?.map(l => ({
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)),
estimated: (data.total_hours || 0) + (data.total_minutes || 0) / 60,
logged: (data.time_spent?.hours || 0) + (data.time_spent?.minutes || 0) / 60,
},
customFields: {},
createdAt: data.created_at || new Date().toISOString(),
updatedAt: data.updated_at || new Date().toISOString(),
order: data.sort_order || 0,
};
// 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;
@@ -608,26 +668,24 @@ export const useTaskSocketHandlers = () => {
// For phase grouping, use phase_id
groupId = data.phase_id;
}
// Use addTaskToGroup with the actual group UUID
dispatch(addTaskToGroup({ task, groupId }));
// Also update enhanced kanban slice for regular task creation
dispatch(addEnhancedKanbanTaskToGroup({
sectionId: groupId || '',
task: data
}));
dispatch(
addEnhancedKanbanTaskToGroup({
sectionId: groupId || '',
task: data,
})
);
}
},
[dispatch]
);
const handleTaskProgressUpdated = useCallback(
(data: {
task_id: string;
progress_value?: number;
weight?: number;
}) => {
(data: { task_id: string; progress_value?: number; weight?: number }) => {
if (!data || !taskGroups) return;
if (data.progress_value !== undefined) {
@@ -651,16 +709,13 @@ export const useTaskSocketHandlers = () => {
);
// 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);
},
[]
);
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(() => {
@@ -678,9 +733,18 @@ export const useTaskSocketHandlers = () => {
{ 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.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 },
];
@@ -714,4 +778,4 @@ export const useTaskSocketHandlers = () => {
handleNewTaskReceived,
handleTaskProgressUpdated,
]);
};
};

View File

@@ -33,14 +33,12 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
const clearTimerInterval = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [taskId]);
const resetTimer = useCallback(() => {
clearTimerInterval();
setTimeString(DEFAULT_TIME_LEFT);
setLocalStarted(false);
@@ -48,15 +46,11 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
// Timer management effect
useEffect(() => {
if (started && localStarted && reduxStartTime) {
clearTimerInterval();
timerTick();
intervalRef.current = setInterval(timerTick, 1000);
} else {
clearTimerInterval();
setTimeString(DEFAULT_TIME_LEFT);
if (started !== localStarted) {
@@ -65,7 +59,6 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
}
return () => {
clearTimerInterval();
};
}, [reduxStartTime, started, localStarted, timerTick, clearTimerInterval, taskId]);
@@ -73,11 +66,9 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
// Initialize timer only on first mount if Redux is unset
useEffect(() => {
if (!hasInitialized.current && initialStartTime && reduxStartTime === undefined) {
dispatch(updateTaskTimeTracking({ taskId, timeTracking: initialStartTime }));
setLocalStarted(true);
} else if (reduxStartTime && !localStarted) {
setLocalStarted(true);
}
hasInitialized.current = true; // Mark as initialized
@@ -87,7 +78,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
if (started || !taskId) return;
try {
const now = Date.now();
dispatch(updateTaskTimeTracking({ taskId, timeTracking: now }));
setLocalStarted(true);
socket?.emit(SocketEvents.TASK_TIMER_START.toString(), JSON.stringify({ task_id: taskId }));
@@ -98,7 +89,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
const handleStopTimer = useCallback(() => {
if (!taskId) return;
resetTimer();
socket?.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId }));
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
@@ -112,7 +103,6 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
try {
const { task_id } = typeof data === 'string' ? JSON.parse(data) : data;
if (task_id === taskId) {
resetTimer();
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
}
@@ -126,7 +116,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
if (task_id === taskId && start_time) {
const time = typeof start_time === 'number' ? start_time : parseInt(start_time);
dispatch(updateTaskTimeTracking({ taskId, timeTracking: time }));
setLocalStarted(true);
}
@@ -150,4 +140,4 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
handleStartTimer,
handleStopTimer,
};
};
};

View File

@@ -25,16 +25,16 @@ export const useTranslationPreloader = (
const loadTranslations = async () => {
try {
setIsLoading(true);
// Ensure translations are loaded
await ensureTranslationsLoaded(namespaces);
// Wait for i18next to be ready
if (!ready) {
// If i18next is not ready, wait a bit and check again
await new Promise(resolve => setTimeout(resolve, 100));
}
if (isMounted) {
setIsLoaded(true);
setIsLoading(false);
@@ -74,4 +74,4 @@ export const useBulkActionTranslations = () => {
*/
export const useTaskManagementTranslations = () => {
return useTranslationPreloader(['task-management', 'tasks/task-table-bulk-actions']);
};
};