feat(task-management): add task phase and priority dropdown components

- Introduced `TaskPhaseDropdown` and `TaskPriorityDropdown` components for managing task phases and priorities within the task management interface.
- Integrated these components into the `TaskRow` to enhance user interaction and streamline task updates.
- Updated socket handlers to handle phase and priority changes, ensuring real-time updates and improved task organization.
- Enhanced dropdown functionality with animations and improved accessibility features.
This commit is contained in:
chamiakJ
2025-06-27 07:28:47 +05:30
parent e73196a249
commit 9a254105fb
4 changed files with 732 additions and 97 deletions

View File

@@ -0,0 +1,298 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { Task } from '@/types/task-management.types';
import { ClearOutlined } from '@ant-design/icons';
interface TaskPhaseDropdownProps {
task: Task;
projectId: string;
isDarkMode?: boolean;
}
const TaskPhaseDropdown: React.FC<TaskPhaseDropdownProps> = ({
task,
projectId,
isDarkMode = false
}) => {
const { socket, connected } = useSocket();
const [isOpen, setIsOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const { phaseList } = useAppSelector(state => state.phaseReducer);
// Find current phase details
const currentPhase = useMemo(() => {
return phaseList.find(phase => phase.name === task.phase);
}, [phaseList, task.phase]);
// Handle phase change
const handlePhaseChange = useCallback((phaseId: string, phaseName: string) => {
if (!task.id || !phaseId || !connected) return;
console.log('🎯 Phase change initiated:', { taskId: task.id, phaseId, phaseName });
socket?.emit(
SocketEvents.TASK_PHASE_CHANGE.toString(),
{
task_id: task.id,
phase_id: phaseId,
parent_task: null, // Assuming top-level tasks for now
}
);
setIsOpen(false);
}, [task.id, connected, socket]);
// Handle phase clear
const handlePhaseClear = useCallback(() => {
if (!task.id || !connected) return;
console.log('🎯 Phase clear initiated:', { taskId: task.id });
socket?.emit(
SocketEvents.TASK_PHASE_CHANGE.toString(),
{
task_id: task.id,
phase_id: null,
parent_task: null,
}
);
setIsOpen(false);
}, [task.id, connected, socket]);
// Calculate dropdown position and handle outside clicks
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
return; // Don't close if clicking the button
}
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen && buttonRef.current) {
// Calculate position
const rect = buttonRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + window.scrollY + 4,
left: rect.left + window.scrollX,
});
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// Get phase color
const getPhaseColor = useCallback((phase: any) => {
return phase?.color_code || '#722ed1';
}, []);
// Format phase name for display
const formatPhaseName = useCallback((name: string) => {
if (!name) return 'Select';
return name;
}, []);
// Determine if no phase is selected
const hasPhase = task.phase && task.phase.trim() !== '';
return (
<>
{/* Phase Button - Show "Select" when no phase */}
<button
ref={buttonRef}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
}}
className={`
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] max-w-full justify-center
whitespace-nowrap
`}
style={{
backgroundColor: hasPhase && currentPhase
? getPhaseColor(currentPhase)
: (isDarkMode ? '#4b5563' : '#9ca3af'),
color: 'white',
}}
>
<span className="truncate">
{hasPhase && currentPhase ? formatPhaseName(currentPhase.name || '') : 'Select'}
</span>
<svg
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown Menu */}
{isOpen && createPortal(
<div
ref={dropdownRef}
className={`
fixed min-w-[160px] max-w-[220px]
rounded border backdrop-blur-sm z-[9999]
${isDarkMode
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
}
`}
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
zIndex: 9999,
animation: 'fadeInScale 0.15s ease-out',
}}
>
{/* Phase Options */}
<div className="py-1 max-h-64 overflow-y-auto">
{/* No Phase Option */}
<button
onClick={handlePhaseClear}
className={`
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
${isDarkMode
? 'hover:bg-gray-700/80 text-gray-100'
: 'hover:bg-gray-50/70 text-gray-900'
}
${!hasPhase
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
: ''
}
`}
style={{
animation: 'slideInFromLeft 0.2s ease-out forwards',
}}
>
{/* Clear Icon */}
<div className="flex items-center justify-center w-4 h-4">
<ClearOutlined className="w-3 h-3" />
</div>
{/* No Phase Color Indicator */}
<div
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
}`}
style={{ backgroundColor: isDarkMode ? '#4b5563' : '#9ca3af' }}
/>
{/* No Phase Text */}
<span className="flex-1 truncate">No Phase</span>
{/* Current Selection Badge */}
{!hasPhase && (
<div className="flex items-center gap-1">
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
Current
</span>
</div>
)}
</button>
{/* Phase Options */}
{phaseList.map((phase, index) => {
const isSelected = phase.name === task.phase;
return (
<button
key={phase.id}
onClick={() => handlePhaseChange(phase.id!, phase.name!)}
className={`
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
${isDarkMode
? 'hover:bg-gray-700/80 text-gray-100'
: 'hover:bg-gray-50/70 text-gray-900'
}
${isSelected
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
: ''
}
`}
style={{
animationDelay: `${(index + 1) * 30}ms`,
animation: 'slideInFromLeft 0.2s ease-out forwards',
}}
>
{/* Phase Color Indicator */}
<div
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
}`}
style={{ backgroundColor: getPhaseColor(phase) }}
/>
{/* Phase Name */}
<span className="flex-1 truncate">
{formatPhaseName(phase.name || '')}
</span>
{/* Current Phase Badge */}
{isSelected && (
<div className="flex items-center gap-1">
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
Current
</span>
</div>
)}
</button>
);
})}
</div>
</div>,
document.body
)}
{/* CSS Animations */}
{isOpen && createPortal(
<style>
{`
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95) translateY(-5px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`}
</style>,
document.head
)}
</>
);
};
export default TaskPhaseDropdown;

