feat(store): integrate task management reducers into the store

- Added taskManagementReducer, groupingReducer, and selectionReducer to the Redux store.
- Organized imports and store configuration for better clarity and maintainability.
This commit is contained in:
chamikaJ
2025-06-18 17:02:23 +05:30
parent 20039a07ff
commit c1a303e78c
15 changed files with 2791 additions and 0 deletions

View File

@@ -73,6 +73,11 @@ import timeReportsOverviewReducer from '@features/reporting/time-reports/time-re
import roadmapReducer from '../features/roadmap/roadmap-slice';
import teamMembersReducer from '@features/team-members/team-members.slice';
import groupByFilterDropdownReducer from '../features/group-by-filter-dropdown/group-by-filter-dropdown-slice';
// Task Management System
import taskManagementReducer from '@features/task-management/task-management.slice';
import groupingReducer from '@features/task-management/grouping.slice';
import selectionReducer from '@features/task-management/selection.slice';
import homePageApiService from '@/api/home-page/home-page.api.service';
import { projectsApi } from '@/api/projects/projects.v1.api.service';
@@ -159,6 +164,11 @@ export const store = configureStore({
roadmapReducer: roadmapReducer,
groupByFilterDropdownReducer: groupByFilterDropdownReducer,
timeReportsOverviewReducer: timeReportsOverviewReducer,
// Task Management System
taskManagement: taskManagementReducer,
grouping: groupingReducer,
taskManagementSelection: selectionReducer,
},
});

View File

