diff --git a/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx b/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx index 21410c3b..49350ffa 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import { getContrastColor } from '@/utils/colorUtils'; interface TaskGroupHeaderProps { group: { @@ -13,40 +14,50 @@ interface TaskGroupHeaderProps { } const TaskGroupHeader: React.FC = ({ group, isCollapsed, onToggle }) => { + const headerBackgroundColor = group.color || '#F0F0F0'; // Default light gray if no color + const headerTextColor = getContrastColor(headerBackgroundColor); + return (
{/* Chevron button */} {/* Group indicator and name */}
- {/* Color indicator */} -
+ {/* Color indicator (removed as full header is colored) */} {/* Group name and count */}
- + {group.name} - + {group.count}
diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx index 58769bf0..322fbe79 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx @@ -1,5 +1,21 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { GroupedVirtuoso } from 'react-virtuoso'; +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + PointerSensor, + useSensor, + useSensors, + KeyboardSensor, + TouchSensor, +} from '@dnd-kit/core'; +import { + SortableContext, + verticalListSortingStrategy, + sortableKeyboardCoordinates, +} from '@dnd-kit/sortable'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { @@ -15,7 +31,6 @@ import { import { selectCurrentGrouping, selectCollapsedGroups, - selectIsGroupCollapsed, toggleGroupCollapsed, } from '@/features/task-management/grouping.slice'; import { @@ -36,6 +51,7 @@ import { TaskListField } from '@/types/task-list-field.types'; import { useParams } from 'react-router-dom'; import ImprovedTaskFilters from '@/components/task-management/improved-task-filters'; import { Bars3Icon } from '@heroicons/react/24/outline'; +import { HolderOutlined } from '@ant-design/icons'; import { COLUMN_KEYS } from '@/features/tasks/tasks.slice'; // Base column configuration @@ -75,10 +91,33 @@ interface TaskListV2Props { const TaskListV2: React.FC = ({ projectId }) => { const dispatch = useAppDispatch(); const { projectId: urlProjectId } = useParams(); - const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + + // Drag and drop state + const [activeId, setActiveId] = useState(null); + + // Configure sensors for drag and drop + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }) + ); + + // Using Redux state for collapsedGroups instead of local state + const collapsedGroups = useAppSelector(selectCollapsedGroups); // Selectors - const tasks = useAppSelector(selectAllTasksArray); + const allTasks = useAppSelector(selectAllTasksArray); // Renamed to allTasks for clarity const groups = useAppSelector(selectGroups); const grouping = useAppSelector(selectGrouping); const loading = useAppSelector(selectLoading); @@ -114,7 +153,7 @@ const TaskListV2: React.FC = ({ projectId }) => { if (event.ctrlKey || event.metaKey) { dispatch(toggleTaskSelection(taskId)); } else if (event.shiftKey && lastSelectedTaskId) { - const taskIds = tasks.map(t => t.id); + const taskIds = allTasks.map(t => t.id); // Use allTasks here const startIdx = taskIds.indexOf(lastSelectedTaskId); const endIdx = taskIds.indexOf(taskId); const rangeIds = taskIds.slice( @@ -126,58 +165,126 @@ const TaskListV2: React.FC = ({ projectId }) => { dispatch(clearSelection()); dispatch(selectTask(taskId)); } - }, [dispatch, lastSelectedTaskId, tasks]); + }, [dispatch, lastSelectedTaskId, allTasks]); const handleGroupCollapse = useCallback((groupId: string) => { - setCollapsedGroups(prev => { - const next = new Set(prev); - if (next.has(groupId)) { - next.delete(groupId); - } else { - next.add(groupId); - } - return next; - }); + dispatch(toggleGroupCollapsed(groupId)); // Dispatch Redux action to toggle collapsed state + }, [dispatch]); + + // Drag and drop handlers + const handleDragStart = useCallback((event: DragStartEvent) => { + setActiveId(event.active.id as string); }, []); - // Memoized values - const groupCounts = useMemo(() => { - return groups.map(group => { - const visibleTasks = tasks.filter(task => group.taskIds.includes(task.id)); - return visibleTasks.length; - }); - }, [groups, tasks]); + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + setActiveId(null); - const visibleGroups = useMemo(() => { - return groups.filter(group => !collapsedGroups.has(group.id)); - }, [groups, collapsedGroups]); + if (!over || active.id === over.id) { + return; + } + + // Find the active task + const activeTask = allTasks.find(task => task.id === active.id); + if (!activeTask) { + console.error('Active task not found:', active.id); + return; + } + + // Find which group the task is being moved to + const overTask = allTasks.find(task => task.id === over.id); + if (!overTask) { + console.error('Over task not found:', over.id); + return; + } + + // Find the groups for both tasks + const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id)); + const overGroup = groups.find(group => group.taskIds.includes(overTask.id)); + + if (!activeGroup || !overGroup) { + console.error('Could not find groups for tasks'); + return; + } + + // Calculate new positions + const activeIndex = allTasks.findIndex(task => task.id === active.id); + const overIndex = allTasks.findIndex(task => task.id === over.id); + + console.log('Drag operation:', { + activeId: active.id, + overId: over.id, + activeIndex, + overIndex, + activeGroup: activeGroup.id, + overGroup: overGroup.id, + }); + + // TODO: Implement the actual reordering logic + // This would typically involve: + // 1. Updating the task order in Redux + // 2. Sending the update to the backend + // 3. Optimistic UI updates + + }, [allTasks, groups]); + + // Memoized values for GroupedVirtuoso + const virtuosoGroups = useMemo(() => { + let currentTaskIndex = 0; + return groups.map(group => { + const isCurrentGroupCollapsed = collapsedGroups.has(group.id); + const visibleTasksInGroup = isCurrentGroupCollapsed ? [] : allTasks.filter(task => group.taskIds.includes(task.id)); + const tasksForVirtuoso = visibleTasksInGroup.map(task => ({ + ...task, + originalIndex: allTasks.indexOf(task), + })); + + const groupData = { + ...group, + tasks: tasksForVirtuoso, + startIndex: currentTaskIndex, + count: tasksForVirtuoso.length, + }; + currentTaskIndex += tasksForVirtuoso.length; + return groupData; + }); + }, [groups, allTasks, collapsedGroups]); + + const virtuosoGroupCounts = useMemo(() => { + return virtuosoGroups.map(group => group.count); + }, [virtuosoGroups]); + + const virtuosoItems = useMemo(() => { + return virtuosoGroups.flatMap(group => group.tasks); + }, [virtuosoGroups]); // Render functions const renderGroup = useCallback((groupIndex: number) => { - const group = groups[groupIndex]; + const group = virtuosoGroups[groupIndex]; return ( handleGroupCollapse(group.id)} /> ); - }, [groups, groupCounts, collapsedGroups, handleGroupCollapse]); + }, [virtuosoGroups, collapsedGroups, handleGroupCollapse]); const renderTask = useCallback((taskIndex: number) => { - const task = tasks[taskIndex]; + const task = virtuosoItems[taskIndex]; // Get task from the flattened virtuosoItems + if (!task) return null; // Should not happen if logic is correct return ( ); - }, [tasks, visibleColumns]); + }, [virtuosoItems, visibleColumns]); if (loading) return
Loading...
; if (error) return
Error: {error}
; @@ -185,79 +292,99 @@ const TaskListV2: React.FC = ({ projectId }) => { // Log data for debugging console.log('Rendering with:', { groups, - tasks, - groupCounts + allTasks, + virtuosoGroups, + virtuosoGroupCounts, + virtuosoItems, }); return ( -
- {/* Task Filters */} -
- -
+ +
+ {/* Task Filters */} +
+ +
- {/* Column Headers */} -
-
-
- {visibleColumns.map((column, index) => { - const columnStyle: ColumnStyle = { - width: column.width, - ...(column.isSticky ? { - position: 'sticky', - left: index === 0 ? 0 : index === 1 ? 32 : 132, - backgroundColor: 'inherit', - zIndex: 2, - } : {}), - }; + {/* Column Headers */} +
+
+
+ {visibleColumns.map((column, index) => { + const columnStyle: ColumnStyle = { + width: column.width, + // Removed sticky functionality to prevent overlap with group headers + // ...(column.isSticky ? { + // position: 'sticky', + // left: index === 0 ? 0 : index === 1 ? 32 : 132, + // backgroundColor: 'inherit', + // zIndex: 2, + // } : {}), + }; - return ( -
- {column.id === 'dragHandle' ? ( - - ) : ( - column.label - )} -
- ); - })} + return ( +
+ {column.id === 'dragHandle' ? ( + + ) : ( + column.label + )} +
+ ); + })} +
+
+ + {/* Task List */} +
+ task.id)} + strategy={verticalListSortingStrategy} + > + (({ style, children }, ref) => ( +
+ {children} +
+ )), + }} + /> +
- {/* Task List */} -
- ( -
- {children} -
- ), - List: React.forwardRef(({ style, children }, ref) => ( -
- {children} -
- )), - }} - /> -
+ {/* Drag Overlay */} + + {activeId ? ( +
+
+ + {allTasks.find(task => task.id === activeId)?.name || 'Task'} + +
+
+ ) : null} +
-
+ ); }; diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index 043fc67c..eb9e040c 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -1,9 +1,16 @@ import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { HolderOutlined } from '@ant-design/icons'; import { Task } from '@/types/task-management.types'; +import { InlineMember } from '@/types/teamMembers/inlineMember.types'; import Avatar from '@/components/Avatar'; +import AssigneeSelector from '@/components/AssigneeSelector'; import { format } from 'date-fns'; import { Bars3Icon } from '@heroicons/react/24/outline'; import { ClockIcon } from '@heroicons/react/24/outline'; +import AvatarGroup from '../AvatarGroup'; +import { DEFAULT_TASK_NAME } from '@/shared/constants'; interface TaskRowProps { task: Task; @@ -14,49 +21,100 @@ interface TaskRowProps { }>; } +// Utility function to get task display name with fallbacks +const getTaskDisplayName = (task: Task): string => { + // Check each field and only use if it has actual content after trimming + if (task.title && task.title.trim()) return task.title.trim(); + if (task.name && task.name.trim()) return task.name.trim(); + if (task.task_key && task.task_key.trim()) return task.task_key.trim(); + return DEFAULT_TASK_NAME; +}; + const TaskRow: React.FC = ({ task, visibleColumns }) => { + // Drag and drop functionality + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: task.id, + data: { + type: 'task', + task, + }, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + // Convert Task to IProjectTask format for AssigneeSelector compatibility + const convertTaskToProjectTask = (task: Task) => { + return { + id: task.id, + name: getTaskDisplayName(task), + task_key: task.task_key || getTaskDisplayName(task), + assignees: + task.assignee_names?.map((assignee: InlineMember, index: number) => ({ + team_member_id: assignee.team_member_id || `assignee-${index}`, + id: assignee.team_member_id || `assignee-${index}`, + project_member_id: assignee.team_member_id || `assignee-${index}`, + name: assignee.name || '', + })) || [], + parent_task_id: task.parent_task_id, + // Add other required fields with defaults + status_id: undefined, + project_id: undefined, + manual_progress: undefined, // Required field + }; + }; + const renderColumn = (columnId: string, width: string, isSticky?: boolean, index?: number) => { const baseStyle = { width, - ...(isSticky ? { - position: 'sticky' as const, - left: index === 0 ? 0 : index === 1 ? 32 : 132, - backgroundColor: 'inherit', - zIndex: 1, - } : {}), + // Removed sticky functionality to prevent overlap with group headers + // ...(isSticky + // ? { + // position: 'sticky' as const, + // left: index === 0 ? 0 : index === 1 ? 32 : 132, + // backgroundColor: 'inherit', + // zIndex: 1, + // } + // : {}), }; switch (columnId) { case 'dragHandle': return (
- +
); case 'taskKey': return ( -
+
- {task.task_key} + {task.task_key || 'N/A'}
); case 'title': return ( -
+
- {task.title || task.name} + {getTaskDisplayName(task)}
); @@ -64,7 +122,7 @@ const TaskRow: React.FC = ({ task, visibleColumns }) => { case 'status': return (
- = ({ task, visibleColumns }) => { case 'assignees': return (
- {task.assignee_names?.slice(0, 3).map((assignee, index) => ( - - ))} - {(task.assignee_names?.length || 0) > 3 && ( - - +{task.assignee_names!.length - 3} - - )} + } + {/* Add AssigneeSelector for adding/managing assignees */} +
); case 'priority': return (
- @@ -183,7 +245,7 @@ const TaskRow: React.FC = ({ task, visibleColumns }) => { case 'estimation': return (
- {task.timeTracking.estimated && ( + {task.timeTracking?.estimated && ( {task.timeTracking.estimated}h @@ -216,9 +278,9 @@ const TaskRow: React.FC = ({ task, visibleColumns }) => { case 'createdDate': return (
- {task.createdAt && ( + {task.created_at && ( - {format(new Date(task.createdAt), 'MMM d')} + {format(new Date(task.created_at), 'MMM d')} )}
@@ -239,9 +301,7 @@ const TaskRow: React.FC = ({ task, visibleColumns }) => { return (
{task.reporter && ( - - {task.reporter} - + {task.reporter} )}
); @@ -252,10 +312,18 @@ const TaskRow: React.FC = ({ task, visibleColumns }) => { }; return ( -
- {visibleColumns.map((column, index) => renderColumn(column.id, column.width, column.isSticky, index))} +
+ {visibleColumns.map((column, index) => + renderColumn(column.id, column.width, column.isSticky, index) + )}
); }; -export default TaskRow; \ No newline at end of file +export default TaskRow; diff --git a/worklenz-frontend/src/features/task-management/grouping.slice.ts b/worklenz-frontend/src/features/task-management/grouping.slice.ts index 7af5cd82..182d0903 100644 --- a/worklenz-frontend/src/features/task-management/grouping.slice.ts +++ b/worklenz-frontend/src/features/task-management/grouping.slice.ts @@ -1,10 +1,12 @@ import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit'; -import { GroupingState, TaskGroup, TaskGrouping } from '@/types/task-management.types'; +import { TaskGroup } from '@/types/task-management.types'; import { RootState } from '@/app/store'; -import { selectAllTasks } from './task-management.slice'; +import { selectAllTasksArray } from './task-management.slice'; -interface GroupingState { - currentGrouping: TaskGrouping | null; +type GroupingType = 'status' | 'priority' | 'phase'; + +interface LocalGroupingState { + currentGrouping: GroupingType | null; customPhases: string[]; groupOrder: { status: string[]; @@ -12,10 +14,10 @@ interface GroupingState { phase: string[]; }; groupStates: Record; - collapsedGroups: Set; + collapsedGroups: string[]; } -const initialState: GroupingState = { +const initialState: LocalGroupingState = { currentGrouping: null, customPhases: ['Planning', 'Development', 'Testing', 'Deployment'], groupOrder: { @@ -24,14 +26,14 @@ const initialState: GroupingState = { phase: ['Planning', 'Development', 'Testing', 'Deployment'], }, groupStates: {}, - collapsedGroups: new Set(), + collapsedGroups: [], }; const groupingSlice = createSlice({ name: 'grouping', initialState, reducers: { - setCurrentGrouping: (state, action: PayloadAction) => { + setCurrentGrouping: (state, action: PayloadAction) => { state.currentGrouping = action.payload; }, @@ -54,20 +56,19 @@ const groupingSlice = createSlice({ state.groupOrder.phase = action.payload; }, - updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => { + updateGroupOrder: (state, action: PayloadAction<{ groupType: keyof LocalGroupingState['groupOrder']; order: string[] }>) => { const { groupType, order } = action.payload; state.groupOrder[groupType] = order; }, toggleGroupCollapsed: (state, action: PayloadAction) => { const groupId = action.payload; - const collapsedGroups = new Set(state.collapsedGroups); - if (collapsedGroups.has(groupId)) { - collapsedGroups.delete(groupId); + const isCollapsed = state.collapsedGroups.includes(groupId); + if (isCollapsed) { + state.collapsedGroups = state.collapsedGroups.filter(id => id !== groupId); } else { - collapsedGroups.add(groupId); + state.collapsedGroups.push(groupId); } - state.collapsedGroups = collapsedGroups; }, setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => { @@ -79,11 +80,11 @@ const groupingSlice = createSlice({ }, collapseAllGroups: (state, action: PayloadAction) => { - state.collapsedGroups = new Set(action.payload); + state.collapsedGroups = action.payload; }, expandAllGroups: state => { - state.collapsedGroups = new Set(); + state.collapsedGroups = []; }, resetGrouping: () => initialState, @@ -108,54 +109,59 @@ export const selectCurrentGrouping = (state: RootState) => state.grouping.curren export const selectCustomPhases = (state: RootState) => state.grouping.customPhases; export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder; export const selectGroupStates = (state: RootState) => state.grouping.groupStates; -export const selectCollapsedGroups = (state: RootState) => state.grouping.collapsedGroups; +export const selectCollapsedGroups = (state: RootState) => new Set(state.grouping.collapsedGroups); export const selectIsGroupCollapsed = (state: RootState, groupId: string) => - state.grouping.collapsedGroups.has(groupId); + state.grouping.collapsedGroups.includes(groupId); // Complex selectors using createSelector for memoization export const selectCurrentGroupOrder = createSelector( [selectCurrentGrouping, selectGroupOrder], - (currentGrouping, groupOrder) => groupOrder[currentGrouping] || [] + (currentGrouping, groupOrder) => { + if (!currentGrouping) return []; + return groupOrder[currentGrouping] || []; + } ); export const selectTaskGroups = createSelector( - [selectAllTasks, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates], + [selectAllTasksArray, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates], (tasks, currentGrouping, groupOrder, groupStates) => { const groups: TaskGroup[] = []; + if (!currentGrouping) return groups; + // 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; - }) - ), - ]; + : Array.from(new Set( + tasks.map(task => { + if (currentGrouping === 'status') return task.status; + if (currentGrouping === 'priority') return task.priority; + return task.phase; + }) + )); groupValues.forEach(value => { + if (!value) return; // Skip undefined values + 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); + .sort((a, b) => (a.order || 0) - (b.order || 0)); 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), + type: currentGrouping, color: getGroupColor(currentGrouping, value), + collapsed: groupStates[groupId]?.collapsed || false, + groupValue: value, }); }); @@ -164,15 +170,17 @@ export const selectTaskGroups = createSelector( ); export const selectTasksByCurrentGrouping = createSelector( - [selectAllTasks, selectCurrentGrouping], + [selectAllTasksArray, selectCurrentGrouping], (tasks, currentGrouping) => { const grouped: Record = {}; + if (!currentGrouping) return grouped; + tasks.forEach(task => { let key: string; if (currentGrouping === 'status') key = task.status; else if (currentGrouping === 'priority') key = task.priority; - else key = task.phase; + else key = task.phase || 'Development'; if (!grouped[key]) grouped[key] = []; grouped[key].push(task); @@ -180,7 +188,7 @@ export const selectTasksByCurrentGrouping = createSelector( // Sort tasks within each group by order Object.keys(grouped).forEach(key => { - grouped[key].sort((a, b) => a.order - b.order); + grouped[key].sort((a, b) => (a.order || 0) - (b.order || 0)); }); return grouped; @@ -188,7 +196,7 @@ export const selectTasksByCurrentGrouping = createSelector( ); // Helper function to get group colors -const getGroupColor = (groupType: string, value: string): string => { +const getGroupColor = (groupType: GroupingType, value: string): string => { const colorMaps = { status: { todo: '#f0f0f0', @@ -209,7 +217,8 @@ const getGroupColor = (groupType: string, value: string): string => { }, }; - return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9'; + const colorMap = colorMaps[groupType]; + return (colorMap as any)?.[value] || '#d9d9d9'; }; export default groupingSlice.reducer; diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index b2f4c4cb..e94d99a4 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -14,6 +14,7 @@ import { ITaskListV3Response, } from '@/api/tasks/tasks.api.service'; import logger from '@/utils/errorLogger'; +import { DEFAULT_TASK_NAME } from '@/shared/constants'; // Helper function to safely convert time values const convertTimeValue = (value: any): number => { @@ -137,7 +138,7 @@ export const fetchTasks = createAsyncThunk( group.tasks.map((task: any) => ({ id: task.id, task_key: task.task_key || '', - title: task.name || '', + 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', @@ -221,13 +222,17 @@ export const fetchTasksV3 = createAsyncThunk( // Log raw response for debugging console.log('Raw API response:', response.body); + console.log('Sample task from backend:', response.body.allTasks?.[0]); // Ensure tasks are properly normalized const tasks = response.body.allTasks.map((task: any) => { const now = new Date().toISOString(); + + + return { id: task.id, - title: task.name || '', + title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME, description: task.description || '', status: task.status || 'todo', priority: task.priority || 'medium', diff --git a/worklenz-frontend/src/types/task-management.types.ts b/worklenz-frontend/src/types/task-management.types.ts index f8e554c8..708ebf20 100644 --- a/worklenz-frontend/src/types/task-management.types.ts +++ b/worklenz-frontend/src/types/task-management.types.ts @@ -3,13 +3,21 @@ import { EntityState } from '@reduxjs/toolkit'; export interface Task { id: string; - title: string; + title?: string; // Make title optional since it can be empty from database + name?: string; // Alternative name field + task_key?: string; // Task key field description?: string; status: string; priority: string; phase?: string; assignee?: string; + assignee_names?: InlineMember[]; // Array of assigned members + names?: InlineMember[]; // Alternative names field due_date?: string; + dueDate?: string; // Alternative due date field + startDate?: string; // Start date field + completedAt?: string; // Completion date + updatedAt?: string; // Update timestamp created_at: string; updated_at: string; sub_tasks?: Task[]; @@ -27,6 +35,11 @@ export interface Task { has_dependencies?: boolean; schedule_id?: string | null; order?: number; + reporter?: string; // Reporter field + timeTracking?: { // Time tracking information + logged?: number; + estimated?: number; + }; // Add any other task properties as needed } diff --git a/worklenz-frontend/src/utils/colorUtils.ts b/worklenz-frontend/src/utils/colorUtils.ts index b905e9dd..1d7e9c0c 100644 --- a/worklenz-frontend/src/utils/colorUtils.ts +++ b/worklenz-frontend/src/utils/colorUtils.ts @@ -1,3 +1,20 @@ export const tagBackground = (color: string): string => { return `${color}1A`; // 1A is 10% opacity in hex }; + +export const getContrastColor = (hexcolor: string): string => { + // If a color is not a valid hex, default to a sensible contrast + if (!/^#([A-Fa-f0-9]{3}){1,2}$/.test(hexcolor)) { + return '#000000'; // Default to black for invalid colors + } + + const r = parseInt(hexcolor.slice(1, 3), 16); + const g = parseInt(hexcolor.slice(3, 5), 16); + const b = parseInt(hexcolor.slice(5, 7), 16); + + // Perceptual luminance calculation (from WCAG 2.0) + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + + // Use a threshold to decide between black and white text + return luminance > 0.5 ? '#000000' : '#FFFFFF'; +};