View File

@@ -0,0 +1,257 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { Task } from '@/types/task-management.types';
import { MinusOutlined, PauseOutlined, DoubleRightOutlined } from '@ant-design/icons';
interface TaskPriorityDropdownProps {
task: Task;
projectId: string;
isDarkMode?: boolean;
}
const TaskPriorityDropdown: React.FC<TaskPriorityDropdownProps> = ({
task,
projectId,
isDarkMode = false
}) => {
const { socket, connected } = useSocket();
const [isOpen, setIsOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
// Find current priority details
const currentPriority = useMemo(() => {
return priorityList.find(priority =>
priority.name?.toLowerCase() === task.priority?.toLowerCase() ||
priority.id === task.priority
);
}, [priorityList, task.priority]);
// Handle priority change
const handlePriorityChange = useCallback((priorityId: string, priorityName: string) => {
if (!task.id || !priorityId || !connected) return;
console.log('🎯 Priority change initiated:', { taskId: task.id, priorityId, priorityName });
socket?.emit(
SocketEvents.TASK_PRIORITY_CHANGE.toString(),
JSON.stringify({
task_id: task.id,
priority_id: priorityId,
team_id: projectId, // Using projectId as teamId
})
);
setIsOpen(false);
}, [task.id, connected, socket, projectId]);
// Calculate dropdown position and handle outside clicks
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
return; // Don't close if clicking the button
}
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen && buttonRef.current) {
// Calculate position
const rect = buttonRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + window.scrollY + 4,
left: rect.left + window.scrollX,
});
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// Get priority color
const getPriorityColor = useCallback((priority: any) => {
if (isDarkMode) {
return priority?.color_code_dark || priority?.color_code || '#4b5563';
}
return priority?.color_code || '#6b7280';
}, [isDarkMode]);
// Get priority icon
const getPriorityIcon = useCallback((priorityName: string) => {
const name = priorityName?.toLowerCase();
switch (name) {
case 'low':
return <MinusOutlined className="w-3 h-3" />;
case 'medium':
return <PauseOutlined className="w-3 h-3" style={{ transform: 'rotate(90deg)' }} />;
case 'high':
return <DoubleRightOutlined className="w-3 h-3" style={{ transform: 'rotate(90deg)' }} />;
default:
return <MinusOutlined className="w-3 h-3" />;
}
}, []);
// Format priority name for display
const formatPriorityName = useCallback((name: string) => {
if (!name) return name;
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
}, []);
if (!task.priority) return null;
return (
<>
{/* Priority Button - Simple text display like status */}
<button
ref={buttonRef}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
}}
className={`
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] max-w-full justify-center
whitespace-nowrap
`}
style={{
backgroundColor: currentPriority ? getPriorityColor(currentPriority) : (isDarkMode ? '#4b5563' : '#9ca3af'),
color: 'white',
}}
>
<span className="truncate">
{currentPriority ? formatPriorityName(currentPriority.name || '') : formatPriorityName(task.priority)}
</span>
<svg
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown Menu */}
{isOpen && createPortal(
<div
ref={dropdownRef}
className={`
fixed min-w-[160px] max-w-[220px]
rounded border backdrop-blur-sm z-[9999]
${isDarkMode
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
}
`}
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
zIndex: 9999,
animation: 'fadeInScale 0.15s ease-out',
}}
>
{/* Priority Options */}
<div className="py-1 max-h-64 overflow-y-auto">
{priorityList.map((priority, index) => {
const isSelected = priority.name?.toLowerCase() === task.priority?.toLowerCase() || priority.id === task.priority;
return (
<button
key={priority.id}
onClick={() => handlePriorityChange(priority.id!, priority.name!)}
className={`
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
${isDarkMode
? 'hover:bg-gray-700/80 text-gray-100'
: 'hover:bg-gray-50/70 text-gray-900'
}
${isSelected
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
: ''
}
`}
style={{
animationDelay: `${index * 30}ms`,
animation: 'slideInFromLeft 0.2s ease-out forwards',
}}
>
{/* Priority Icon */}
<div className="flex items-center justify-center w-4 h-4">
{getPriorityIcon(priority.name || '')}
</div>
{/* Priority Color Indicator */}
<div
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
}`}
style={{ backgroundColor: getPriorityColor(priority) }}
/>
{/* Priority Name */}
<span className="flex-1 truncate">
{formatPriorityName(priority.name || '')}
</span>
{/* Current Priority Badge */}
{isSelected && (
<div className="flex items-center gap-1">
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
Current
</span>
</div>
)}
</button>
);
})}
</div>
</div>,
document.body
)}
{/* CSS Animations */}
{isOpen && createPortal(
<style>
{`
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95) translateY(-5px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`}
</style>,
document.head
)}
</>
);
};
export default TaskPriorityDropdown;

View File

@@ -22,6 +22,8 @@ import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLa
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import TaskStatusDropdown from './task-status-dropdown';
import TaskPriorityDropdown from './task-priority-dropdown';
import TaskPhaseDropdown from './task-phase-dropdown';
import {
formatDate as utilFormatDate,
formatDateTime as utilFormatDateTime,
@@ -419,6 +421,24 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
</div>
);
case 'priority':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
{task.priority || 'Medium'}
</div>
</div>
);
case 'phase':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
{task.phase || 'No Phase'}
</div>
</div>
);
default:
// For non-essential columns, show placeholder during initial load
return (
@@ -580,10 +600,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
case 'phase':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
{task.phase || 'No Phase'}
</span>
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width, minWidth: col.width }}>
<div className="w-full">
<TaskPhaseDropdown
task={task}
projectId={projectId}
isDarkMode={isDarkMode}
/>
</div>
</div>
);
@@ -602,8 +626,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
case 'priority':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<TaskPriority priority={task.priority} isDarkMode={isDarkMode} />
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width, minWidth: col.width }}>
<div className="w-full">
<TaskPriorityDropdown
task={task}
projectId={projectId}
isDarkMode={isDarkMode}
/>
</div>
</div>
);

View File

@@ -240,65 +240,81 @@ export const useTaskSocketHandlers = () => {
(response: ITaskListPriorityChangeResponse) => {
if (!response) return;
console.log('🎯 Priority change received:', response);
// Update the old task slice (for backward compatibility)
dispatch(updateTaskPriority(response));
dispatch(setTaskPriority(response));
dispatch(deselectAll());
// For the task management slice, move task between groups if grouping by priority
// For the task management slice, always update the task entity first
const state = store.getState();
const groups = state.taskManagement.groups;
const currentTask = state.taskManagement.entities[response.id];
const currentGrouping = state.taskManagement.grouping;
if (groups && groups.length > 0 && currentTask && response.priority_id && currentGrouping === 'priority') {
// Find current group containing the task
const currentGroup = groups.find(group => group.taskIds.includes(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);
// Find target group based on new priority ID
const targetGroup = groups.find(group => group.id === response.priority_id);
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
console.log('🔄 Moving task between priority groups:', {
taskId: response.id,
fromGroup: currentGroup.title,
toGroup: targetGroup.title
});
// Determine priority value from target group
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
const priorityValue = targetGroup.groupValue.toLowerCase();
if (['critical', 'high', 'medium', 'low'].includes(priorityValue)) {
newPriorityValue = priorityValue as 'critical' | 'high' | 'medium' | 'low';
}
dispatch(moveTaskBetweenGroups({
taskId: response.id,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
priority: newPriorityValue,
}
}));
} else if (!currentGroup || !targetGroup) {
// Fallback to refetch if groups not found
console.log('🔄 Priority groups not found, refetching tasks...');
if (projectId) {
dispatch(fetchTasksV3(projectId));
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';
}
}
} else if (currentGrouping !== 'priority') {
// If not grouping by priority, just update the task
if (currentTask) {
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
// We need to map priority_id to priority value - this might require additional logic
// For now, let's just update without changing groups
dispatch(updateTask({
id: response.id,
changes: {
// priority: newPriorityValue, // We'd need to map priority_id to value
}
}));
console.log('🔧 Updating task priority:', {
taskId: response.id,
oldPriority: currentTask.priority,
newPriority: newPriorityValue,
priorityId: response.priority_id,
currentGrouping: state.taskManagement.grouping
});
// Update the task entity
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()
);
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
console.log('🔄 Moving task between priority groups:', {
taskId: response.id,
fromGroup: currentGroup.title,
toGroup: targetGroup.title,
newPriority: 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');
}
} else {
console.log('🔧 Not grouped by priority, skipping group movement');
}
}
},
@@ -349,61 +365,95 @@ export const useTaskSocketHandlers = () => {
(data: ITaskPhaseChangeResponse) => {
if (!data) return;
console.log('🎯 Phase change received:', data);
// Update the old task slice (for backward compatibility)
dispatch(updateTaskPhase(data));
dispatch(deselectAll());
// For the task management slice, move task between groups if grouping by phase
// For the task management slice, always update the task entity first
const state = store.getState();
const groups = state.taskManagement.groups;
const currentTask = state.taskManagement.entities[data.task_id || data.id];
const currentGrouping = state.taskManagement.grouping;
const taskId = data.task_id || data.id;
const taskId = data.task_id;
if (groups && groups.length > 0 && currentTask && taskId && currentGrouping === 'phase') {
// Find current group containing the task
const currentGroup = groups.find(group => group.taskIds.includes(taskId));
if (taskId) {
const currentTask = state.taskManagement.entities[taskId];
// For phase changes, we need to find the target group by phase name/value
// The response might not have a direct phase_id, so we'll look for the group by value
let targetGroup: any = null;
if (currentTask) {
// Get phase list to map phase_id to phase name
const phaseList = state.phaseReducer?.phaseList || [];
let newPhaseValue = '';
// Try to find target group - this might need adjustment based on the actual response structure
if (data.id) {
targetGroup = groups.find(group => group.id === 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 = '';
}
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
console.log('🔄 Moving task between phase groups:', {
taskId: taskId,
fromGroup: currentGroup.title,
toGroup: targetGroup.title
});
dispatch(moveTaskBetweenGroups({
taskId: taskId,
fromGroupId: currentGroup.id,
toGroupId: targetGroup.id,
taskUpdate: {
phase: targetGroup.groupValue,
}
}));
} else if (!currentGroup || !targetGroup) {
// Fallback to refetch if groups not found
console.log('🔄 Phase groups not found, refetching tasks...');
if (projectId) {
dispatch(fetchTasksV3(projectId));
console.log('🔧 Updating task phase:', {
taskId: taskId,
oldPhase: currentTask.phase,
newPhase: newPhaseValue,
phaseId: data.id,
currentGrouping: state.taskManagement.grouping
});
// 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) {
console.log('🔄 Moving task between phase groups:', {
taskId: taskId,
fromGroup: currentGroup.title,
toGroup: targetGroup.title,
newPhase: newPhaseValue || 'No Phase'
});
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');
}
} else if (currentGrouping !== 'phase') {
// If not grouping by phase, just update the task
if (currentTask && taskId) {
dispatch(updateTask({
id: taskId,
changes: {
// phase: newPhaseValue, // We'd need to determine the phase value
}
}));
}
}
},