@@ -0,0 +1,174 @@
import React from 'react';
import { Card, Button, Space, Typography, Dropdown, Menu, Popconfirm, message } from 'antd';
import {
DeleteOutlined,
EditOutlined,
TagOutlined,
UserOutlined,
CheckOutlined,
CloseOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { useDispatch, useSelector } from 'react-redux';
import { IGroupBy, bulkUpdateTasks, bulkDeleteTasks } from '@/features/tasks/tasks.slice';
import { AppDispatch, RootState } from '@/app/store';
const { Text } = Typography;
interface BulkActionBarProps {
selectedTaskIds: string[];
totalSelected: number;
currentGrouping: IGroupBy;
projectId: string;
onClearSelection?: () => void;
}
const BulkActionBar: React.FC<BulkActionBarProps> = ({
selectedTaskIds,
totalSelected,
currentGrouping,
projectId,
onClearSelection,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { statuses, priorities } = useSelector((state: RootState) => state.taskReducer);
const handleBulkStatusChange = (statusId: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { status: statusId } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkPriorityChange = (priority: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { priority } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkDelete = () => {
// dispatch(bulkDeleteTasks(selectedTaskIds));
message.success(`Deleted ${totalSelected} tasks`);
onClearSelection?.();
};
const statusMenu = (
<Menu
onClick={({ key }) => handleBulkStatusChange(key)}
items={statuses.map(status => ({
key: status.id!,
label: (
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: status.color_code }}
/>
<span>{status.name}</span>
</div>
),
}))}
/>
);
const priorityMenu = (
<Menu
onClick={({ key }) => handleBulkPriorityChange(key)}
items={[
{ key: 'critical', label: 'Critical', icon: <div className="w-2 h-2 rounded-full bg-red-500" /> },
{ key: 'high', label: 'High', icon: <div className="w-2 h-2 rounded-full bg-orange-500" /> },
{ key: 'medium', label: 'Medium', icon: <div className="w-2 h-2 rounded-full bg-yellow-500" /> },
{ key: 'low', label: 'Low', icon: <div className="w-2 h-2 rounded-full bg-green-500" /> },
]}
/>
);
const moreActionsMenu = (
<Menu
items={[
{
key: 'assign',
label: 'Assign to member',
icon: <UserOutlined />,
},
{
key: 'labels',
label: 'Add labels',
icon: <TagOutlined />,
},
{
key: 'archive',
label: 'Archive tasks',
icon: <EditOutlined />,
},
]}
/>
);
return (
<Card
size="small"
className="mb-4 bg-blue-50 border-blue-200"
styles={{ body: { padding: '8px 16px' } }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Text strong className="text-blue-700">
{totalSelected} task{totalSelected > 1 ? 's' : ''} selected
</Text>
</div>
<Space>
{/* Status Change */}
{currentGrouping !== 'status' && (
<Dropdown overlay={statusMenu} trigger={['click']}>
<Button size="small" icon={<CheckOutlined />}>
Change Status
</Button>
</Dropdown>
)}
{/* Priority Change */}
{currentGrouping !== 'priority' && (
<Dropdown overlay={priorityMenu} trigger={['click']}>
<Button size="small" icon={<EditOutlined />}>
Set Priority
</Button>
</Dropdown>
)}
{/* More Actions */}
<Dropdown overlay={moreActionsMenu} trigger={['click']}>
<Button size="small" icon={<MoreOutlined />}>
More Actions
</Button>
</Dropdown>
{/* Delete */}
<Popconfirm
title={`Delete ${totalSelected} task${totalSelected > 1 ? 's' : ''}?`}
description="This action cannot be undone."
onConfirm={handleBulkDelete}
okText="Delete"
cancelText="Cancel"
okType="danger"
>
<Button size="small" danger icon={<DeleteOutlined />}>
Delete
</Button>
</Popconfirm>
{/* Clear Selection */}
<Button
size="small"
icon={<CloseOutlined />}
onClick={onClearSelection}
title="Clear selection"
>
Clear
</Button>
</Space>
</div>
</Card>
);
};
export default BulkActionBar;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Select, Typography } from 'antd';
import { IGroupBy } from '@/features/tasks/tasks.slice';
import { IGroupByOption } from '@/types/tasks/taskList.types';
const { Text } = Typography;
const { Option } = Select;
interface GroupingSelectorProps {
currentGrouping: IGroupBy;
onChange: (groupBy: IGroupBy) => void;
options: IGroupByOption[];
disabled?: boolean;
}
const GroupingSelector: React.FC<GroupingSelectorProps> = ({
currentGrouping,
onChange,
options,
disabled = false,
}) => {
return (
<div className="flex items-center space-x-2">
<Text className="text-sm text-gray-600">Group by:</Text>
<Select
value={currentGrouping}
onChange={onChange}
disabled={disabled}
size="small"
style={{ minWidth: 100 }}
className="capitalize"
>
{options.map((option) => (
<Option key={option.value} value={option.value} className="capitalize">
{option.label}
</Option>
))}
</Select>
</div>
);
};
export default GroupingSelector;

View File

@@ -0,0 +1,199 @@
import React, { useState } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Card, Button, Typography, Badge, Collapse, Space, Tooltip } from 'antd';
import {
CaretRightOutlined,
CaretDownOutlined,
PlusOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { ITaskListGroup, IProjectTask } from '@/types/tasks/taskList.types';
import { IGroupBy } from '@/features/tasks/tasks.slice';
import TaskRow from './TaskRow';
const { Text } = Typography;
interface TaskGroupProps {
group: ITaskListGroup;
projectId: string;
currentGrouping: IGroupBy;
selectedTaskIds: string[];
onAddTask?: (groupId: string) => void;
onToggleCollapse?: (groupId: string) => void;
}
const TaskGroup: React.FC<TaskGroupProps> = ({
group,
projectId,
currentGrouping,
selectedTaskIds,
onAddTask,
onToggleCollapse,
}) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const { setNodeRef, isOver } = useDroppable({
id: group.id,
data: {
type: 'group',
groupId: group.id,
},
});
// Get task IDs for sortable context
const taskIds = group.tasks.map(task => task.id!);
// Calculate group statistics
const completedTasks = group.tasks.filter(task =>
task.status_category === 'DONE' || task.complete_ratio === 100
).length;
const totalTasks = group.tasks.length;
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
// Get group color based on grouping type
const getGroupColor = () => {
if (group.color_code) return group.color_code;
// Fallback colors based on group value
switch (currentGrouping) {
case 'status':
return group.id === 'todo' ? '#faad14' :
group.id === 'doing' ? '#1890ff' : '#52c41a';
case 'priority':
return group.id === 'critical' ? '#ff4d4f' :
group.id === 'high' ? '#fa8c16' :
group.id === 'medium' ? '#faad14' : '#52c41a';
case 'phase':
return '#722ed1';
default:
return '#d9d9d9';
}
};
const handleToggleCollapse = () => {
setIsCollapsed(!isCollapsed);
onToggleCollapse?.(group.id);
};
const handleAddTask = () => {
onAddTask?.(group.id);
};
return (
<Card
ref={setNodeRef}
className={`task-group ${isOver ? 'drag-over' : ''}`}
style={{
borderLeft: `4px solid ${getGroupColor()}`,
backgroundColor: isOver ? '#f0f8ff' : undefined,
}}
styles={{
body: { padding: 0 },
}}
>
{/* Group Header */}
<div className="group-header px-4 py-3 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
type="text"
size="small"
icon={isCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
onClick={handleToggleCollapse}
className="p-0 w-6 h-6 flex items-center justify-center"
/>
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: getGroupColor() }}
/>
<Text strong className="text-base">
{group.name}
</Text>
<Badge count={totalTasks} showZero style={{ backgroundColor: '#f0f0f0', color: '#666' }} />
</div>
{completionRate > 0 && (
<Text type="secondary" className="text-sm">
{completionRate}% complete
</Text>
)}
</div>
<Space>
<Tooltip title="Add task to this group">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={handleAddTask}
className="opacity-60 hover:opacity-100"
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<MoreOutlined />}
className="opacity-60 hover:opacity-100"
/>
</Space>
</div>
{/* Progress Bar */}
{totalTasks > 0 && (
<div className="mt-2">
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className="h-1 rounded-full transition-all duration-300"
style={{
width: `${completionRate}%`,
backgroundColor: getGroupColor(),
}}
/>
</div>
</div>
)}
</div>
{/* Tasks List */}
{!isCollapsed && (
<div className="tasks-container">
{group.tasks.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<Text type="secondary">No tasks in this group</Text>
<br />
<Button
type="link"
icon={<PlusOutlined />}
onClick={handleAddTask}
className="mt-2"
>
Add first task
</Button>
</div>
) : (
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
<div className="divide-y divide-gray-100">
{group.tasks.map((task, index) => (
<TaskRow
key={task.id}
task={task}
projectId={projectId}
groupId={group.id}
currentGrouping={currentGrouping}
isSelected={selectedTaskIds.includes(task.id!)}
index={index}
/>
))}
</div>
</SortableContext>
)}
</div>
)}
</Card>
);
};
export default TaskGroup;

View File

