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 (
+
+
+ );
+ }
+
+ // For subtasks with manual progress enabled, show the progress
+ return (
+
);
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;