diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx index e1d7248a..ae1e3c35 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx @@ -19,7 +19,6 @@ import { useAuthService } from '@/hooks/useAuth'; import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; import alertService from '@/services/alerts/alertService'; import logger from '@/utils/errorLogger'; -import Skeleton from 'antd/es/skeleton/Skeleton'; import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; @@ -148,11 +147,11 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (!sourceGroup || !targetGroup) return; - const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId); if (taskIdx === -1) return; const movedTask = sourceGroup.tasks[taskIdx]; + let didStatusChange = false; if (groupBy === 'status' && movedTask.id) { if (sourceGroup.id !== targetGroup.id) { const canContinue = await checkTaskDependencyStatus(movedTask.id, targetGroupId); @@ -163,6 +162,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project ); return; } + didStatusChange = true; } } let insertIdx = hoveredTaskIdx; @@ -259,6 +259,18 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project team_id: teamId, }); + // Emit progress update if status changed + if (didStatusChange) { + socket.emit( + SocketEvents.TASK_STATUS_CHANGE.toString(), + JSON.stringify({ + task_id: movedTask.id, + status_id: targetGroupId, + parent_task: movedTask.parent_task_id || null, + team_id: teamId, + }) + ); + } } setDraggedTaskId(null); diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index 4046a8f2..fa4f0ddb 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -15,6 +15,7 @@ import { SocketEvents } from '@/shared/socket-events'; import { getUserSession } from '@/utils/session-helper'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice'; +import TaskProgressCircle from './TaskProgressCircle'; // Simple Portal component const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -69,7 +70,6 @@ const TaskCard: React.FC = memo(({ const d = selectedDate || new Date(); return new Date(d.getFullYear(), d.getMonth(), 1); }); - const [showSubtasks, setShowSubtasks] = useState(false); useEffect(() => { setSelectedDate(task.end_date ? new Date(task.end_date) : null); @@ -202,7 +202,11 @@ const TaskCard: React.FC = memo(({ return ( <> -
+
+ {/* Progress circle at top right */} +
+ +
onTaskDragStart(e, task.id!, groupId)} diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx new file mode 100644 index 00000000..b0303b1a --- /dev/null +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx @@ -0,0 +1,42 @@ +import { IProjectTask } from "@/types/project/projectTasksViewModel.types"; + +// Add a simple circular progress component +const TaskProgressCircle: React.FC<{ task: IProjectTask; size?: number }> = ({ task, size = 28 }) => { + const progress = typeof task.complete_ratio === 'number' + ? task.complete_ratio + : (typeof task.progress === 'number' ? task.progress : 0); + const strokeWidth = 1.5; + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (progress / 100) * circumference; + return ( + + + + {task.complete_ratio && + {Math.round(progress)} + } + + ); +}; + +export default TaskProgressCircle; \ No newline at end of file diff --git a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css deleted file mode 100644 index f8dd177a..00000000 --- a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css +++ /dev/null @@ -1,101 +0,0 @@ -.performance-monitor { - position: fixed; - top: 80px; - right: 16px; - width: 280px; - z-index: 1000; - background: var(--ant-color-bg-elevated); - border: 1px solid var(--ant-color-border); - box-shadow: 0 4px 12px var(--ant-color-shadow); -} - -.performance-monitor-header { - display: flex; - justify-content: space-between; - align-items: center; - font-weight: 600; - color: var(--ant-color-text); -} - -.performance-status { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.performance-metrics { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; - margin-bottom: 12px; -} - -.performance-metrics .ant-statistic { - text-align: center; -} - -.performance-metrics .ant-statistic-title { - font-size: 12px; - color: var(--ant-color-text-secondary); - margin-bottom: 4px; -} - -.performance-metrics .ant-statistic-content { - font-size: 14px; - color: var(--ant-color-text); -} - -.virtualization-status { - grid-column: 1 / -1; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-top: 1px solid var(--ant-color-border); -} - -.status-label { - font-size: 12px; - color: var(--ant-color-text-secondary); - font-weight: 500; -} - -.performance-tips { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--ant-color-border); -} - -.performance-tips h4 { - font-size: 12px; - color: var(--ant-color-text); - margin-bottom: 8px; - font-weight: 600; -} - -.performance-tips ul { - margin: 0; - padding-left: 16px; -} - -.performance-tips li { - font-size: 11px; - color: var(--ant-color-text-secondary); - margin-bottom: 4px; - line-height: 1.4; -} - -/* Responsive design */ -@media (max-width: 768px) { - .performance-monitor { - position: static; - width: 100%; - margin-bottom: 16px; - } - - .performance-metrics { - grid-template-columns: 1fr; - gap: 8px; - } -} diff --git a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx deleted file mode 100644 index 1d203c9c..00000000 --- a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { Card, Statistic, Tooltip, Badge } from 'antd'; -import { useSelector } from 'react-redux'; -import { RootState } from '@/app/store'; -import './PerformanceMonitor.css'; - -const PerformanceMonitor: React.FC = () => { - const { performanceMetrics } = useSelector((state: RootState) => state.enhancedKanbanReducer); - - // Only show if there are tasks loaded - if (performanceMetrics.totalTasks === 0) { - return null; - } - - const getPerformanceStatus = () => { - if (performanceMetrics.totalTasks > 1000) return 'critical'; - if (performanceMetrics.totalTasks > 500) return 'warning'; - if (performanceMetrics.totalTasks > 100) return 'good'; - return 'excellent'; - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'critical': - return 'red'; - case 'warning': - return 'orange'; - case 'good': - return 'blue'; - case 'excellent': - return 'green'; - default: - return 'default'; - } - }; - - const status = getPerformanceStatus(); - const statusColor = getStatusColor(status); - - return ( - - Performance Monitor - -
- } - > -
- - - - - - - - - - - - - -
- Virtualization: - -
-
-
- - {performanceMetrics.totalTasks > 500 && ( -
-

Performance Tips:

-
    -
  • Use filters to reduce the number of visible tasks
  • -
  • Consider grouping by different criteria
  • -
  • Virtualization is automatically enabled for large groups
  • -
-
- )} - - ); -}; - -export default React.memo(PerformanceMonitor); diff --git a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css deleted file mode 100644 index 478ac4ac..00000000 --- a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css +++ /dev/null @@ -1,60 +0,0 @@ -.virtualized-task-list { - background: transparent; - border-radius: 6px; - overflow: hidden; -} - -.virtualized-task-row { - padding: 4px 0; - display: flex; - align-items: stretch; -} - -.virtualized-empty-state { - display: flex; - align-items: center; - justify-content: center; - background: var(--ant-color-bg-container); - border-radius: 6px; - border: 2px dashed var(--ant-color-border); -} - -.empty-message { - color: var(--ant-color-text-secondary); - font-size: 14px; - font-weight: 500; -} - -/* Ensure virtualized list works well with drag and drop */ -.virtualized-task-list .react-window__inner { - overflow: visible !important; -} - -/* Performance optimizations */ -.virtualized-task-list * { - will-change: transform; -} - -/* Smooth scrolling */ -.virtualized-task-list { - scroll-behavior: smooth; -} - -/* Custom scrollbar for better UX */ -.virtualized-task-list::-webkit-scrollbar { - width: 6px; -} - -.virtualized-task-list::-webkit-scrollbar-track { - background: var(--ant-color-bg-container); - border-radius: 3px; -} - -.virtualized-task-list::-webkit-scrollbar-thumb { - background: var(--ant-color-border); - border-radius: 3px; -} - -.virtualized-task-list::-webkit-scrollbar-thumb:hover { - background: var(--ant-color-text-tertiary); -} diff --git a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx deleted file mode 100644 index 7269bf2e..00000000 --- a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useMemo, useCallback } from 'react'; -import { FixedSizeList as List } from 'react-window'; -import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; -import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard'; -import './VirtualizedTaskList.css'; - -interface VirtualizedTaskListProps { - tasks: IProjectTask[]; - height: number; - itemHeight?: number; - activeTaskId?: string | null; - overId?: string | null; - onTaskRender?: (task: IProjectTask, index: number) => void; -} - -const VirtualizedTaskList: React.FC = ({ - tasks, - height, - itemHeight = 80, - activeTaskId, - overId, - onTaskRender, -}) => { - // Memoize task data to prevent unnecessary re-renders - const taskData = useMemo( - () => ({ - tasks, - activeTaskId, - overId, - onTaskRender, - }), - [tasks, activeTaskId, overId, onTaskRender] - ); - - // Row renderer for virtualized list - const Row = useCallback( - ({ index, style }: { index: number; style: React.CSSProperties }) => { - const task = tasks[index]; - if (!task) return null; - - // Call onTaskRender callback if provided - onTaskRender?.(task, index); - - return ( - - ); - }, - [tasks, activeTaskId, overId, onTaskRender] - ); - - // Memoize the list component to prevent unnecessary re-renders - const VirtualizedList = useMemo( - () => ( - - {Row} - - ), - [height, tasks.length, itemHeight, taskData, Row] - ); - - if (tasks.length === 0) { - return ( -
-
- No tasks in this group -
-
- ); - } - - return VirtualizedList; -}; - -export default React.memo(VirtualizedTaskList);