@@ -0,0 +1,342 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
DndContext,
DragOverlay,
DragStartEvent,
DragEndEvent,
DragOverEvent,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { Card, Button, Select, Space, Typography, Spin, Empty } from 'antd';
import { ExpandOutlined, CompressOutlined, PlusOutlined } from '@ant-design/icons';
import { RootState } from '@/app/store';
import {
IGroupBy,
GROUP_BY_OPTIONS,
setGroup,
fetchTaskGroups,
reorderTasks,
collapseAllGroups,
expandAllGroups,
} from '@/features/tasks/tasks.slice';
import { IProjectTask, ITaskListGroup } from '@/types/tasks/taskList.types';
import TaskGroup from './TaskGroup';
import TaskRow from './TaskRow';
import BulkActionBar from './BulkActionBar';
import GroupingSelector from './GroupingSelector';
import { AppDispatch } from '@/app/store';
const { Title } = Typography;
const { Option } = Select;
interface TaskListBoardProps {
projectId: string;
className?: string;
}
interface DragState {
activeTask: IProjectTask | null;
activeGroupId: string | null;
}
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
const dispatch = useDispatch<AppDispatch>();
const [dragState, setDragState] = useState<DragState>({
activeTask: null,
activeGroupId: null,
});
// Redux selectors
const {
taskGroups,
loadingGroups,
error,
groupBy,
search,
archived,
} = useSelector((state: RootState) => state.taskReducer);
// Selection state (assuming you have a selection slice)
// const selectedTaskIds = useSelector((state: RootState) => state.selection?.selectedTaskIds || []);
const selectedTaskIds: string[] = []; // Temporary placeholder
// Drag and Drop sensors
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Fetch task groups when component mounts or dependencies change
useEffect(() => {
if (projectId) {
dispatch(fetchTaskGroups(projectId));
}
}, [dispatch, projectId, groupBy, search, archived]);
// Memoized calculations
const allTaskIds = useMemo(() => {
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
}, [taskGroups]);
const totalTasksCount = useMemo(() => {
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
}, [taskGroups]);
const hasSelection = selectedTaskIds.length > 0;
// Handlers
const handleGroupingChange = (newGroupBy: IGroupBy) => {
dispatch(setGroup(newGroupBy));
};
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const taskId = active.id as string;
// Find the task and its group
let activeTask: IProjectTask | null = null;
let activeGroupId: string | null = null;
for (const group of taskGroups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
activeTask = task;
activeGroupId = group.id;
break;
}
}
setDragState({
activeTask,
activeGroupId,
});
};
const handleDragOver = (event: DragOverEvent) => {
// Handle drag over logic if needed for visual feedback
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setDragState({
activeTask: null,
activeGroupId: null,
});
if (!over || !dragState.activeTask || !dragState.activeGroupId) {
return;
}
const activeTaskId = active.id as string;
const overContainer = over.id as string;
// Determine if dropping on a group or task
const overGroup = taskGroups.find(g => g.id === overContainer);
let targetGroupId = overContainer;
let targetIndex = -1;
if (!overGroup) {
// Dropping on a task, find which group it belongs to
for (const group of taskGroups) {
const taskIndex = group.tasks.findIndex(t => t.id === overContainer);
if (taskIndex !== -1) {
targetGroupId = group.id;
targetIndex = taskIndex;
break;
}
}
}
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
if (!sourceGroup || !targetGroup) return;
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
if (sourceIndex === -1) return;
// Calculate new positions
const finalTargetIndex = targetIndex === -1 ? targetGroup.tasks.length : targetIndex;
// Create updated task arrays
const updatedSourceTasks = [...sourceGroup.tasks];
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
let updatedTargetTasks: IProjectTask[];
if (sourceGroup.id === targetGroup.id) {
// Moving within the same group
updatedTargetTasks = updatedSourceTasks;
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
} else {
// Moving between different groups
updatedTargetTasks = [...targetGroup.tasks];
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
}
// Dispatch the reorder action
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: finalTargetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
};
const handleCollapseAll = () => {
// This would need to be implemented in the tasks slice
// dispatch(collapseAllGroups());
};
const handleExpandAll = () => {
// This would need to be implemented in the tasks slice
// dispatch(expandAllGroups());
};
const handleRefresh = () => {
if (projectId) {
dispatch(fetchTaskGroups(projectId));
}
};
if (error) {
return (
<Card className={className}>
<Empty
description={`Error loading tasks: ${error}`}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</Card>
);
}
return (
<div className={`task-list-board ${className}`}>
{/* Header Controls */}
<Card
size="small"
className="mb-4"
styles={{ body: { padding: '12px 16px' } }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Title level={4} className="mb-0">
Tasks ({totalTasksCount})
</Title>
<GroupingSelector
currentGrouping={groupBy}
onChange={handleGroupingChange}
options={GROUP_BY_OPTIONS}
/>
</div>
<Space>
<Button
size="small"
icon={<CompressOutlined />}
onClick={handleCollapseAll}
title="Collapse All Groups"
/>
<Button
size="small"
icon={<ExpandOutlined />}
onClick={handleExpandAll}
title="Expand All Groups"
/>
<Button
size="small"
onClick={handleRefresh}
loading={loadingGroups}
title="Refresh"
>
Refresh
</Button>
</Space>
</div>
</Card>
{/* Bulk Action Bar */}
{hasSelection && (
<BulkActionBar
selectedTaskIds={selectedTaskIds}
totalSelected={selectedTaskIds.length}
currentGrouping={groupBy}
projectId={projectId}
/>
)}
{/* Task Groups */}
<div className="task-groups-container">
{loadingGroups ? (
<Card>
<div className="flex justify-center items-center py-8">
<Spin size="large" />
</div>
</Card>
) : taskGroups.length === 0 ? (
<Card>
<Empty
description="No tasks found"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</Card>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="space-y-4">
{taskGroups.map((group) => (
<TaskGroup
key={group.id}
group={group}
projectId={projectId}
currentGrouping={groupBy}
selectedTaskIds={selectedTaskIds}
/>
))}
</div>
<DragOverlay>
{dragState.activeTask ? (
<TaskRow
task={dragState.activeTask}
projectId={projectId}
groupId={dragState.activeGroupId!}
currentGrouping={groupBy}
isSelected={false}
isDragOverlay
/>
) : null}
</DragOverlay>
</DndContext>
)}
</div>
</div>
);
};
export default TaskListBoard;

View File

