Files
worklenz/docs/task-progress-methods.md
chamikaJ a5b881c609 Enhance task progress calculation and UI handling
- Updated task progress calculation logic to incorporate weights and time-based estimations for subtasks.
- Improved SQL migrations to support new progress calculation methods and ensure accurate parent task updates.
- Enhanced frontend components to conditionally display progress inputs based on task type and project settings.
- Implemented socket events for real-time updates on subtask counts and progress changes, ensuring consistent UI behavior.
- Added logging for progress updates and task state changes to improve debugging and user experience.
2025-05-02 13:21:32 +05:30

22 KiB

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. Only one progress method can be enabled at a time. If none of these methods are enabled, progress will be calculated based on task completion status as described in the "Default Progress Tracking" section below.

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:

// 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
}));

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
  • Manual progress input is still required for tasks without subtasks
  • Default weight is 100 if not specified
  • Weight values range from 0 to 100%

Calculation Logic:

  • For tasks without subtasks: Uses the manually entered progress value
  • 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 values

Code Example:

// 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
  • Manual progress input is still required for tasks without subtasks
  • No separate socket handler needed as it's calculated automatically

Calculation Logic:

  • For tasks without subtasks: Uses the manually entered progress value
  • 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:

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:

-- 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.

Manual Progress Input Implementation

Regardless of which progress tracking method is selected for a project, tasks without subtasks (leaf tasks) require manual progress input. This section details how manual progress input is implemented and used across all progress tracking methods.

UI Component

The manual progress input component is implemented in worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx and includes:

  1. Progress Slider: A slider UI control that allows users to set progress values from 0% to 100%
  2. Progress Input Field: A numeric input field that accepts direct entry of progress percentage
  3. Progress Display: Visual representation of the current progress value

The component is conditionally rendered in the task drawer for tasks that don't have subtasks.

Usage Across Progress Methods:

  • In Manual Progress Mode: Only the progress slider/input is shown
  • In Weighted Progress Mode: Both the progress slider/input and weight input are shown
  • In Time-based Progress Mode: The progress slider/input is shown alongside time estimate fields

Progress Update Flow

When a user updates a task's progress manually, the following process occurs:

  1. User Input: User adjusts the progress slider or enters a value in the input field
  2. UI Event Handler: The UI component captures the change event and validates the input
  3. Socket Event Emission: The component emits a UPDATE_TASK_PROGRESS socket event with:
    {
      task_id: task.id,
      progress_value: value, // The new progress value (0-100)
      parent_task_id: task.parent_task_id // For recalculation
    }
    
  4. Server Processing: The socket event handler on the server:
    • Updates the task's progress_value in the database
    • Sets the manual_progress flag to true
    • Triggers recalculation of parent task progress

Progress Calculation Across Methods

The calculation of progress differs based on the active progress method:

  1. For Leaf Tasks (no subtasks) in all methods:

    • Progress is always the manually entered value (progress_value)
    • If the task is marked as completed, progress is automatically set to 100%
  2. For Parent Tasks:

    • Manual Progress Mode: Simple average of all subtask progress values
    • Weighted Progress Mode: Weighted average where each subtask's progress is multiplied by its weight
    • Time-based Progress Mode: Weighted average where each subtask's progress is multiplied by its estimated time
    • Default Mode: Percentage of completed tasks (including parent) vs. total tasks

Detailed Calculation for Weighted Progress Method

