From 8f913b0f4e89e7b04a92c667bfb750c2a962a949 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Fri, 2 May 2025 07:37:40 +0530 Subject: [PATCH] Add task progress tracking methods documentation and enhance progress update logic - Introduced a new markdown file detailing task progress tracking methods: manual, weighted, and time-based. - Updated backend logic to include complete ratio calculations for tasks. - Improved socket command for task progress updates, enabling recursive updates for ancestor tasks. - Enhanced frontend components to reflect progress changes based on the selected tracking method, including updates to task display and progress input handling. - Added support for manual progress flag in task model to facilitate accurate progress representation. --- task-progress-methods.md | 244 ++++++++++++++++++ .../src/controllers/tasks-controller-v2.ts | 1 + .../commands/on-update-task-progress.ts | 67 +++-- .../src/features/tasks/tasks.slice.ts | 29 ++- .../task-list-progress-cell.tsx | 44 +++- .../project/projectTasksViewModel.types.ts | 1 + 6 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 task-progress-methods.md diff --git a/task-progress-methods.md b/task-progress-methods.md new file mode 100644 index 00000000..11b18ef5 --- /dev/null +++ b/task-progress-methods.md @@ -0,0 +1,244 @@ +# Task Progress Tracking Methods in WorkLenz + +## Overview +WorkLenz supports three different methods for tracking task progress, each suitable for different project management approaches: + +1. **Manual Progress** - Direct input of progress percentages +2. **Weighted Progress** - Tasks have weights that affect overall progress calculation +3. **Time-based Progress** - Progress calculated based on estimated time vs. time spent + +These modes can be selected when creating or editing a project in the project drawer. + +## 1. Manual Progress Mode + +This mode allows direct input of progress percentages for individual tasks without subtasks. + +**Implementation:** +- Enabled by setting `use_manual_progress` to true in the project settings +- Progress is updated through the `on-update-task-progress.ts` socket event handler +- The UI shows a manual progress input slider in the task drawer for tasks without subtasks +- Updates the database with `progress_value` and sets `manual_progress` flag to true + +**Calculation Logic:** +- For tasks without subtasks: Uses the manually set progress value +- For parent tasks: Calculates the average of all subtask progress values +- Subtask progress comes from either manual values or completion status (0% or 100%) + +**Code Example:** +```typescript +// Manual progress update via socket.io +socket?.emit(SocketEvents.UPDATE_TASK_PROGRESS.toString(), JSON.stringify({ + task_id: task.id, + progress_value: value, + parent_task_id: task.parent_task_id +})); +``` + +### Showing Progress in Subtask Rows + +When manual progress is enabled in a project, progress is shown in the following ways: + +1. **In Task List Views**: + - Subtasks display their individual progress values in the progress column + - Parent tasks display the calculated average progress of all subtasks + +2. **Implementation Details**: + - The progress values are stored in the `progress_value` column in the database + - For subtasks with manual progress set, the value is shown directly + - For subtasks without manual progress, the completion status determines the value (0% or 100%) + - The task view model includes both `progress` and `complete_ratio` properties + +**Relevant Components:** +```typescript +// From task-list-progress-cell.tsx +const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => { + return task.is_sub_task ? null : ( + + + + ); +}; +``` + +**Task Progress Calculation in Backend:** +```typescript +// From tasks-controller-base.ts +// For tasks without subtasks, respect manual progress if set +if (task.manual_progress === true && task.progress_value !== null) { + // For manually set progress, use that value directly + task.progress = parseInt(task.progress_value); + task.complete_ratio = parseInt(task.progress_value); +} +``` + +## 2. Weighted Progress Mode + +This mode allows assigning different weights to subtasks to reflect their relative importance in the overall task or project progress. + +**Implementation:** +- Enabled by setting `use_weighted_progress` to true in the project settings +- Weights are updated through the `on-update-task-weight.ts` socket event handler +- The UI shows a weight input for subtasks in the task drawer +- Default weight is 100 if not specified + +**Calculation Logic:** +- Progress is calculated using a weighted average: `SUM(progress_value * weight) / SUM(weight)` +- This gives more influence to tasks with higher weights +- A parent task's progress is the weighted average of its subtasks' progress + +**Code Example:** +```typescript +// Weight update via socket.io +socket?.emit(SocketEvents.UPDATE_TASK_WEIGHT.toString(), JSON.stringify({ + task_id: task.id, + weight: value, + parent_task_id: task.parent_task_id +})); +``` + +## 3. Time-based Progress Mode + +This mode calculates progress based on estimated time vs. actual time spent. + +**Implementation:** +- Enabled by setting `use_time_progress` to true in the project settings +- Uses task time estimates (hours and minutes) for calculation +- No separate socket handler needed as it's calculated automatically + +**Calculation Logic:** +- Progress is calculated using time as the weight: `SUM(progress_value * estimated_minutes) / SUM(estimated_minutes)` +- For tasks with time tracking, estimated vs. actual time can be factored in +- Parent task progress is weighted by the estimated time of each subtask + +**SQL Example:** +```sql +WITH subtask_progress AS ( + SELECT + CASE + WHEN manual_progress IS TRUE AND progress_value IS NOT NULL THEN + progress_value + ELSE + CASE + WHEN EXISTS( + SELECT 1 + FROM tasks_with_status_view + WHERE tasks_with_status_view.task_id = t.id + AND is_done IS TRUE + ) THEN 100 + ELSE 0 + END + END AS progress_value, + COALESCE(total_hours * 60 + total_minutes, 0) AS estimated_minutes + FROM tasks t + WHERE t.parent_task_id = _task_id + AND t.archived IS FALSE +) +SELECT COALESCE( + SUM(progress_value * estimated_minutes) / NULLIF(SUM(estimated_minutes), 0), + 0 +) +FROM subtask_progress +INTO _ratio; +``` + +## Default Progress Tracking (when no special mode is selected) + +If no specific progress mode is enabled, the system falls back to a traditional completion-based calculation: + +**Implementation:** +- Default mode when all three special modes are disabled +- Based on task completion status only + +**Calculation Logic:** +- For tasks without subtasks: 0% if not done, 100% if done +- For parent tasks: `(completed_tasks / total_tasks) * 100` +- Counts both the parent and all subtasks in the calculation + +**SQL Example:** +```sql +-- Traditional calculation based on completion status +SELECT (CASE + WHEN EXISTS(SELECT 1 + FROM tasks_with_status_view + WHERE tasks_with_status_view.task_id = _task_id + AND is_done IS TRUE) THEN 1 + ELSE 0 END) +INTO _parent_task_done; + +SELECT COUNT(*) +FROM tasks_with_status_view +WHERE parent_task_id = _task_id + AND is_done IS TRUE +INTO _sub_tasks_done; + +_total_completed = _parent_task_done + _sub_tasks_done; +_total_tasks = _sub_tasks_count + 1; -- +1 for the parent task + +IF _total_tasks = 0 THEN + _ratio = 0; +ELSE + _ratio = (_total_completed / _total_tasks) * 100; +END IF; +``` + +## Technical Implementation Details + +The progress calculation logic is implemented in PostgreSQL functions, primarily in the `get_task_complete_ratio` function. Progress updates flow through the system as follows: + +1. **User Action**: User updates task progress or weight in the UI +2. **Socket Event**: Client emits socket event (UPDATE_TASK_PROGRESS or UPDATE_TASK_WEIGHT) +3. **Server Handler**: Server processes the event in the respective handler function +4. **Database Update**: Progress/weight value is updated in the database +5. **Recalculation**: If needed, parent task progress is recalculated +6. **Broadcast**: Changes are broadcast to all clients in the project room +7. **UI Update**: Client UI updates to reflect the new progress values + +This architecture allows for real-time updates and consistent progress calculation across all clients. + +## Associated Files and Components + +### Backend Files + +1. **Socket Event Handlers**: + - `worklenz-backend/src/socket.io/commands/on-update-task-progress.ts` - Handles manual progress updates + - `worklenz-backend/src/socket.io/commands/on-update-task-weight.ts` - Handles task weight updates + +2. **Database Functions**: + - `worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql` - Contains the `get_task_complete_ratio` function that calculates progress based on the selected method + - Functions that support project creation/updates with progress mode settings: + - `create_project` + - `update_project` + +3. **Controllers**: + - `worklenz-backend/src/controllers/project-workload/workload-gannt-base.ts` - Contains the `calculateTaskCompleteRatio` method + - `worklenz-backend/src/controllers/projects-controller.ts` - Handles project-level progress calculations + - `worklenz-backend/src/controllers/tasks-controller-base.ts` - Handles task progress calculation and updates task view models + +### Frontend Files + +1. **Project Configuration**: + - `worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx` - Contains UI for selecting progress method when creating/editing projects + +2. **Progress Visualization Components**: + - `worklenz-frontend/src/components/project-list/project-list-table/project-list-progress/progress-list-progress.tsx` - Displays project progress + - `worklenz-frontend/src/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress.tsx` - Displays task progress + - `worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx` - Alternative task progress cell + - `worklenz-frontend/src/components/task-list-common/task-row/task-row-progress/task-row-progress.tsx` - Displays progress in task rows + +3. **Progress Input Components**: + - `worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx` - Component for inputting task progress/weight + +## Choosing the Right Progress Method + +Each progress method is suitable for different types of projects: + +- **Manual Progress**: Best for creative work where progress is subjective +- **Weighted Progress**: Ideal for projects where some tasks are more significant than others +- **Time-based Progress**: Perfect for projects where time estimates are reliable and important + +Project managers can choose the appropriate method when creating or editing a project in the project drawer, based on their team's workflow and project requirements. \ No newline at end of file diff --git a/worklenz-backend/src/controllers/tasks-controller-v2.ts b/worklenz-backend/src/controllers/tasks-controller-v2.ts index 131be72a..e3992563 100644 --- a/worklenz-backend/src/controllers/tasks-controller-v2.ts +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -198,6 +198,7 @@ export default class TasksControllerV2 extends TasksControllerBase { (SELECT use_manual_progress FROM projects WHERE id = t.project_id) AS project_use_manual_progress, (SELECT use_weighted_progress FROM projects WHERE id = t.project_id) AS project_use_weighted_progress, (SELECT use_time_progress FROM projects WHERE id = t.project_id) AS project_use_time_progress, + (SELECT (get_task_complete_ratio(t.id)).ratio) AS complete_ratio, (SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id, (SELECT name diff --git a/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts b/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts index 90d3ca3a..ac8ebbdb 100644 --- a/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts +++ b/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts @@ -35,7 +35,7 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str // Get the current progress value to log the change const currentProgressResult = await db.query( - "SELECT progress_value, project_id, FROM tasks WHERE id = $1", + "SELECT progress_value, project_id FROM tasks WHERE id = $1", [task_id] ); @@ -70,24 +70,8 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str console.log(`Emitted progress update for task ${task_id} to project room ${projectId}`); - // If this is a subtask, update the parent task's progress - if (parent_task_id) { - const progressRatio = await db.query( - "SELECT get_task_complete_ratio($1) as ratio", - [parent_task_id] - ); - - console.log(`Updated parent task ${parent_task_id} progress: ${progressRatio?.rows[0]?.ratio}`); - - // Emit the parent task's updated progress - io.to(projectId).emit( - SocketEvents.TASK_PROGRESS_UPDATED.toString(), - { - task_id: parent_task_id, - progress_value: progressRatio?.rows[0]?.ratio - } - ); - } + // Recursively update all ancestors in the task hierarchy + await updateTaskAncestors(io, projectId, parent_task_id); // Notify that project updates are available notifyProjectUpdates(socket, task_id); @@ -95,4 +79,49 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str } catch (error) { log_error(error); } +} + +/** + * Recursively updates all ancestor tasks' progress when a subtask changes + * @param io Socket.io instance + * @param projectId Project ID for room broadcasting + * @param taskId The task ID to update (starts with the parent task) + */ +async function updateTaskAncestors(io: any, projectId: string, taskId: string | null) { + if (!taskId) return; + + try { + // Get the current task's progress ratio + const progressRatio = await db.query( + "SELECT get_task_complete_ratio($1) as ratio", + [taskId] + ); + + const ratio = progressRatio?.rows[0]?.ratio; + console.log(`Updated task ${taskId} progress: ${ratio}`); + + // Emit the updated progress + io.to(projectId).emit( + SocketEvents.TASK_PROGRESS_UPDATED.toString(), + { + task_id: taskId, + progress_value: ratio + } + ); + + // Find this task's parent to continue the recursive update + const parentResult = await db.query( + "SELECT parent_task_id FROM tasks WHERE id = $1", + [taskId] + ); + + const parentTaskId = parentResult.rows[0]?.parent_task_id; + + // If there's a parent, recursively update it + if (parentTaskId) { + await updateTaskAncestors(io, projectId, parentTaskId); + } + } catch (error) { + log_error(`Error updating ancestor task ${taskId}: ${error}`); + } } \ No newline at end of file diff --git a/worklenz-frontend/src/features/tasks/tasks.slice.ts b/worklenz-frontend/src/features/tasks/tasks.slice.ts index dbc2f955..320a5cd1 100644 --- a/worklenz-frontend/src/features/tasks/tasks.slice.ts +++ b/worklenz-frontend/src/features/tasks/tasks.slice.ts @@ -572,14 +572,29 @@ const taskSlice = createSlice({ ) => { const { taskId, progress, totalTasksCount, completedCount } = action.payload; - for (const group of state.taskGroups) { - const task = group.tasks.find(task => task.id === taskId); - if (task) { - task.complete_ratio = progress; - task.total_tasks_count = totalTasksCount; - task.completed_count = completedCount; - break; + // Helper function to find and update a task at any nesting level + const findAndUpdateTask = (tasks: IProjectTask[]) => { + for (const task of tasks) { + if (task.id === taskId) { + task.complete_ratio = progress; + task.total_tasks_count = totalTasksCount; + task.completed_count = completedCount; + return true; + } + + // Check subtasks if they exist + if (task.sub_tasks && task.sub_tasks.length > 0) { + const found = findAndUpdateTask(task.sub_tasks); + if (found) return true; + } } + return false; + }; + + // Try to find and update the task in any task group + for (const group of state.taskGroups) { + const found = findAndUpdateTask(group.tasks); + if (found) break; } }, diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx index 4589e5aa..1db3a56c 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx @@ -1,20 +1,54 @@ +import React from 'react'; import { Progress, Tooltip } from 'antd'; import './task-list-progress-cell.css'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; +import { useAppSelector } from '@/hooks/useAppSelector'; type TaskListProgressCellProps = { task: IProjectTask; }; const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => { - return task.is_sub_task ? null : ( - + const { project } = useAppSelector(state => state.projectReducer); + const isManualProgressEnabled = project?.use_manual_progress; + const isSubtask = task.is_sub_task; + const hasManualProgress = task.manual_progress; + + // Handle different cases: + // 1. For subtasks when manual progress is enabled, show the progress + // 2. For parent tasks, always show progress + // 3. For subtasks when manual progress is not enabled, don't show progress (null) + + if (isSubtask && !isManualProgressEnabled) { + return null; // Don't show progress for subtasks when manual progress is disabled + } + + // For parent tasks, show completion ratio with task count tooltip + if (!isSubtask) { + return ( + + = 100 ? 9 : 7} + /> + + ); + } + + // For subtasks with manual progress enabled, show the progress + return ( + = 100 ? 9 : 7} + strokeWidth={(task.progress || 0) >= 100 ? 9 : 7} /> ); diff --git a/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts b/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts index 94e93c4c..4ab36c27 100644 --- a/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts +++ b/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts @@ -16,6 +16,7 @@ export interface ITaskStatusCategory { } export interface IProjectTask { + manual_progress: any; due_time?: string; id?: string; name?: string;