@@ -0,0 +1,285 @@
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
import {
DragOutlined,
EyeOutlined,
MessageOutlined,
PaperClipOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { IProjectTask } from '@/types/tasks/taskList.types';
import { IGroupBy } from '@/features/tasks/tasks.slice';
const { Text } = Typography;
interface TaskRowProps {
task: IProjectTask;
projectId: string;
groupId: string;
currentGrouping: IGroupBy;
isSelected: boolean;
isDragOverlay?: boolean;
index?: number;
onSelect?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
}
const TaskRow: React.FC<TaskRowProps> = ({
task,
projectId,
groupId,
currentGrouping,
isSelected,
isDragOverlay = false,
index,
onSelect,
onToggleSubtasks,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: task.id!,
data: {
type: 'task',
taskId: task.id,
groupId,
},
disabled: isDragOverlay,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const handleSelectChange = (checked: boolean) => {
onSelect?.(task.id!, checked);
};
const handleToggleSubtasks = () => {
onToggleSubtasks?.(task.id!);
};
// Format due date
const formatDueDate = (dateString?: string) => {
if (!dateString) return null;
const date = new Date(dateString);
const now = new Date();
const diffTime = date.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return { text: `${Math.abs(diffDays)}d overdue`, color: 'error' };
} else if (diffDays === 0) {
return { text: 'Due today', color: 'warning' };
} else if (diffDays <= 3) {
return { text: `Due in ${diffDays}d`, color: 'warning' };
} else {
return { text: `Due ${date.toLocaleDateString()}`, color: 'default' };
}
};
const dueDate = formatDueDate(task.end_date);
return (
<div
ref={setNodeRef}
style={style}
className={`task-row p-4 hover:bg-gray-50 ${isSelected ? 'bg-blue-50 border-l-2 border-l-blue-500' : ''} ${isDragOverlay ? 'shadow-lg bg-white rounded-md border' : ''}`}
>
<div className="flex items-center space-x-3">
{/* Drag Handle */}
<Button
type="text"
size="small"
icon={<DragOutlined />}
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
{...attributes}
{...listeners}
/>
{/* Selection Checkbox */}
<Checkbox
checked={isSelected}
onChange={(e) => handleSelectChange(e.target.checked)}
/>
{/* Task Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
{/* Task Name and Key */}
<div className="flex items-center space-x-2 mb-1">
{task.project_id && (
<Text code className="text-xs text-gray-500">
{task.key}
</Text>
)}
<Text
strong
className={`text-sm ${task.complete_ratio === 100 ? 'line-through text-gray-500' : ''}`}
>
{task.name}
</Text>
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
<Button
type="text"
size="small"
onClick={handleToggleSubtasks}
className="text-xs text-gray-500 px-1 h-5"
>
{task.show_sub_tasks ? '' : '+'} {task.sub_tasks_count}
</Button>
)}
</div>
{/* Description (if exists) */}
{task.description && (
<Text type="secondary" className="text-xs line-clamp-1">
{task.description}
</Text>
)}
{/* Labels */}
{task.labels && task.labels.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{task.labels.slice(0, 3).map((label) => (
<Tag
key={label.id}
color={label.color_code}
className="text-xs m-0 px-1 py-0 text-white"
style={{
backgroundColor: label.color_code,
border: 'none',
fontSize: '10px',
}}
>
{label.name}
</Tag>
))}
{task.labels.length > 3 && (
<Text type="secondary" className="text-xs">
+{task.labels.length - 3} more
</Text>
)}
</div>
)}
</div>
{/* Task Metadata */}
<div className="flex items-center space-x-3 ml-4">
{/* Progress */}
{task.complete_ratio !== undefined && task.complete_ratio > 0 && (
<div className="w-16">
<Progress
percent={task.complete_ratio}
size="small"
showInfo={false}
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
/>
</div>
)}
{/* Assignees */}
{task.assignees && task.assignees.length > 0 && (
<Avatar.Group size="small" maxCount={3}>
{task.assignees.map((assignee) => (
<Tooltip key={assignee.id} title={assignee.name}>
<Avatar
size="small"
src={assignee.avatar_url}
style={{ backgroundColor: assignee.color_code }}
>
{assignee.name?.charAt(0)?.toUpperCase()}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
)}
{/* Priority Indicator */}
{task.priority_color && (
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: task.priority_color }}
title={`Priority: ${task.priority}`}
/>
)}
{/* Due Date */}
{dueDate && (
<div className="flex items-center space-x-1">
<ClockCircleOutlined className="text-xs text-gray-400" />
<Text
className={`text-xs ${
dueDate.color === 'error' ? 'text-red-500' :
dueDate.color === 'warning' ? 'text-orange-500' :
'text-gray-500'
}`}
>
{dueDate.text}
</Text>
</div>
)}
{/* Task Indicators */}
<Space size={0}>
{task.comments_count && task.comments_count > 0 && (
<div className="flex items-center space-x-1">
<MessageOutlined className="text-xs text-gray-400" />
<Text className="text-xs text-gray-500">{task.comments_count}</Text>
</div>
)}
{task.attachments_count && task.attachments_count > 0 && (
<div className="flex items-center space-x-1">
<PaperClipOutlined className="text-xs text-gray-400" />
<Text className="text-xs text-gray-500">{task.attachments_count}</Text>
</div>
)}
</Space>
{/* View/Edit Button */}
<Button
type="text"
size="small"
icon={<EyeOutlined />}
className="opacity-60 hover:opacity-100"
onClick={() => {
// Handle task view/edit
}}
/>
</div>
</div>
</div>
</div>
{/* Subtasks */}
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
<div className="mt-3 ml-8 pl-4 border-l-2 border-gray-200">
{task.sub_tasks.map((subtask) => (
<TaskRow
key={subtask.id}
task={subtask}
projectId={projectId}
groupId={groupId}
currentGrouping={currentGrouping}
isSelected={isSelected}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
);
};
export default TaskRow;

View File

@@ -0,0 +1,189 @@
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { GroupingState, TaskGroup } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { taskManagementSelectors } from './task-management.slice';
const initialState: GroupingState = {
currentGrouping: 'status',
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
groupOrder: {
status: ['todo', 'doing', 'done'],
priority: ['critical', 'high', 'medium', 'low'],
phase: ['Planning', 'Development', 'Testing', 'Deployment'],
},
groupStates: {},
};
const groupingSlice = createSlice({
name: 'grouping',
initialState,
reducers: {
setCurrentGrouping: (state, action: PayloadAction<'status' | 'priority' | 'phase'>) => {
state.currentGrouping = action.payload;
},
addCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload.trim();
if (phase && !state.customPhases.includes(phase)) {
state.customPhases.push(phase);
state.groupOrder.phase.push(phase);
}
},
removeCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload;
state.customPhases = state.customPhases.filter(p => p !== phase);
state.groupOrder.phase = state.groupOrder.phase.filter(p => p !== phase);
},
updateCustomPhases: (state, action: PayloadAction<string[]>) => {
state.customPhases = action.payload;
state.groupOrder.phase = action.payload;
},
updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => {
const { groupType, order } = action.payload;
state.groupOrder[groupType] = order;
},
toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
const groupId = action.payload;
if (!state.groupStates[groupId]) {
state.groupStates[groupId] = { collapsed: false };
}
state.groupStates[groupId].collapsed = !state.groupStates[groupId].collapsed;
},
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
const { groupId, collapsed } = action.payload;
if (!state.groupStates[groupId]) {
state.groupStates[groupId] = { collapsed: false };
}
state.groupStates[groupId].collapsed = collapsed;
},
collapseAllGroups: (state) => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = true;
});
},
expandAllGroups: (state) => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = false;
});
},
},
});
export const {
setCurrentGrouping,
addCustomPhase,
removeCustomPhase,
updateCustomPhases,
updateGroupOrder,
toggleGroupCollapsed,
setGroupCollapsed,
collapseAllGroups,
expandAllGroups,
} = groupingSlice.actions;
// Selectors
export const selectCurrentGrouping = (state: RootState) => state.grouping.currentGrouping;
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
// Complex selectors using createSelector for memoization
export const selectCurrentGroupOrder = createSelector(
[selectCurrentGrouping, selectGroupOrder],
(currentGrouping, groupOrder) => groupOrder[currentGrouping] || []
);
export const selectTaskGroups = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
(tasks, currentGrouping, groupOrder, groupStates) => {
const groups: TaskGroup[] = [];
// Get unique values for the current grouping
const groupValues = groupOrder.length > 0 ? groupOrder :
[...new Set(tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
return task.phase;
}))];
groupValues.forEach(value => {
const tasksInGroup = tasks.filter(task => {
if (currentGrouping === 'status') return task.status === value;
if (currentGrouping === 'priority') return task.priority === value;
return task.phase === value;
}).sort((a, b) => a.order - b.order);
const groupId = `${currentGrouping}-${value}`;
groups.push({
id: groupId,
title: value.charAt(0).toUpperCase() + value.slice(1),
groupType: currentGrouping,
groupValue: value,
collapsed: groupStates[groupId]?.collapsed || false,
taskIds: tasksInGroup.map(task => task.id),
color: getGroupColor(currentGrouping, value),
});
});
return groups;
}
);
export const selectTasksByCurrentGrouping = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping],
(tasks, currentGrouping) => {
const grouped: Record<string, typeof tasks> = {};
tasks.forEach(task => {
let key: string;
if (currentGrouping === 'status') key = task.status;
else if (currentGrouping === 'priority') key = task.priority;
else key = task.phase;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(task);
});
// Sort tasks within each group by order
Object.keys(grouped).forEach(key => {
grouped[key].sort((a, b) => a.order - b.order);
});
return grouped;
}
);
// Helper function to get group colors
const getGroupColor = (groupType: string, value: string): string => {
const colorMaps = {
status: {
todo: '#f0f0f0',
doing: '#1890ff',
done: '#52c41a',
},
priority: {
critical: '#ff4d4f',
high: '#ff7a45',
medium: '#faad14',
low: '#52c41a',
},
phase: {
Planning: '#722ed1',
Development: '#1890ff',
Testing: '#faad14',
Deployment: '#52c41a',
},
};
return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9';
};
export default groupingSlice.reducer;