In Weighted Progress mode, both the manual progress input and weight assignment are critical components:

  1. Manual Progress Input:

    • For leaf tasks (without subtasks), users must manually input progress percentages (0-100%)
    • If a leaf task is marked as complete, its progress is automatically set to 100%
    • If a leaf task's progress is not manually set, it defaults to 0% (or 100% if completed)
  2. Weight Assignment:

    • Each task can be assigned a weight value between 0-100% (default 100% if not specified)
    • Higher weight values give tasks more influence in parent task progress calculations
    • A weight of 0% means the task doesn't contribute to the parent's progress calculation
  3. Parent Task Calculation: The weighted progress formula is:

    ParentProgress = ∑(SubtaskProgress * SubtaskWeight) / ∑(SubtaskWeight)
    

    Example Calculation: Consider a parent task with three subtasks:

    • Subtask A: Progress 50%, Weight 60%
    • Subtask B: Progress 75%, Weight 20%
    • Subtask C: Progress 25%, Weight 100%

    Calculation:

    ParentProgress = ((50 * 60) + (75 * 20) + (25 * 100)) / (60 + 20 + 100)
    ParentProgress = (3000 + 1500 + 2500) / 180
    ParentProgress = 7000 / 180
    ParentProgress = 38.89%
    

    Notice that Subtask C, despite having the lowest progress, has a significant impact on the parent task progress due to its higher weight.

  4. Zero Weight Handling: Tasks with zero weight are excluded from the calculation:

    • Subtask A: Progress 40%, Weight 50%
    • Subtask B: Progress 80%, Weight 0%

    Calculation:

    ParentProgress = ((40 * 50) + (80 * 0)) / (50 + 0)
    ParentProgress = 2000 / 50
    ParentProgress = 40%
    

    In this case, only Subtask A influences the parent task progress because Subtask B has a weight of 0%.

  5. Default Weight Behavior: When weights aren't explicitly assigned to some tasks:

    • Subtask A: Progress 30%, Weight 60% (explicitly set)
    • Subtask B: Progress 70%, Weight not set (defaults to 100%)
    • Subtask C: Progress 90%, Weight not set (defaults to 100%)

    Calculation:

    ParentProgress = ((30 * 60) + (70 * 100) + (90 * 100)) / (60 + 100 + 100)
    ParentProgress = (1800 + 7000 + 9000) / 260
    ParentProgress = 17800 / 260
    ParentProgress = 68.46%
    

    Note that Subtasks B and C have more influence than Subtask A because they have higher default weights.

  6. All Zero Weights Edge Case: If all subtasks have zero weight, the progress is calculated as 0%:

    ParentProgress = SUM(progress_value * 0) / SUM(0) = 0 / 0 = undefined
    

    The SQL implementation handles this with NULLIF and COALESCE to return 0% in this case.

  7. Actual SQL Implementation: The database function implements the weighted calculation as follows:

    WITH subtask_progress AS (
        SELECT 
            CASE 
                -- If subtask has manual progress, use that value
                WHEN manual_progress IS TRUE AND progress_value IS NOT NULL THEN 
                    progress_value
                -- Otherwise use completion status (0 or 100)
                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(weight, 100) AS weight
        FROM tasks t
        WHERE t.parent_task_id = _task_id
        AND t.archived IS FALSE
    )
    SELECT COALESCE(
        SUM(progress_value * weight) / NULLIF(SUM(weight), 0),
        0
    )
    FROM subtask_progress
    INTO _ratio;
    

    This SQL implementation:

    • Gets all non-archived subtasks of the parent task
    • For each subtask, determines its progress value:
      • If manual progress is set, uses that value
      • Otherwise, uses 100% if the task is done or 0% if not done
    • Uses COALESCE to default weight to 100 if not specified
    • Calculates the weighted average, handling the case where sum of weights might be zero
    • Returns 0 if there are no subtasks with weights

Detailed Calculation for Time-based Progress Method

