Files
worklenz/worklenz-frontend/src/features/task-management/task-management.slice.ts
chamikaJ 7e6d7d8580 feat(task-management): enhance task date handling and UI components in TaskListV2
- Added startDate and dueDate fields to task data structure for improved date management.
- Updated TaskRow to include date pickers for start and due dates with clear functionality.
- Enhanced LabelsSelector to support dynamic label rendering and improved visual feedback.
- Refactored AssigneeSelector and CustomColordLabel components for better integration with task data.
- Improved dropdown positioning logic in LabelsSelector for better user experience.
- Added translations for new date-related UI elements in multiple languages.
2025-07-04 10:29:51 +05:30

834 lines
29 KiB
TypeScript

import {
createSlice,
createEntityAdapter,
PayloadAction,
createAsyncThunk,
EntityState,
EntityId,
} from '@reduxjs/toolkit';
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import {
tasksApiService,
ITaskListConfigV2,
ITaskListV3Response,
} from '@/api/tasks/tasks.api.service';
import logger from '@/utils/errorLogger';
import { DEFAULT_TASK_NAME } from '@/shared/constants';
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
// Helper function to safely convert time values
const convertTimeValue = (value: any): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const parsed = parseFloat(value);
return isNaN(parsed) ? 0 : parsed;
}
if (typeof value === 'object' && value !== null) {
// Handle time objects like {hours: 2, minutes: 30}
if ('hours' in value || 'minutes' in value) {
const hours = Number(value.hours || 0);
const minutes = Number(value.minutes || 0);
return hours + minutes / 60;
}
}
return 0;
};
export enum IGroupBy {
STATUS = 'status',
PRIORITY = 'priority',
PHASE = 'phase',
MEMBERS = 'members',
}
// Entity adapter for normalized state
const tasksAdapter = createEntityAdapter<Task>();
// Get the initial state from the adapter
const initialState: TaskManagementState = {
ids: [],
entities: {},
loading: false,
error: null,
groups: [],
grouping: undefined,
selectedPriorities: [],
search: '',
};
// Async thunk to fetch tasks from API
export const fetchTasks = createAsyncThunk(
'taskManagement/fetchTasks',
async (projectId: string, { rejectWithValue, getState }) => {
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
group: currentGrouping || '',
field: '',
order: '',
search: '',
statuses: '',
members: '',
projects: '',
isSubtasksInclude: false,
labels: '',
priorities: '',
};
const response = await tasksApiService.getTaskList(config);
// Helper function to safely convert time values
const convertTimeValue = (value: any): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const parsed = parseFloat(value);
return isNaN(parsed) ? 0 : parsed;
}
if (typeof value === 'object' && value !== null) {
// Handle time objects like {hours: 2, minutes: 30}
if ('hours' in value || 'minutes' in value) {
const hours = Number(value.hours || 0);
const minutes = Number(value.minutes || 0);
return hours + minutes / 60;
}
}
return 0;
};
// Create a mapping from status IDs to group names
const statusIdToNameMap: Record<string, string> = {};
const priorityIdToNameMap: Record<string, string> = {};
response.body.forEach((group: any) => {
statusIdToNameMap[group.id] = group.name.toLowerCase();
});
// For priority mapping, we need to get priority names from the tasks themselves
// Since the API doesn't provide priority names in the group structure
response.body.forEach((group: any) => {
group.tasks.forEach((task: any) => {
// Map priority value to name (this is an assumption based on common patterns)
if (task.priority_value !== undefined) {
switch (task.priority_value) {
case 0:
priorityIdToNameMap[task.priority] = 'low';
break;
case 1:
priorityIdToNameMap[task.priority] = 'medium';
break;
case 2:
priorityIdToNameMap[task.priority] = 'high';
break;
case 3:
priorityIdToNameMap[task.priority] = 'critical';
break;
default:
priorityIdToNameMap[task.priority] = 'medium';
}
}
});
});
// Transform the API response to our Task type
const tasks: Task[] = response.body.flatMap((group: any) =>
group.tasks.map((task: any) => ({
id: task.id,
task_key: task.task_key || '',
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
description: task.description || '',
status: statusIdToNameMap[task.status] || 'todo',
priority: priorityIdToNameMap[task.priority] || 'medium',
phase: task.phase_name || 'Development',
progress: typeof task.complete_ratio === 'number' ? task.complete_ratio : 0,
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
assignee_names: task.assignee_names || task.names || [],
labels:
task.labels?.map((l: any) => ({
id: l.id || l.label_id,
name: l.name,
color: l.color || '#1890ff',
end: l.end,
names: l.names,
})) || [],
dueDate: task.dueDate,
startDate: task.startDate,
timeTracking: {
estimated: convertTimeValue(task.total_time),
logged: convertTimeValue(task.time_spent),
},
customFields: {},
createdAt: task.created_at || new Date().toISOString(),
updatedAt: task.updated_at || new Date().toISOString(),
created_at: task.created_at || new Date().toISOString(),
updated_at: task.updated_at || new Date().toISOString(),
order: typeof task.sort_order === 'number' ? task.sort_order : 0,
// Ensure all Task properties are mapped, even if undefined in API response
sub_tasks: task.sub_tasks || [],
sub_tasks_count: task.sub_tasks_count || 0,
show_sub_tasks: task.show_sub_tasks || false,
parent_task_id: task.parent_task_id || undefined,
weight: task.weight || 0,
color: task.color || undefined,
statusColor: task.statusColor || undefined,
priorityColor: task.priorityColor || undefined,
comments_count: task.comments_count || 0,
attachments_count: task.attachments_count || 0,
has_dependencies: task.has_dependencies || false,
schedule_id: task.schedule_id || null,
}))
);
return tasks;
} catch (error) {
logger.error('Fetch Tasks', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch tasks');
}
}
);
// New V3 fetch that minimizes frontend processing
export const fetchTasksV3 = createAsyncThunk(
'taskManagement/fetchTasksV3',
async (projectId: string, { rejectWithValue, getState }) => {
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
// Get selected labels from taskReducer
const selectedLabels = state.taskReducer.labels
.filter((l: any) => l.selected && l.id)
.map((l: any) => l.id)
.join(' ');
// Get selected assignees from taskReducer
const selectedAssignees = state.taskReducer.taskAssignees
.filter((m: any) => m.selected && m.id)
.map((m: any) => m.id)
.join(' ');
// Get selected priorities from taskReducer
const selectedPriorities = state.taskReducer.priorities.join(' ');
// Get search value from taskReducer
const searchValue = state.taskReducer.search || '';
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
group: currentGrouping || '',
field: '',
order: '',
search: searchValue,
statuses: '',
members: selectedAssignees,
projects: '',
isSubtasksInclude: false,
labels: selectedLabels,
priorities: selectedPriorities,
};
const response = await tasksApiService.getTaskListV3(config);
// Ensure tasks are properly normalized
const tasks: Task[] = response.body.allTasks.map((task: any) => {
const now = new Date().toISOString();
const transformedTask = {
id: task.id,
task_key: task.task_key || task.key || '',
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
description: task.description || '',
status: task.status || 'todo',
priority: task.priority || 'medium',
phase: task.phase || 'Development',
progress: typeof task.complete_ratio === 'number' ? task.complete_ratio : 0,
assignees: task.assignees?.map((a: { team_member_id: string }) => a.team_member_id) || [],
assignee_names: task.assignee_names || task.names || [],
labels: task.labels?.map((l: { id: string; label_id: string; name: string; color_code: string; end: boolean; names: string[] }) => ({
id: l.id || l.label_id,
name: l.name,
color: l.color || '#1890ff',
end: l.end,
names: l.names,
})) || [],
dueDate: task.dueDate,
startDate: task.startDate,
timeTracking: {
estimated: convertTimeValue(task.total_time),
logged: convertTimeValue(task.time_spent),
},
customFields: {},
createdAt: task.created_at || now,
updatedAt: task.updated_at || now,
created_at: task.created_at || now,
updated_at: task.updated_at || now,
order: typeof task.sort_order === 'number' ? task.sort_order : 0,
sub_tasks: task.sub_tasks || [],
sub_tasks_count: task.sub_tasks_count || 0,
show_sub_tasks: task.show_sub_tasks || false,
parent_task_id: task.parent_task_id || undefined,
weight: task.weight || 0,
color: task.color || undefined,
statusColor: task.statusColor || undefined,
priorityColor: task.priorityColor || undefined,
comments_count: task.comments_count || 0,
attachments_count: task.attachments_count || 0,
has_dependencies: task.has_dependencies || false,
schedule_id: task.schedule_id || null,
};
return transformedTask;
});
return {
allTasks: tasks,
groups: response.body.groups,
grouping: response.body.grouping,
totalTasks: response.body.totalTasks,
};
} catch (error) {
logger.error('Fetch Tasks V3', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch tasks V3');
}
}
);
// Refresh task progress separately to avoid slowing down initial load
export const fetchSubTasks = createAsyncThunk(
'taskManagement/fetchSubTasks',
async (
{ taskId, projectId }: { taskId: string; projectId: string },
{ rejectWithValue, getState }
) => {
try {
const state = getState() as RootState;
const currentGrouping = state.grouping.currentGrouping;
const config: ITaskListConfigV2 = {
id: projectId,
archived: false,
group: currentGrouping || '',
field: '',
order: '',
search: '',
statuses: '',
members: '',
projects: '',
isSubtasksInclude: false,
labels: '',
priorities: '',
parent_task: taskId,
};
const response = await tasksApiService.getTaskListV3(config);
return { parentTaskId: taskId, subtasks: response.body.allTasks };
} 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 refreshTaskProgress = createAsyncThunk(
'taskManagement/refreshTaskProgress',
async (projectId: string, { rejectWithValue }) => {
try {
const response = await tasksApiService.refreshTaskProgress(projectId);
return response.body;
} catch (error) {
logger.error('Refresh Task Progress', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to refresh task progress');
}
}
);
// Async thunk to reorder tasks with API call
export const reorderTasksWithAPI = createAsyncThunk(
'taskManagement/reorderTasksWithAPI',
async (
{ taskIds, newOrder, projectId }: { taskIds: string[]; newOrder: number[]; projectId: string },
{ rejectWithValue }
) => {
try {
// Make API call to update task order
const response = await tasksApiService.reorderTasks({
taskIds,
newOrder,
projectId,
});
if (response.done) {
return { taskIds, newOrder };
} else {
return rejectWithValue('Failed to reorder tasks');
}
} catch (error) {
logger.error('Reorder Tasks API Error:', error);
return rejectWithValue('Failed to reorder tasks');
}
}
);
// Async thunk to move task between groups with API call
export const moveTaskToGroupWithAPI = createAsyncThunk(
'taskManagement/moveTaskToGroupWithAPI',
async (
{
taskId,
groupType,
groupValue,
projectId,
}: {
taskId: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
projectId: string;
},
{ rejectWithValue }
) => {
try {
// Make API call to update task group
const response = await tasksApiService.updateTaskGroup({
taskId,
groupType,
groupValue,
projectId,
});
if (response.done) {
return { taskId, groupType, groupValue };
} else {
return rejectWithValue('Failed to move task');
}
} catch (error) {
logger.error('Move Task API Error:', error);
return rejectWithValue('Failed to move task');
}
}
);
// Add action to update task with subtasks
export const updateTaskWithSubtasks = createAsyncThunk(
'taskManagement/updateTaskWithSubtasks',
async ({ taskId, subtasks }: { taskId: string; subtasks: any[] }, { getState }) => {
return { taskId, subtasks };
}
);
// Create the slice
const taskManagementSlice = createSlice({
name: 'taskManagement',
initialState,
reducers: {
setTasks: (state, action: PayloadAction<Task[]>) => {
const tasks = action.payload;
state.ids = tasks.map(task => task.id);
state.entities = tasks.reduce((acc, task) => {
acc[task.id] = task;
return acc;
}, {} as Record<string, Task>);
},
addTask: (state, action: PayloadAction<Task>) => {
const task = action.payload;
state.ids.push(task.id);
state.entities[task.id] = task;
},
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
const { task, groupId } = action.payload;
state.ids.push(task.id);
state.entities[task.id] = task;
const group = state.groups.find(g => g.id === groupId);
if (group) {
group.taskIds.push(task.id);
}
},
updateTask: (state, action: PayloadAction<Task>) => {
tasksAdapter.upsertOne(state as EntityState<Task, string>, action.payload);
// Additionally, update the task within its group if necessary (e.g., if status changed)
const updatedTask = action.payload;
const oldTask = state.entities[updatedTask.id];
if (oldTask && state.grouping?.id === IGroupBy.STATUS && oldTask.status !== updatedTask.status) {
// Remove from old status group
const oldGroup = state.groups.find(group => group.id === oldTask.status);
if (oldGroup) {
oldGroup.taskIds = oldGroup.taskIds.filter(id => id !== updatedTask.id);
}
// Add to new status group
const newGroup = state.groups.find(group => group.id === updatedTask.status);
if (newGroup) {
newGroup.taskIds.push(updatedTask.id);
}
}
},
deleteTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
delete state.entities[taskId];
state.ids = state.ids.filter(id => id !== taskId);
state.groups = state.groups.map(group => ({
...group,
taskIds: group.taskIds.filter(id => id !== taskId),
}));
},
bulkUpdateTasks: (state, action: PayloadAction<Task[]>) => {
action.payload.forEach(task => {
state.entities[task.id] = task;
});
},
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
const taskIds = action.payload;
taskIds.forEach(taskId => {
delete state.entities[taskId];
});
state.ids = state.ids.filter(id => !taskIds.includes(id));
state.groups = state.groups.map(group => ({
...group,
taskIds: group.taskIds.filter(id => !taskIds.includes(id)),
}));
},
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; groupId: string }>) => {
const { taskIds, groupId } = action.payload;
const group = state.groups.find(g => g.id === groupId);
if (group) {
group.taskIds = taskIds;
}
},
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupId: string }>) => {
const { taskId, groupId } = action.payload;
state.groups = state.groups.map(group => ({
...group,
taskIds:
group.id === groupId
? [...group.taskIds, taskId]
: group.taskIds.filter(id => id !== taskId),
}));
},
moveTaskBetweenGroups: (
state,
action: PayloadAction<{
taskId: string;
sourceGroupId: string;
targetGroupId: string;
}>
) => {
const { taskId, sourceGroupId, targetGroupId } = action.payload;
state.groups = state.groups.map(group => ({
...group,
taskIds:
group.id === targetGroupId
? [...group.taskIds, taskId]
: group.id === sourceGroupId
? group.taskIds.filter(id => id !== taskId)
: group.taskIds,
}));
},
optimisticTaskMove: (
state,
action: PayloadAction<{
taskId: string;
sourceGroupId: string;
targetGroupId: string;
}>
) => {
const { taskId, sourceGroupId, targetGroupId } = action.payload;
state.groups = state.groups.map(group => ({
...group,
taskIds:
group.id === targetGroupId
? [...group.taskIds, taskId]
: group.id === sourceGroupId
? group.taskIds.filter(id => id !== taskId)
: group.taskIds,
}));
},
reorderTasksInGroup: (
state,
action: PayloadAction<{
sourceTaskId: string;
destinationTaskId: string;
sourceGroupId: string;
destinationGroupId: string;
}>
) => {
const { sourceTaskId, destinationTaskId, sourceGroupId, destinationGroupId } = action.payload;
// Get a mutable copy of entities for updates
const newEntities = { ...state.entities };
const sourceTask = newEntities[sourceTaskId];
const destinationTask = newEntities[destinationTaskId];
if (!sourceTask || !destinationTask) return;
if (sourceGroupId === destinationGroupId) {
// Reordering within the same group
const group = state.groups.find(g => g.id === sourceGroupId);
if (group) {
const newTasks = Array.from(group.taskIds);
const [removed] = newTasks.splice(newTasks.indexOf(sourceTaskId), 1);
newTasks.splice(newTasks.indexOf(destinationTaskId), 0, removed);
group.taskIds = newTasks;
// Update order for affected tasks. Assuming simple reordering affects order.
// This might need more sophisticated logic based on how `order` is used.
newTasks.forEach((id, index) => {
if (newEntities[id]) {
newEntities[id] = { ...newEntities[id], order: index };
}
});
}
} else {
// Moving between different groups
const sourceGroup = state.groups.find(g => g.id === sourceGroupId);
const destinationGroup = state.groups.find(g => g.id === destinationGroupId);
if (sourceGroup && destinationGroup) {
// Remove from source group
sourceGroup.taskIds = sourceGroup.taskIds.filter(id => id !== sourceTaskId);
// Add to destination group at the correct position relative to destinationTask
const destinationIndex = destinationGroup.taskIds.indexOf(destinationTaskId);
if (destinationIndex !== -1) {
destinationGroup.taskIds.splice(destinationIndex, 0, sourceTaskId);
} else {
destinationGroup.taskIds.push(sourceTaskId); // Add to end if destination task not found
}
// Update task's grouping field to reflect new group (e.g., status, priority, phase)
// This assumes the group ID directly corresponds to the task's field value
if (sourceTask) {
let updatedTask = { ...sourceTask };
switch (state.grouping?.id) {
case IGroupBy.STATUS:
updatedTask.status = destinationGroup.id;
break;
case IGroupBy.PRIORITY:
updatedTask.priority = destinationGroup.id;
break;
case IGroupBy.PHASE:
updatedTask.phase = destinationGroup.id;
break;
case IGroupBy.MEMBERS:
// If moving to a member group, ensure task is assigned to that member
// This assumes the group ID is the member ID
if (!updatedTask.assignees) {
updatedTask.assignees = [];
}
if (!updatedTask.assignees.includes(destinationGroup.id)) {
updatedTask.assignees.push(destinationGroup.id);
}
// If moving from a member group, and the task is no longer in any member group,
// consider removing the assignment (more complex logic might be needed here)
break;
default:
break;
}
newEntities[sourceTaskId] = updatedTask;
}
// Update order for affected tasks in both groups if necessary
sourceGroup.taskIds.forEach((id, index) => {
if (newEntities[id]) newEntities[id] = { ...newEntities[id], order: index };
});
destinationGroup.taskIds.forEach((id, index) => {
if (newEntities[id]) newEntities[id] = { ...newEntities[id], order: index };
});
}
}
// Update the state's entities after all modifications
state.entities = newEntities;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
state.selectedPriorities = action.payload;
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload;
},
resetTaskManagement: state => {
state.loading = false;
state.error = null;
state.groups = [];
state.grouping = undefined;
state.selectedPriorities = [];
state.search = '';
state.ids = [];
state.entities = {};
},
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
const task = state.entities[action.payload];
if (task) {
task.show_sub_tasks = !task.show_sub_tasks;
}
},
addSubtaskToParent: (
state,
action: PayloadAction<{ parentId: string; subtask: Task }>
) => {
const { parentId, subtask } = action.payload;
const parent = state.entities[parentId];
if (parent) {
state.ids.push(subtask.id);
state.entities[subtask.id] = subtask;
if (!parent.sub_tasks) {
parent.sub_tasks = [];
}
parent.sub_tasks.push(subtask);
parent.sub_tasks_count = (parent.sub_tasks_count || 0) + 1;
}
},
updateTaskAssignees: (state, action: PayloadAction<{
taskId: string;
assigneeIds: string[];
assigneeNames: InlineMember[];
}>) => {
const { taskId, assigneeIds, assigneeNames } = action.payload;
const existingTask = state.entities[taskId];
if (existingTask) {
state.entities[taskId] = {
...existingTask,
assignees: assigneeIds,
assignee_names: assigneeNames,
};
}
},
},
extraReducers: builder => {
builder
.addCase(fetchTasksV3.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(fetchTasksV3.fulfilled, (state, action) => {
state.loading = false;
const { allTasks, groups, grouping } = action.payload;
tasksAdapter.setAll(state as EntityState<Task, string>, allTasks || []); // Ensure allTasks is an array
state.ids = (allTasks || []).map(task => task.id); // Also update ids
state.groups = groups;
state.grouping = grouping;
})
.addCase(fetchTasksV3.rejected, (state, action) => {
state.loading = false;
state.error = action.error?.message || (action.payload as string) || 'Failed to load tasks (V3)';
state.ids = [];
state.entities = {};
state.groups = [];
})
.addCase(fetchSubTasks.pending, (state, action) => {
// Don't set global loading state for subtasks
state.error = null;
})
.addCase(fetchSubTasks.fulfilled, (state, action) => {
const { parentTaskId, subtasks } = action.payload;
const parentTask = state.entities[parentTaskId];
if (parentTask) {
parentTask.sub_tasks = subtasks;
parentTask.sub_tasks_count = subtasks.length;
parentTask.show_sub_tasks = true;
}
})
.addCase(fetchSubTasks.rejected, (state, action) => {
// Set error but don't clear task data
state.error = action.error.message || action.payload || 'Failed to fetch subtasks. Please try again.';
})
.addCase(fetchTasks.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTasks.fulfilled, (state, action) => {
state.loading = false;
tasksAdapter.setAll(state as EntityState<Task, string>, action.payload || []); // Ensure payload is an array
state.ids = (action.payload || []).map(task => task.id); // Also update ids
state.groups = []; // Assuming no groups when using old fetchTasks
state.grouping = undefined; // Assuming no grouping when using old fetchTasks
})
.addCase(fetchTasks.rejected, (state, action) => {
state.loading = false;
state.error = action.error?.message || (action.payload as string) || 'Failed to load tasks';
state.ids = [];
state.entities = {};
state.groups = [];
});
},
});
// Export the slice reducer and actions
export const {
setTasks,
addTask,
addTaskToGroup,
updateTask,
deleteTask,
bulkUpdateTasks,
bulkDeleteTasks,
reorderTasks,
moveTaskToGroup,
moveTaskBetweenGroups,
optimisticTaskMove,
reorderTasksInGroup,
setLoading,
setError,
setSelectedPriorities,
setSearch,
resetTaskManagement,
toggleTaskExpansion,
addSubtaskToParent,
updateTaskAssignees,
} = taskManagementSlice.actions;
// Export the selectors
export const selectAllTasks = (state: RootState) => state.taskManagement.entities;
export const selectAllTasksArray = (state: RootState) => Object.values(state.taskManagement.entities);
export const selectTaskById = (state: RootState, taskId: string) => state.taskManagement.entities[taskId];
export const selectTaskIds = (state: RootState) => state.taskManagement.ids;
export const selectGroups = (state: RootState) => state.taskManagement.groups;
export const selectGrouping = (state: RootState) => state.taskManagement.grouping;
export const selectLoading = (state: RootState) => state.taskManagement.loading;
export const selectError = (state: RootState) => state.taskManagement.error;
export const selectSelectedPriorities = (state: RootState) => state.taskManagement.selectedPriorities;
export const selectSearch = (state: RootState) => state.taskManagement.search;
// Memoized selectors
export const selectTasksByStatus = (state: RootState, status: string) =>
Object.values(state.taskManagement.entities).filter(task => task.status === status);
export const selectTasksByPriority = (state: RootState, priority: string) =>
Object.values(state.taskManagement.entities).filter(task => task.priority === priority);
export const selectTasksByPhase = (state: RootState, phase: string) =>
Object.values(state.taskManagement.entities).filter(task => task.phase === phase);
// Export the reducer as default
export default taskManagementSlice.reducer;
// V3 API selectors - no processing needed, data is pre-processed by backend
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;