View File

@@ -0,0 +1,110 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SelectionState } from '@/types/task-management.types';
import { RootState } from '@/app/store';
const initialState: SelectionState = {
selectedTaskIds: [],
lastSelectedId: null,
};
const selectionSlice = createSlice({
name: 'selection',
initialState,
reducers: {
toggleTaskSelection: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
const index = state.selectedTaskIds.indexOf(taskId);
if (index === -1) {
state.selectedTaskIds.push(taskId);
} else {
state.selectedTaskIds.splice(index, 1);
}
state.lastSelectedId = taskId;
},
selectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
if (!state.selectedTaskIds.includes(taskId)) {
state.selectedTaskIds.push(taskId);
}
state.lastSelectedId = taskId;
},
deselectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
if (state.lastSelectedId === taskId) {
state.lastSelectedId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
}
},
selectMultipleTasks: (state, action: PayloadAction<string[]>) => {
const taskIds = action.payload;
// Add new task IDs that aren't already selected
taskIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = taskIds[taskIds.length - 1] || state.lastSelectedId;
},
selectRangeTasks: (state, action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>) => {
const { startId, endId, allTaskIds } = action.payload;
const startIndex = allTaskIds.indexOf(startId);
const endIndex = allTaskIds.indexOf(endId);
if (startIndex !== -1 && endIndex !== -1) {
const [start, end] = startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
const rangeIds = allTaskIds.slice(start, end + 1);
// Add range IDs that aren't already selected
rangeIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = endId;
}
},
selectAllTasks: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
clearSelection: (state) => {
state.selectedTaskIds = [];
state.lastSelectedId = null;
},
setSelection: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
},
});
export const {
toggleTaskSelection,
selectTask,
deselectTask,
selectMultipleTasks,
selectRangeTasks,
selectAllTasks,
clearSelection,
setSelection,
} = selectionSlice.actions;
// Selectors
export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
export const selectLastSelectedId = (state: RootState) => state.taskManagementSelection.lastSelectedId;
export const selectHasSelection = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length > 0;
export const selectSelectionCount = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length;
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.includes(taskId);
export default selectionSlice.reducer;