In Time-based Progress mode, the task's estimated time serves as its weight in progress calculations:

  1. Manual Progress Input:

    • As with weighted progress, leaf tasks require manual progress input
    • Progress is entered as a percentage (0-100%)
    • Completed tasks are automatically set to 100% progress
  2. Time Estimation:

    • Each task has an estimated time in hours and minutes
    • These values are stored in total_hours and total_minutes fields
    • Time estimates effectively function as weights in progress calculations
    • Tasks with longer estimated durations have more influence on parent task progress
    • Tasks with zero or no time estimate don't contribute to the parent's progress calculation
  3. Parent Task Calculation: The time-based progress formula is:

    ParentProgress = ∑(SubtaskProgress * SubtaskEstimatedMinutes) / ∑(SubtaskEstimatedMinutes)
    

    where SubtaskEstimatedMinutes = (SubtaskHours * 60) + SubtaskMinutes

    Example Calculation: Consider a parent task with three subtasks:

    • Subtask A: Progress 40%, Estimated Time 2h 30m (150 minutes)
    • Subtask B: Progress 80%, Estimated Time 1h (60 minutes)
    • Subtask C: Progress 10%, Estimated Time 4h (240 minutes)

    Calculation:

    ParentProgress = ((40 * 150) + (80 * 60) + (10 * 240)) / (150 + 60 + 240)
    ParentProgress = (6000 + 4800 + 2400) / 450
    ParentProgress = 13200 / 450
    ParentProgress = 29.33%
    

    Note how Subtask C, with its large time estimate, significantly pulls down the overall progress despite Subtask B being mostly complete.

  4. Zero Time Estimate Handling: Tasks with zero time estimate are excluded from the calculation:

    • Subtask A: Progress 40%, Estimated Time 3h (180 minutes)
    • Subtask B: Progress 80%, Estimated Time 0h (0 minutes)

    Calculation:

    ParentProgress = ((40 * 180) + (80 * 0)) / (180 + 0)
    ParentProgress = 7200 / 180
    ParentProgress = 40%
    

    In this case, only Subtask A influences the parent task progress because Subtask B has no time estimate.

  5. All Zero Time Estimates Edge Case: If all subtasks have zero time estimates, the progress is calculated as 0%:

    ParentProgress = SUM(progress_value * 0) / SUM(0) = 0 / 0 = undefined
    

    The SQL implementation handles this with NULLIF and COALESCE to return 0% in this case.

  6. Actual SQL Implementation: The SQL function for this calculation first converts hours to minutes for consistent measurement:

    WITH subtask_progress AS (
        SELECT 
            CASE 
                -- If subtask has manual progress, use that value
                WHEN manual_progress IS TRUE AND progress_value IS NOT NULL THEN 
                    progress_value
                -- Otherwise use completion status (0 or 100)
                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;
    

    This implementation:

    • Gets all non-archived subtasks of the parent task
    • Determines each subtask's progress value (manual or completion-based)
    • Calculates total minutes by converting hours to minutes and adding them together
    • Uses COALESCE to treat NULL time estimates as 0 minutes
    • Uses NULLIF to handle cases where all time estimates are zero
    • Returns 0% progress if there are no subtasks with time estimates

Common Implementation Considerations

For both weighted and time-based progress calculation:

  1. Null Handling:

    • Tasks with NULL progress values are treated as 0% progress (unless completed)
    • Tasks with NULL weights default to 100 in weighted mode
    • Tasks with NULL time estimates are treated as 0 minutes in time-based mode
  2. Progress Propagation:

    • When a leaf task's progress changes, all ancestor tasks are recalculated
    • Progress updates are propagated through socket events to all connected clients
    • The recalculation happens server-side to ensure consistency
  3. Edge Cases:

    • If all subtasks have zero weight/time, the system falls back to a simple average
    • If a parent task has no subtasks, its own manual progress value is used
    • If a task is archived, it's excluded from parent task calculations

Database Implementation

The manual progress value is stored in the tasks table with these relevant fields:

tasks (
  -- other fields
  progress_value FLOAT, -- The manually entered progress value (0-100)
  manual_progress BOOLEAN, -- Flag indicating if progress was manually set
  weight INTEGER DEFAULT 100, -- For weighted progress calculation
  total_hours INTEGER, -- For time-based progress calculation
  total_minutes INTEGER -- For time-based progress calculation
)

Integration with Parent Task Calculation

When a subtask's progress is updated manually, the parent task's progress is automatically recalculated based on the active progress method:

// Pseudocode for parent task recalculation
function recalculateParentTaskProgress(taskId, parentTaskId) {
  if (!parentTaskId) return;
  
  // Get project settings to determine active progress method
  const project = getProjectByTaskId(taskId);
  
  if (project.use_manual_progress) {
    // Calculate average of all subtask progress values
    updateParentProgress(parentTaskId, calculateAverageProgress(parentTaskId));
  } 
  else if (project.use_weighted_progress) {
    // Calculate weighted average using subtask weights
    updateParentProgress(parentTaskId, calculateWeightedProgress(parentTaskId));
  }
  else if (project.use_time_progress) {
    // Calculate weighted average using time estimates
    updateParentProgress(parentTaskId, calculateTimeBasedProgress(parentTaskId));
  }
  else {
    // Default: Calculate based on task completion
    updateParentProgress(parentTaskId, calculateCompletionBasedProgress(parentTaskId));
  }
  
  // If this parent has a parent, continue recalculation up the tree
  const grandparentId = getParentTaskId(parentTaskId);
  if (grandparentId) {
    recalculateParentTaskProgress(parentTaskId, grandparentId);
  }
}

This recursive approach ensures that changes to any task's progress are properly propagated up the task hierarchy.

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

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
  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.