View File

@@ -0,0 +1,135 @@
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
import { Task, TaskManagementState } from '@/types/task-management.types';
import { RootState } from '@/app/store';
// Entity adapter for normalized state
const tasksAdapter = createEntityAdapter<Task>({
selectId: (task) => task.id,
sortComparer: (a, b) => a.order - b.order,
});
const initialState: TaskManagementState = {
entities: {},
ids: [],
loading: false,
error: null,
};
const taskManagementSlice = createSlice({
name: 'taskManagement',
initialState: tasksAdapter.getInitialState(initialState),
reducers: {
// Basic CRUD operations
setTasks: (state, action: PayloadAction<Task[]>) => {
tasksAdapter.setAll(state, action.payload);
state.loading = false;
state.error = null;
},
addTask: (state, action: PayloadAction<Task>) => {
tasksAdapter.addOne(state, action.payload);
},
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
tasksAdapter.updateOne(state, {
id: action.payload.id,
changes: {
...action.payload.changes,
updatedAt: new Date().toISOString(),
},
});
},
deleteTask: (state, action: PayloadAction<string>) => {
tasksAdapter.removeOne(state, action.payload);
},
// Bulk operations
bulkUpdateTasks: (state, action: PayloadAction<{ ids: string[]; changes: Partial<Task> }>) => {
const { ids, changes } = action.payload;
const updates = ids.map(id => ({
id,
changes: {
...changes,
updatedAt: new Date().toISOString(),
},
}));
tasksAdapter.updateMany(state, updates);
},
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
tasksAdapter.removeMany(state, action.payload);
},
// Drag and drop operations
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; newOrder: number[] }>) => {
const { taskIds, newOrder } = action.payload;
const updates = taskIds.map((id, index) => ({
id,
changes: { order: newOrder[index] },
}));
tasksAdapter.updateMany(state, updates);
},
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string }>) => {
const { taskId, groupType, groupValue } = action.payload;
const changes: Partial<Task> = {
updatedAt: new Date().toISOString(),
};
// Update the appropriate field based on group type
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
} else if (groupType === 'priority') {
changes.priority = groupValue as Task['priority'];
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
tasksAdapter.updateOne(state, { id: taskId, changes });
},
// Loading states
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
state.loading = false;
},
},
});
export const {
setTasks,
addTask,
updateTask,
deleteTask,
bulkUpdateTasks,
bulkDeleteTasks,
reorderTasks,
moveTaskToGroup,
setLoading,
setError,
} = taskManagementSlice.actions;
// Selectors
export const taskManagementSelectors = tasksAdapter.getSelectors<RootState>(
(state) => state.taskManagement
);
// Additional selectors
export const selectTasksByStatus = (state: RootState, status: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.status === status);
export const selectTasksByPriority = (state: RootState, priority: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.priority === priority);
export const selectTasksByPhase = (state: RootState, phase: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.phase === phase);
export const selectTasksLoading = (state: RootState) => state.taskManagement.loading;
export const selectTasksError = (state: RootState) => state.taskManagement.error;
export default taskManagementSlice.reducer;

View File

@@ -0,0 +1,78 @@
import React, { useEffect } from 'react';
import { Layout, Typography, Card, Space, Alert } from 'antd';
import { useDispatch } from 'react-redux';
import TaskListBoard from '@/components/task-management/TaskListBoard';
import { AppDispatch } from '@/app/store';
const { Header, Content } = Layout;
const { Title, Paragraph } = Typography;
const TaskManagementDemo: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
// Mock project ID for demo
const demoProjectId = 'demo-project-123';
useEffect(() => {
// Initialize demo data if needed
// You might want to populate some sample tasks here for demonstration
}, [dispatch]);
return (
<Layout className="min-h-screen bg-gray-50">
<Header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4">
<Title level={2} className="mb-0 text-gray-800">
Enhanced Task Management System
</Title>
</div>
</Header>
<Content className="max-w-7xl mx-auto px-4 py-6 w-full">
<Space direction="vertical" size="large" className="w-full">
{/* Introduction */}
<Card>
<Title level={3}>Task Management Features</Title>
<Paragraph>
This enhanced task management system provides a comprehensive interface for managing tasks
with the following key features:
</Paragraph>
<ul className="list-disc list-inside space-y-1 text-gray-700">
<li><strong>Dynamic Grouping:</strong> Group tasks by Status, Priority, or Phase</li>
<li><strong>Drag & Drop:</strong> Reorder tasks within groups or move between groups</li>
<li><strong>Multi-select:</strong> Select multiple tasks for bulk operations</li>
<li><strong>Bulk Actions:</strong> Change status, priority, assignees, or delete multiple tasks</li>
<li><strong>Subtasks:</strong> Expandable subtask support with progress tracking</li>
<li><strong>Real-time Updates:</strong> Live updates via WebSocket connections</li>
<li><strong>Rich Task Display:</strong> Progress bars, assignees, labels, due dates, and more</li>
</ul>
</Card>
{/* Usage Instructions */}
<Alert
message="Demo Instructions"
description={
<div>
<p><strong>Grouping:</strong> Use the dropdown to switch between Status, Priority, and Phase grouping.</p>
<p><strong>Drag & Drop:</strong> Click and drag tasks to reorder within groups or move between groups.</p>
<p><strong>Selection:</strong> Click checkboxes to select tasks, then use bulk actions in the blue bar.</p>
<p><strong>Subtasks:</strong> Click the +/- buttons next to task names to expand/collapse subtasks.</p>
</div>
}
type="info"
showIcon
className="mb-4"
/>
{/* Task List Board */}
<TaskListBoard
projectId={demoProjectId}
className="task-management-demo"
/>
</Space>
</Content>
</Layout>
);
};
export default TaskManagementDemo;

View File

@@ -0,0 +1,231 @@
/* Task Management System Styles */
.task-list-board {
width: 100%;
}
.task-group {
transition: all 0.2s ease;
}
.task-group.drag-over {
border-color: #1890ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.task-group .group-header {
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.task-group .group-header:hover {
background: #f5f5f5;
}
.task-row {
border-left: 2px solid transparent;
transition: all 0.2s ease;
}
.task-row:hover {
background-color: #f9f9f9 !important;
border-left-color: #d9d9d9;
}
.task-row.selected {
background-color: #e6f7ff !important;
border-left-color: #1890ff;
}
.task-row .drag-handle {
cursor: grab;
transition: opacity 0.2s ease;
}
.task-row .drag-handle:active {
cursor: grabbing;
}
.task-row:not(:hover) .drag-handle {
opacity: 0.3;
}
/* Progress bars */
.ant-progress-line {
margin: 0;
}
.ant-progress-bg {
border-radius: 2px;
}
/* Avatar groups */
.ant-avatar-group .ant-avatar {
border: 1px solid #fff;
}
/* Tags */
.task-row .ant-tag {
margin: 0;
padding: 0 4px;
height: 16px;
line-height: 14px;
font-size: 10px;
border-radius: 2px;
}
/* Checkboxes */
.task-row .ant-checkbox-wrapper {
margin-right: 0;
}
/* Bulk action bar */
.bulk-action-bar {
position: sticky;
top: 0;
z-index: 10;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 6px;
margin-bottom: 16px;
}
/* Collapsible animations */
.task-group .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
/* Drag overlay */
.task-row.drag-overlay {
background: white;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
transform: rotate(5deg);
cursor: grabbing;
z-index: 1000;
}
/* Responsive design */
@media (max-width: 768px) {
.task-row {
padding: 12px;
}
.task-row .flex {
flex-direction: column;
align-items: flex-start;
}
.task-row .task-metadata {
margin-top: 8px;
margin-left: 0 !important;
}
}
/* Subtask indentation */
.task-subtasks {
margin-left: 32px;
padding-left: 16px;
border-left: 2px solid #f0f0f0;
}
.task-subtasks .task-row {
padding: 8px 16px;
font-size: 13px;
}
/* Status indicators */
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.priority-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
/* Animation classes */
.task-row-enter {
opacity: 0;
transform: translateY(-10px);
}
.task-row-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms;
}
.task-row-exit {
opacity: 1;
}
.task-row-exit-active {
opacity: 0;
transform: translateY(-10px);
transition: opacity 300ms, transform 300ms;
}
/* Custom scrollbar */
.task-groups-container::-webkit-scrollbar {
width: 6px;
}
.task-groups-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.task-groups-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.task-groups-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Loading states */
.task-row.loading {
opacity: 0.6;
pointer-events: none;
}
.task-group.loading {
opacity: 0.8;
}
/* Focus styles for accessibility */
.task-row:focus-within {
outline: 2px solid #1890ff;
outline-offset: 2px;
}
.drag-handle:focus {
outline: 2px solid #1890ff;
outline-offset: 2px;
}
/* Dark mode support (if needed) */
@media (prefers-color-scheme: dark) {
.task-row {
background-color: #141414;
color: #ffffff;
}
.task-row:hover {
background-color: #1f1f1f;
}
.task-group .group-header {
background: #1f1f1f;
border-bottom-color: #303030;
}
}

View File

@@ -0,0 +1,125 @@
export interface Task {
id: string;
title: string;
description?: string;
status: 'todo' | 'doing' | 'done';
priority: 'critical' | 'high' | 'medium' | 'low';
phase: string; // Custom phases like 'planning', 'development', 'testing', 'deployment'
progress: number; // 0-100
assignees: string[];
labels: string[];
dueDate?: string;
timeTracking: {
estimated?: number;
logged: number;
};
customFields: Record<string, any>;
createdAt: string;
updatedAt: string;
order: number;
}
export interface TaskGroup {
id: string;
title: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string; // The actual value for the group (e.g., 'todo', 'high', 'development')
collapsed: boolean;
taskIds: string[];
color?: string; // For visual distinction
}
export interface GroupingConfig {
currentGrouping: 'status' | 'priority' | 'phase';
customPhases: string[]; // User-defined phases
groupOrder: Record<string, string[]>; // Order of groups for each grouping type
}
export interface Column {
id: string;
title: string;
dataIndex: string;
width: number;
visible: boolean;
editable: boolean;
type: 'text' | 'select' | 'date' | 'progress' | 'tags' | 'users';
}
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
export interface Label {
id: string;
name: string;
color: string;
}
// Redux State Interfaces
export interface TaskManagementState {
entities: Record<string, Task>;
ids: string[];
loading: boolean;
error: string | null;
}
export interface TaskGroupsState {
entities: Record<string, TaskGroup>;
ids: string[];
}
export interface GroupingState {
currentGrouping: 'status' | 'priority' | 'phase';
customPhases: string[];
groupOrder: Record<string, string[]>;
groupStates: Record<string, { collapsed: boolean }>; // Persist group states
}
export interface SelectionState {
selectedTaskIds: string[];
lastSelectedId: string | null;
}
export interface ColumnsState {
entities: Record<string, Column>;
ids: string[];
order: string[];
}
export interface UIState {
draggedTaskId: string | null;
bulkActionMode: boolean;
editingCell: { taskId: string; field: string } | null;
}
// Drag and Drop
export interface DragEndEvent {
active: {
id: string;
data: {
current?: {
taskId: string;
groupId: string;
};
};
};
over: {
id: string;
data: {
current?: {
groupId: string;
type: 'group' | 'task';
};
};
} | null;
}
// Bulk Actions
export interface BulkAction {
type: 'status' | 'priority' | 'phase' | 'assignee' | 'label' | 'delete';
value?: any;
taskIds: string[];
}

View File

@@ -0,0 +1,166 @@
import { Task, User, Label } from '@/types/task-management.types';
import { nanoid } from 'nanoid';
// Mock users
export const mockUsers: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', avatar: '' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', avatar: '' },
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', avatar: '' },
{ id: '4', name: 'Alice Brown', email: 'alice@example.com', avatar: '' },
{ id: '5', name: 'Charlie Wilson', email: 'charlie@example.com', avatar: '' },
];
// Mock labels
export const mockLabels: Label[] = [
{ id: '1', name: 'Bug', color: '#ff4d4f' },
{ id: '2', name: 'Feature', color: '#52c41a' },
{ id: '3', name: 'Enhancement', color: '#1890ff' },
{ id: '4', name: 'Documentation', color: '#722ed1' },
{ id: '5', name: 'Urgent', color: '#fa541c' },
{ id: '6', name: 'Research', color: '#faad14' },
];
// Task titles for variety
const taskTitles = [
'Implement user authentication system',
'Design responsive navigation component',
'Fix CSS styling issues on mobile',
'Add drag and drop functionality',
'Optimize database queries',
'Write unit tests for API endpoints',
'Update documentation for new features',
'Refactor legacy code components',
'Set up CI/CD pipeline',
'Configure monitoring and logging',
'Implement real-time notifications',
'Create user onboarding flow',
'Add search functionality',
'Optimize image loading performance',
'Implement data export feature',
'Add multi-language support',
'Create admin dashboard',
'Fix memory leak in background process',
'Implement caching strategy',
'Add email notification system',
'Create API rate limiting',
'Implement user roles and permissions',
'Add file upload functionality',
'Create backup and restore system',
'Implement advanced filtering',
'Add calendar integration',
'Create reporting dashboard',
'Implement websocket connections',
'Add payment processing',
'Create mobile app version',
];
const taskDescriptions = [
'This task requires careful consideration of security best practices and user experience.',
'Need to ensure compatibility across all modern browsers and devices.',
'Critical bug that affects user workflow and needs immediate attention.',
'Enhancement to improve overall system performance and user satisfaction.',
'Research task to explore new technologies and implementation approaches.',
'Documentation update to keep project information current and accurate.',
'Refactoring work to improve code maintainability and reduce technical debt.',
'Testing task to ensure reliability and prevent regression bugs.',
];
const statuses: Task['status'][] = ['todo', 'doing', 'done'];
const priorities: Task['priority'][] = ['critical', 'high', 'medium', 'low'];
const phases = ['Planning', 'Development', 'Testing', 'Deployment'];
function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
function getRandomElements<T>(array: T[], min: number = 0, max?: number): T[] {
const maxCount = max ?? array.length;
const count = Math.floor(Math.random() * (maxCount - min + 1)) + min;
const shuffled = [...array].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
}
function getRandomProgress(): number {
const progressOptions = [0, 10, 25, 50, 75, 90, 100];
return getRandomElement(progressOptions);
}
function getRandomTimeTracking() {
const estimated = Math.floor(Math.random() * 40) + 1; // 1-40 hours
const logged = Math.floor(Math.random() * estimated); // 0 to estimated hours
return { estimated, logged };
}
function getRandomDueDate(): string | undefined {
if (Math.random() < 0.7) { // 70% chance of having a due date
const now = new Date();
const daysToAdd = Math.floor(Math.random() * 30) - 10; // -10 to +20 days from now
const dueDate = new Date(now.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
return dueDate.toISOString().split('T')[0];
}
return undefined;
}
export function generateMockTask(index: number): Task {
const now = new Date();
const createdAt = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000); // Up to 30 days ago
return {
id: nanoid(),
title: getRandomElement(taskTitles),
description: Math.random() < 0.8 ? getRandomElement(taskDescriptions) : undefined,
status: getRandomElement(statuses),
priority: getRandomElement(priorities),
phase: getRandomElement(phases),
progress: getRandomProgress(),
assignees: getRandomElements(mockUsers, 0, 3).map(user => user.id), // 0-3 assignees
labels: getRandomElements(mockLabels, 0, 4).map(label => label.id), // 0-4 labels
dueDate: getRandomDueDate(),
timeTracking: getRandomTimeTracking(),
customFields: {},
createdAt: createdAt.toISOString(),
updatedAt: createdAt.toISOString(),
order: index,
};
}
export function generateMockTasks(count: number = 100): Task[] {
return Array.from({ length: count }, (_, index) => generateMockTask(index));
}
// Generate tasks with specific distribution for testing
export function generateBalancedMockTasks(count: number = 100): Task[] {
const tasks: Task[] = [];
const statusDistribution = { todo: 0.4, doing: 0.4, done: 0.2 };
const priorityDistribution = { critical: 0.1, high: 0.3, medium: 0.4, low: 0.2 };
for (let i = 0; i < count; i++) {
const task = generateMockTask(i);
// Distribute statuses
const statusRand = Math.random();
if (statusRand < statusDistribution.todo) {
task.status = 'todo';
} else if (statusRand < statusDistribution.todo + statusDistribution.doing) {
task.status = 'doing';
} else {
task.status = 'done';
}
// Distribute priorities
const priorityRand = Math.random();
if (priorityRand < priorityDistribution.critical) {
task.priority = 'critical';
} else if (priorityRand < priorityDistribution.critical + priorityDistribution.high) {
task.priority = 'high';
} else if (priorityRand < priorityDistribution.critical + priorityDistribution.high + priorityDistribution.medium) {
task.priority = 'medium';
} else {
task.priority = 'low';
}
tasks.push(task);
}
return tasks;
}