Merge pull request #103 from chamikaJ/fix/custom-progress-methods
Fix/custom progress methods
This commit is contained in:
@@ -62,17 +62,17 @@ The parent task will show as 60% complete (average of 30%, 60%, and 90%).
|
||||
### Example
|
||||
|
||||
If you have a parent task with three subtasks:
|
||||
- Subtask A: 50% complete, Weight 200 (critical task)
|
||||
- Subtask B: 75% complete, Weight 100 (standard task)
|
||||
- Subtask C: 25% complete, Weight 300 (major task)
|
||||
- Subtask A: 50% complete, Weight 60% (important task)
|
||||
- Subtask B: 75% complete, Weight 20% (less important task)
|
||||
- Subtask C: 25% complete, Weight 100% (critical task)
|
||||
|
||||
The parent task will be approximately 42% complete, with Subtask C having the greatest impact due to its higher weight.
|
||||
The parent task will be approximately 39% complete, with Subtask C having the greatest impact due to its higher weight.
|
||||
|
||||
### Important Notes About Weights
|
||||
|
||||
- Default weight is 100 if not specified
|
||||
- You can set weights from 0 to any reasonable number (typically 1-1000)
|
||||
- Setting a weight to 0 removes that task from progress calculations
|
||||
- Default weight is 100% if not specified
|
||||
- Weights range from 0% to 100%
|
||||
- Setting a weight to 0% removes that task from progress calculations
|
||||
- Only explicitly set weights for tasks that should have different importance
|
||||
- Weights are only relevant for subtasks, not for independent tasks
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ This mode allows assigning different weights to subtasks to reflect their relati
|
||||
- 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
|
||||
@@ -224,10 +225,9 @@ In Weighted Progress mode, both the manual progress input and weight assignment
|
||||
- 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 (default 100 if not specified)
|
||||
- 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
|
||||
- Weight values are typically whole numbers between 0 and 1000
|
||||
- A weight of 0 means the task doesn't contribute to the parent's progress calculation
|
||||
- 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:
|
||||
@@ -237,24 +237,24 @@ In Weighted Progress mode, both the manual progress input and weight assignment
|
||||
|
||||
**Example Calculation**:
|
||||
Consider a parent task with three subtasks:
|
||||
- Subtask A: Progress 50%, Weight 200
|
||||
- Subtask B: Progress 75%, Weight 100
|
||||
- Subtask C: Progress 25%, Weight 300
|
||||
- Subtask A: Progress 50%, Weight 60%
|
||||
- Subtask B: Progress 75%, Weight 20%
|
||||
- Subtask C: Progress 25%, Weight 100%
|
||||
|
||||
Calculation:
|
||||
```
|
||||
ParentProgress = ((50 * 200) + (75 * 100) + (25 * 300)) / (200 + 100 + 300)
|
||||
ParentProgress = (10000 + 7500 + 7500) / 600
|
||||
ParentProgress = 25000 / 600
|
||||
ParentProgress = 41.67%
|
||||
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
|
||||
- Subtask A: Progress 40%, Weight 50%
|
||||
- Subtask B: Progress 80%, Weight 0%
|
||||
|
||||
Calculation:
|
||||
```
|
||||
@@ -263,13 +263,13 @@ In Weighted Progress mode, both the manual progress input and weight assignment
|
||||
ParentProgress = 40%
|
||||
```
|
||||
|
||||
In this case, only Subtask A influences the parent task progress because Subtask B has a weight of 0.
|
||||
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)
|
||||
- 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:
|
||||
```
|
||||
|
||||
244
task-progress-methods.md
Normal file
244
task-progress-methods.md
Normal file
@@ -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 : (
|
||||
<Tooltip title={`${task.completed_count || 0} / ${task.total_tasks_count || 0}`}>
|
||||
<Progress
|
||||
percent={task.complete_ratio || 0}
|
||||
type="circle"
|
||||
size={24}
|
||||
style={{ cursor: 'default' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**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.
|
||||
@@ -39,8 +39,14 @@ BEGIN
|
||||
INTO _use_manual_progress, _use_weighted_progress, _use_time_progress;
|
||||
END IF;
|
||||
|
||||
-- If manual progress is enabled and has a value, use it directly
|
||||
IF _is_manual IS TRUE AND _manual_value IS NOT NULL THEN
|
||||
-- Get all subtasks
|
||||
SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE parent_task_id = _task_id AND archived IS FALSE
|
||||
INTO _sub_tasks_count;
|
||||
|
||||
-- If manual progress is enabled and has a value AND there are no subtasks, use it directly
|
||||
IF _is_manual IS TRUE AND _manual_value IS NOT NULL AND _sub_tasks_count = 0 THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _manual_value,
|
||||
'total_completed', 0,
|
||||
@@ -49,12 +55,6 @@ BEGIN
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Get all subtasks
|
||||
SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE parent_task_id = _task_id AND archived IS FALSE
|
||||
INTO _sub_tasks_count;
|
||||
|
||||
-- If there are no subtasks, just use the parent task's status
|
||||
IF _sub_tasks_count = 0 THEN
|
||||
SELECT (CASE
|
||||
@@ -145,7 +145,7 @@ BEGIN
|
||||
ELSE 0
|
||||
END
|
||||
END AS progress_value,
|
||||
COALESCE(total_hours * 60 + total_minutes, 0) AS estimated_minutes
|
||||
COALESCE(total_minutes, 0) AS estimated_minutes
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
@@ -350,8 +350,8 @@ BEGIN
|
||||
RETURNING id INTO _project_id;
|
||||
|
||||
-- register the project log
|
||||
INSERT INTO project_logs (project_id, team_id, team_member_id, description)
|
||||
VALUES (_project_id, _team_id, _team_member_id, _project_created_log)
|
||||
INSERT INTO project_logs (project_id, team_id, description)
|
||||
VALUES (_project_id, _team_id, _project_created_log)
|
||||
RETURNING id INTO _project_created_log_id;
|
||||
|
||||
-- add the team member in the project as a user
|
||||
@@ -360,8 +360,8 @@ BEGIN
|
||||
(SELECT id FROM project_access_levels WHERE key = 'MEMBER'));
|
||||
|
||||
-- register the project log
|
||||
INSERT INTO project_logs (project_id, team_id, team_member_id, description)
|
||||
VALUES (_project_id, _team_id, _team_member_id, _project_member_added_log);
|
||||
INSERT INTO project_logs (project_id, team_id, description)
|
||||
VALUES (_project_id, _team_id, _project_member_added_log);
|
||||
|
||||
-- insert default project columns
|
||||
PERFORM insert_task_list_columns(_project_id);
|
||||
@@ -657,4 +657,26 @@ ADD COLUMN IF NOT EXISTS use_manual_progress BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS use_weighted_progress BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS use_time_progress BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- Add a trigger to reset manual progress when a task gets a new subtask
|
||||
CREATE OR REPLACE FUNCTION reset_parent_task_manual_progress() RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
-- When a task gets a new subtask (parent_task_id is set), reset the parent's manual_progress flag
|
||||
IF NEW.parent_task_id IS NOT NULL THEN
|
||||
UPDATE tasks
|
||||
SET manual_progress = false
|
||||
WHERE id = NEW.parent_task_id
|
||||
AND manual_progress = true;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create the trigger on the tasks table
|
||||
DROP TRIGGER IF EXISTS reset_parent_manual_progress_trigger ON tasks;
|
||||
CREATE TRIGGER reset_parent_manual_progress_trigger
|
||||
AFTER INSERT OR UPDATE OF parent_task_id ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reset_parent_task_manual_progress();
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,221 @@
|
||||
-- Migration: Update time-based progress mode to work for all tasks
|
||||
-- Date: 2025-04-25
|
||||
-- Version: 1.0.0
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Update function to use time-based progress for all tasks
|
||||
CREATE OR REPLACE FUNCTION get_task_complete_ratio(_task_id uuid) RETURNS json
|
||||
LANGUAGE plpgsql
|
||||
AS
|
||||
$$
|
||||
DECLARE
|
||||
_parent_task_done FLOAT = 0;
|
||||
_sub_tasks_done FLOAT = 0;
|
||||
_sub_tasks_count FLOAT = 0;
|
||||
_total_completed FLOAT = 0;
|
||||
_total_tasks FLOAT = 0;
|
||||
_ratio FLOAT = 0;
|
||||
_is_manual BOOLEAN = FALSE;
|
||||
_manual_value INTEGER = NULL;
|
||||
_project_id UUID;
|
||||
_use_manual_progress BOOLEAN = FALSE;
|
||||
_use_weighted_progress BOOLEAN = FALSE;
|
||||
_use_time_progress BOOLEAN = FALSE;
|
||||
BEGIN
|
||||
-- Check if manual progress is set for this task
|
||||
SELECT manual_progress, progress_value, project_id
|
||||
FROM tasks
|
||||
WHERE id = _task_id
|
||||
INTO _is_manual, _manual_value, _project_id;
|
||||
|
||||
-- Check if the project uses manual progress
|
||||
IF _project_id IS NOT NULL THEN
|
||||
SELECT COALESCE(use_manual_progress, FALSE),
|
||||
COALESCE(use_weighted_progress, FALSE),
|
||||
COALESCE(use_time_progress, FALSE)
|
||||
FROM projects
|
||||
WHERE id = _project_id
|
||||
INTO _use_manual_progress, _use_weighted_progress, _use_time_progress;
|
||||
END IF;
|
||||
|
||||
-- Get all subtasks
|
||||
SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE parent_task_id = _task_id AND archived IS FALSE
|
||||
INTO _sub_tasks_count;
|
||||
|
||||
-- Always respect manual progress value if set
|
||||
IF _is_manual IS TRUE AND _manual_value IS NOT NULL THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _manual_value,
|
||||
'total_completed', 0,
|
||||
'total_tasks', 0,
|
||||
'is_manual', TRUE
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- If there are no subtasks, just use the parent task's status (unless in time-based mode)
|
||||
IF _sub_tasks_count = 0 THEN
|
||||
-- Use time-based estimation for tasks without subtasks if enabled
|
||||
IF _use_time_progress IS TRUE THEN
|
||||
-- For time-based tasks without subtasks, we still need some progress calculation
|
||||
-- If the task is completed, return 100%
|
||||
-- Otherwise, use the progress value if set manually, or 0
|
||||
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 100
|
||||
ELSE COALESCE(_manual_value, 0)
|
||||
END
|
||||
INTO _ratio;
|
||||
ELSE
|
||||
-- Traditional calculation for non-time-based tasks
|
||||
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;
|
||||
|
||||
_ratio = _parent_task_done * 100;
|
||||
END IF;
|
||||
ELSE
|
||||
-- If project uses manual progress, calculate based on subtask manual progress values
|
||||
IF _use_manual_progress IS TRUE THEN
|
||||
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
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
)
|
||||
SELECT COALESCE(AVG(progress_value), 0)
|
||||
FROM subtask_progress
|
||||
INTO _ratio;
|
||||
-- If project uses weighted progress, calculate based on subtask weights
|
||||
ELSIF _use_weighted_progress IS TRUE THEN
|
||||
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;
|
||||
-- If project uses time-based progress, calculate based on estimated time
|
||||
ELSIF _use_time_progress IS TRUE THEN
|
||||
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_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;
|
||||
ELSE
|
||||
-- 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;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Ensure ratio is between 0 and 100
|
||||
IF _ratio < 0 THEN
|
||||
_ratio = 0;
|
||||
ELSIF _ratio > 100 THEN
|
||||
_ratio = 100;
|
||||
END IF;
|
||||
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _ratio,
|
||||
'total_completed', _total_completed,
|
||||
'total_tasks', _total_tasks,
|
||||
'is_manual', _is_manual
|
||||
);
|
||||
END
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,272 @@
|
||||
-- Migration: Improve parent task progress calculation using weights and time estimation
|
||||
-- Date: 2025-04-26
|
||||
-- Version: 1.0.0
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Update function to better calculate parent task progress based on subtask weights or time estimations
|
||||
CREATE OR REPLACE FUNCTION get_task_complete_ratio(_task_id uuid) RETURNS json
|
||||
LANGUAGE plpgsql
|
||||
AS
|
||||
$$
|
||||
DECLARE
|
||||
_parent_task_done FLOAT = 0;
|
||||
_sub_tasks_done FLOAT = 0;
|
||||
_sub_tasks_count FLOAT = 0;
|
||||
_total_completed FLOAT = 0;
|
||||
_total_tasks FLOAT = 0;
|
||||
_ratio FLOAT = 0;
|
||||
_is_manual BOOLEAN = FALSE;
|
||||
_manual_value INTEGER = NULL;
|
||||
_project_id UUID;
|
||||
_use_manual_progress BOOLEAN = FALSE;
|
||||
_use_weighted_progress BOOLEAN = FALSE;
|
||||
_use_time_progress BOOLEAN = FALSE;
|
||||
BEGIN
|
||||
-- Check if manual progress is set for this task
|
||||
SELECT manual_progress, progress_value, project_id
|
||||
FROM tasks
|
||||
WHERE id = _task_id
|
||||
INTO _is_manual, _manual_value, _project_id;
|
||||
|
||||
-- Check if the project uses manual progress
|
||||
IF _project_id IS NOT NULL THEN
|
||||
SELECT COALESCE(use_manual_progress, FALSE),
|
||||
COALESCE(use_weighted_progress, FALSE),
|
||||
COALESCE(use_time_progress, FALSE)
|
||||
FROM projects
|
||||
WHERE id = _project_id
|
||||
INTO _use_manual_progress, _use_weighted_progress, _use_time_progress;
|
||||
END IF;
|
||||
|
||||
-- Get all subtasks
|
||||
SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE parent_task_id = _task_id AND archived IS FALSE
|
||||
INTO _sub_tasks_count;
|
||||
|
||||
-- Only respect manual progress for tasks without subtasks
|
||||
IF _is_manual IS TRUE AND _manual_value IS NOT NULL AND _sub_tasks_count = 0 THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _manual_value,
|
||||
'total_completed', 0,
|
||||
'total_tasks', 0,
|
||||
'is_manual', TRUE
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- If there are no subtasks, just use the parent task's status
|
||||
IF _sub_tasks_count = 0 THEN
|
||||
-- For tasks without subtasks in time-based mode
|
||||
IF _use_time_progress IS TRUE THEN
|
||||
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 100
|
||||
ELSE COALESCE(_manual_value, 0)
|
||||
END
|
||||
INTO _ratio;
|
||||
ELSE
|
||||
-- Traditional calculation for non-time-based tasks
|
||||
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;
|
||||
|
||||
_ratio = _parent_task_done * 100;
|
||||
END IF;
|
||||
ELSE
|
||||
-- For parent tasks with subtasks, always use the appropriate calculation based on project mode
|
||||
-- If project uses manual progress, calculate based on subtask manual progress values
|
||||
IF _use_manual_progress IS TRUE THEN
|
||||
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
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
)
|
||||
SELECT COALESCE(AVG(progress_value), 0)
|
||||
FROM subtask_progress
|
||||
INTO _ratio;
|
||||
-- If project uses weighted progress, calculate based on subtask weights
|
||||
ELSIF _use_weighted_progress IS TRUE THEN
|
||||
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 -- Default weight is 100 if not specified
|
||||
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;
|
||||
-- If project uses time-based progress, calculate based on estimated time (total_minutes)
|
||||
ELSIF _use_time_progress IS TRUE THEN
|
||||
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_minutes, 0) AS estimated_minutes -- Use time estimation for weighting
|
||||
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;
|
||||
ELSE
|
||||
-- Traditional calculation based on completion status when no special mode is enabled
|
||||
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;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Ensure ratio is between 0 and 100
|
||||
IF _ratio < 0 THEN
|
||||
_ratio = 0;
|
||||
ELSIF _ratio > 100 THEN
|
||||
_ratio = 100;
|
||||
END IF;
|
||||
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _ratio,
|
||||
'total_completed', _total_completed,
|
||||
'total_tasks', _total_tasks,
|
||||
'is_manual', _is_manual
|
||||
);
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Make sure we recalculate parent task progress when subtask progress changes
|
||||
CREATE OR REPLACE FUNCTION update_parent_task_progress() RETURNS TRIGGER AS
|
||||
$$
|
||||
DECLARE
|
||||
_parent_task_id UUID;
|
||||
BEGIN
|
||||
-- Check if this is a subtask
|
||||
IF NEW.parent_task_id IS NOT NULL THEN
|
||||
_parent_task_id := NEW.parent_task_id;
|
||||
|
||||
-- Force any parent task with subtasks to NOT use manual progress
|
||||
UPDATE tasks
|
||||
SET manual_progress = FALSE
|
||||
WHERE id = _parent_task_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger for updates to task progress
|
||||
DROP TRIGGER IF EXISTS update_parent_task_progress_trigger ON tasks;
|
||||
CREATE TRIGGER update_parent_task_progress_trigger
|
||||
AFTER UPDATE OF progress_value, weight, total_minutes ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_parent_task_progress();
|
||||
|
||||
-- Create a function to ensure parent tasks never have manual progress when they have subtasks
|
||||
CREATE OR REPLACE FUNCTION ensure_parent_task_without_manual_progress() RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
-- If this is a new subtask being created or a task is being converted to a subtask
|
||||
IF NEW.parent_task_id IS NOT NULL THEN
|
||||
-- Force the parent task to NOT use manual progress
|
||||
UPDATE tasks
|
||||
SET manual_progress = FALSE
|
||||
WHERE id = NEW.parent_task_id;
|
||||
|
||||
-- Log that we've reset manual progress for a parent task
|
||||
RAISE NOTICE 'Reset manual progress for parent task % because it has subtasks', NEW.parent_task_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger for when tasks are created or updated with a parent_task_id
|
||||
DROP TRIGGER IF EXISTS ensure_parent_task_without_manual_progress_trigger ON tasks;
|
||||
CREATE TRIGGER ensure_parent_task_without_manual_progress_trigger
|
||||
AFTER INSERT OR UPDATE OF parent_task_id ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION ensure_parent_task_without_manual_progress();
|
||||
|
||||
COMMIT;
|
||||
@@ -97,8 +97,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
try {
|
||||
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
|
||||
const [data] = result.rows;
|
||||
data.info.ratio = +data.info.ratio.toFixed();
|
||||
if (data && data.info && data.info.ratio !== undefined) {
|
||||
data.info.ratio = +((data.info.ratio || 0).toFixed());
|
||||
return data.info;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -198,6 +201,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
|
||||
@@ -371,7 +375,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]);
|
||||
const [data] = result.rows;
|
||||
if (data && data.info) {
|
||||
task.complete_ratio = +data.info.ratio.toFixed();
|
||||
task.complete_ratio = +(data.info.ratio || 0).toFixed();
|
||||
task.completed_count = data.info.total_completed;
|
||||
task.total_tasks_count = data.info.total_tasks;
|
||||
}
|
||||
@@ -440,7 +444,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]);
|
||||
const [ratioData] = result.rows;
|
||||
if (ratioData && ratioData.info) {
|
||||
task.complete_ratio = +ratioData.info.ratio.toFixed();
|
||||
task.complete_ratio = +(ratioData.info.ratio || 0).toFixed();
|
||||
task.completed_count = ratioData.info.total_completed;
|
||||
task.total_tasks_count = ratioData.info.total_tasks;
|
||||
console.log(`Updated task ${task.id} (${task.name}) from DB: complete_ratio=${task.complete_ratio}`);
|
||||
@@ -482,6 +486,53 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
return res.status(200).send(new ServerResponse(true, task));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async resetParentTaskManualProgress(parentTaskId: string): Promise<void> {
|
||||
try {
|
||||
// Check if this task has subtasks
|
||||
const subTasksResult = await db.query(
|
||||
"SELECT COUNT(*) as subtask_count FROM tasks WHERE parent_task_id = $1 AND archived IS FALSE",
|
||||
[parentTaskId]
|
||||
);
|
||||
|
||||
const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || "0");
|
||||
|
||||
// If it has subtasks, reset the manual_progress flag to false
|
||||
if (subtaskCount > 0) {
|
||||
await db.query(
|
||||
"UPDATE tasks SET manual_progress = false WHERE id = $1",
|
||||
[parentTaskId]
|
||||
);
|
||||
console.log(`Reset manual progress for parent task ${parentTaskId} with ${subtaskCount} subtasks`);
|
||||
|
||||
// Get the project settings to determine which calculation method to use
|
||||
const projectResult = await db.query(
|
||||
"SELECT project_id FROM tasks WHERE id = $1",
|
||||
[parentTaskId]
|
||||
);
|
||||
|
||||
const projectId = projectResult.rows[0]?.project_id;
|
||||
|
||||
if (projectId) {
|
||||
// Recalculate the parent task's progress based on its subtasks
|
||||
const progressResult = await db.query(
|
||||
"SELECT get_task_complete_ratio($1) AS ratio",
|
||||
[parentTaskId]
|
||||
);
|
||||
|
||||
const progressRatio = progressResult.rows[0]?.ratio?.ratio || 0;
|
||||
|
||||
// Emit the updated progress value to all clients
|
||||
// Note: We don't have socket context here, so we can't directly emit
|
||||
// This will be picked up on the next client refresh
|
||||
console.log(`Recalculated progress for parent task ${parentTaskId}: ${progressRatio}%`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(`Error resetting parent task manual progress: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
@@ -522,6 +573,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
: [req.body.id, req.body.project_id, req.body.parent_task_id, req.body.to_group_id];
|
||||
await db.query(q, params);
|
||||
|
||||
// Reset the parent task's manual progress when converting a task to a subtask
|
||||
if (req.body.parent_task_id) {
|
||||
await this.resetParentTaskManualProgress(req.body.parent_task_id);
|
||||
}
|
||||
|
||||
const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]);
|
||||
const [data] = result.rows;
|
||||
const model = TasksControllerV2.updateTaskViewModel(data.task);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Socket } from "socket.io";
|
||||
import db from "../../config/db";
|
||||
import { SocketEvents } from "../events";
|
||||
import { log_error } from "../util";
|
||||
|
||||
/**
|
||||
* Socket handler to retrieve the number of subtasks for a given task
|
||||
* Used to validate on the client side whether a task should show progress inputs
|
||||
*/
|
||||
export async function on_get_task_subtasks_count(io: any, socket: Socket, taskId: string) {
|
||||
try {
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the count of subtasks for this task
|
||||
const result = await db.query(
|
||||
"SELECT COUNT(*) as subtask_count FROM tasks WHERE parent_task_id = $1 AND archived IS FALSE",
|
||||
[taskId]
|
||||
);
|
||||
|
||||
const subtaskCount = parseInt(result.rows[0]?.subtask_count || "0");
|
||||
|
||||
// Emit the subtask count back to the client
|
||||
socket.emit(
|
||||
"TASK_SUBTASKS_COUNT",
|
||||
{
|
||||
task_id: taskId,
|
||||
subtask_count: subtaskCount,
|
||||
has_subtasks: subtaskCount > 0
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Emitted subtask count for task ${taskId}: ${subtaskCount}`);
|
||||
|
||||
} catch (error) {
|
||||
log_error(`Error getting subtask count for task ${taskId}: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,56 @@ import { SocketEvents } from "../events";
|
||||
import { log_error, notifyProjectUpdates } from "../util";
|
||||
import { getTaskDetails, logTotalMinutes } from "../../services/activity-logs/activity-logs.service";
|
||||
|
||||
export async function on_time_estimation_change(_io: Server, socket: Socket, data?: string) {
|
||||
/**
|
||||
* Recursively updates all ancestor tasks' progress when a subtask changes
|
||||
* @param io Socket.io instance
|
||||
* @param socket Socket instance for emitting events
|
||||
* @param projectId Project ID for room broadcasting
|
||||
* @param taskId The task ID to update (starts with the parent task)
|
||||
*/
|
||||
async function updateTaskAncestors(io: any, socket: Socket, 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?.ratio || 0;
|
||||
console.log(`Updated task ${taskId} progress after time estimation change: ${ratio}`);
|
||||
|
||||
// Emit the updated progress
|
||||
socket.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, socket, projectId, parentTaskId);
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(`Error updating ancestor task ${taskId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function on_time_estimation_change(io: Server, socket: Socket, data?: string) {
|
||||
try {
|
||||
// (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
|
||||
const q = `UPDATE tasks SET total_minutes = $2 WHERE id = $1 RETURNING total_minutes;`;
|
||||
const q = `UPDATE tasks SET total_minutes = $2 WHERE id = $1 RETURNING total_minutes, project_id, parent_task_id;`;
|
||||
const body = JSON.parse(data as string);
|
||||
|
||||
const hours = body.total_hours || 0;
|
||||
@@ -19,7 +65,10 @@ export async function on_time_estimation_change(_io: Server, socket: Socket, dat
|
||||
const task_data = await getTaskDetails(body.task_id, "total_minutes");
|
||||
|
||||
const result0 = await db.query(q, [body.task_id, totalMinutes]);
|
||||
const [data0] = result0.rows;
|
||||
const [taskData] = result0.rows;
|
||||
|
||||
const projectId = taskData.project_id;
|
||||
const parentTaskId = taskData.parent_task_id;
|
||||
|
||||
const result = await db.query("SELECT SUM(time_spent) AS total_minutes_spent FROM task_work_log WHERE task_id = $1;", [body.task_id]);
|
||||
const [dd] = result.rows;
|
||||
@@ -31,6 +80,22 @@ export async function on_time_estimation_change(_io: Server, socket: Socket, dat
|
||||
total_minutes_spent: dd.total_minutes_spent || 0
|
||||
};
|
||||
socket.emit(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), TasksController.updateTaskViewModel(d));
|
||||
|
||||
// If this is a subtask in time-based mode, update parent task progress
|
||||
if (parentTaskId) {
|
||||
const projectSettingsResult = await db.query(
|
||||
"SELECT use_time_progress FROM projects WHERE id = $1",
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const useTimeProgress = projectSettingsResult.rows[0]?.use_time_progress;
|
||||
|
||||
if (useTimeProgress) {
|
||||
// Recalculate parent task progress when subtask time estimation changes
|
||||
await updateTaskAncestors(io, socket, projectId, parentTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
notifyProjectUpdates(socket, d.id);
|
||||
|
||||
logTotalMinutes({
|
||||
|
||||
@@ -10,6 +10,52 @@ interface UpdateTaskProgressData {
|
||||
parent_task_id: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively updates all ancestor tasks' progress when a subtask changes
|
||||
* @param io Socket.io instance
|
||||
* @param socket Socket instance for emitting events
|
||||
* @param projectId Project ID for room broadcasting
|
||||
* @param taskId The task ID to update (starts with the parent task)
|
||||
*/
|
||||
async function updateTaskAncestors(io: any, socket: Socket, 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?.ratio || 0;
|
||||
console.log(`Updated task ${taskId} progress: ${ratio}`);
|
||||
|
||||
// Emit the updated progress
|
||||
socket.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, socket, projectId, parentTaskId);
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(`Error updating ancestor task ${taskId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function on_update_task_progress(io: any, socket: Socket, data: string) {
|
||||
try {
|
||||
const parsedData = JSON.parse(data) as UpdateTaskProgressData;
|
||||
@@ -25,7 +71,7 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
|
||||
[task_id]
|
||||
);
|
||||
|
||||
const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || '0');
|
||||
const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || "0");
|
||||
|
||||
// If this is a parent task, we shouldn't set manual progress
|
||||
if (subtaskCount > 0) {
|
||||
@@ -35,7 +81,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]
|
||||
);
|
||||
|
||||
@@ -53,14 +99,13 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
|
||||
// Log the progress change using the activity logs service
|
||||
await logProgressChange({
|
||||
task_id,
|
||||
old_value: currentProgress !== null ? currentProgress.toString() : '0',
|
||||
old_value: currentProgress !== null ? currentProgress.toString() : "0",
|
||||
new_value: progress_value.toString(),
|
||||
socket
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
// Emit the update to all clients in the project room
|
||||
io.to(projectId).emit(
|
||||
socket.emit(
|
||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||
{
|
||||
task_id,
|
||||
@@ -68,26 +113,10 @@ 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}`);
|
||||
log(`Emitted progress update for task ${task_id} to project room ${projectId}`, null);
|
||||
|
||||
// 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, socket, projectId, parent_task_id);
|
||||
|
||||
// Notify that project updates are available
|
||||
notifyProjectUpdates(socket, task_id);
|
||||
|
||||
@@ -40,14 +40,14 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin
|
||||
// Log the weight change using the activity logs service
|
||||
await logWeightChange({
|
||||
task_id,
|
||||
old_value: currentWeight !== null ? currentWeight.toString() : '100',
|
||||
old_value: currentWeight !== null ? currentWeight.toString() : "100",
|
||||
new_value: weight.toString(),
|
||||
socket
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
// Emit the update to all clients in the project room
|
||||
io.to(projectId).emit(
|
||||
socket.emit(
|
||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||
{
|
||||
task_id,
|
||||
@@ -63,11 +63,11 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin
|
||||
);
|
||||
|
||||
// Emit the parent task's updated progress
|
||||
io.to(projectId).emit(
|
||||
socket.emit(
|
||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||
{
|
||||
task_id: parent_task_id,
|
||||
progress_value: progressRatio?.rows[0]?.ratio
|
||||
progress_value: progressRatio?.rows[0]?.ratio?.ratio || 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,4 +63,8 @@ export enum SocketEvents {
|
||||
UPDATE_TASK_PROGRESS,
|
||||
UPDATE_TASK_WEIGHT,
|
||||
TASK_PROGRESS_UPDATED,
|
||||
|
||||
// Task subtasks count events
|
||||
GET_TASK_SUBTASKS_COUNT,
|
||||
TASK_SUBTASKS_COUNT,
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import { on_task_custom_column_update } from "./commands/on_custom_column_update
|
||||
import { on_custom_column_pinned_change } from "./commands/on_custom_column_pinned_change";
|
||||
import { on_update_task_progress } from "./commands/on-update-task-progress";
|
||||
import { on_update_task_weight } from "./commands/on-update-task-weight";
|
||||
import { on_get_task_subtasks_count } from "./commands/on-get-task-subtasks-count";
|
||||
|
||||
export function register(io: any, socket: Socket) {
|
||||
log(socket.id, "client registered");
|
||||
@@ -110,6 +111,7 @@ export function register(io: any, socket: Socket) {
|
||||
socket.on(SocketEvents.CUSTOM_COLUMN_PINNED_CHANGE.toString(), data => on_custom_column_pinned_change(io, socket, data));
|
||||
socket.on(SocketEvents.UPDATE_TASK_PROGRESS.toString(), data => on_update_task_progress(io, socket, data));
|
||||
socket.on(SocketEvents.UPDATE_TASK_WEIGHT.toString(), data => on_update_task_weight(io, socket, data));
|
||||
socket.on(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), (taskId) => on_get_task_subtasks_count(io, socket, taskId));
|
||||
|
||||
// socket.io built-in event
|
||||
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import Flex from 'antd/lib/flex';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
|
||||
interface TaskDrawerProgressProps {
|
||||
@@ -17,16 +17,48 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const { socket, connected } = useSocket();
|
||||
const [confirmedHasSubtasks, setConfirmedHasSubtasks] = useState<boolean | null>(null);
|
||||
|
||||
const isSubTask = !!task?.parent_task_id;
|
||||
const hasSubTasks = task?.sub_tasks_count > 0;
|
||||
const hasSubTasks = task?.sub_tasks_count > 0 || confirmedHasSubtasks === true;
|
||||
|
||||
// Show manual progress input only for tasks without subtasks (not parent tasks)
|
||||
// Parent tasks get their progress calculated from subtasks
|
||||
// Additional debug logging
|
||||
console.log(`TaskDrawerProgress for task ${task.id} (${task.name}): hasSubTasks=${hasSubTasks}, count=${task.sub_tasks_count}, confirmedHasSubtasks=${confirmedHasSubtasks}`);
|
||||
|
||||
// HIGHEST PRIORITY CHECK: Never show progress inputs for parent tasks with subtasks
|
||||
// This check happens before any other logic to ensure consistency
|
||||
if (hasSubTasks) {
|
||||
console.error(`REJECTED: Progress input for parent task ${task.id} with ${task.sub_tasks_count} subtasks. confirmedHasSubtasks=${confirmedHasSubtasks}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Double-check by directly querying for subtasks from the server
|
||||
useEffect(() => {
|
||||
if (connected && task.id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), task.id);
|
||||
}
|
||||
|
||||
// Listen for the subtask count response
|
||||
const handleSubtasksCount = (data: any) => {
|
||||
if (data.task_id === task.id) {
|
||||
console.log(`Received subtask count for task ${task.id}: ${data.subtask_count}, has_subtasks=${data.has_subtasks}`);
|
||||
setConfirmedHasSubtasks(data.has_subtasks);
|
||||
}
|
||||
};
|
||||
|
||||
socket?.on(SocketEvents.TASK_SUBTASKS_COUNT.toString(), handleSubtasksCount);
|
||||
|
||||
return () => {
|
||||
socket?.off(SocketEvents.TASK_SUBTASKS_COUNT.toString(), handleSubtasksCount);
|
||||
};
|
||||
}, [socket, connected, task.id]);
|
||||
|
||||
// Never show manual progress input for parent tasks (tasks with subtasks)
|
||||
// Only show progress input for tasks without subtasks
|
||||
const showManualProgressInput = !hasSubTasks;
|
||||
|
||||
// Only show weight input for subtasks in weighted progress mode
|
||||
const showTaskWeightInput = project?.use_weighted_progress && isSubTask;
|
||||
const showTaskWeightInput = project?.use_weighted_progress && isSubTask && !hasSubTasks;
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for progress updates from the server
|
||||
@@ -53,8 +85,13 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
};
|
||||
}, [socket, connected, task.id, form]);
|
||||
|
||||
// One last check before rendering
|
||||
if (hasSubTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleProgressChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
if (connected && task.id && value !== null && !hasSubTasks) {
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
@@ -67,13 +104,6 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
})
|
||||
);
|
||||
|
||||
// If this task has subtasks, request recalculation of its progress
|
||||
if (hasSubTasks) {
|
||||
setTimeout(() => {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// If this is a subtask, request the parent's progress to be updated in UI
|
||||
if (parent_task_id) {
|
||||
setTimeout(() => {
|
||||
@@ -84,7 +114,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
};
|
||||
|
||||
const handleWeightChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
if (connected && task.id && value !== null && !hasSubTasks) {
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
@@ -116,6 +146,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
return null; // Don't show any progress inputs if not applicable
|
||||
}
|
||||
|
||||
// Final safety check
|
||||
if (hasSubTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTaskWeightInput && (
|
||||
|
||||
@@ -33,6 +33,42 @@ interface TaskDetailsFormProps {
|
||||
taskFormViewModel?: ITaskFormViewModel | null;
|
||||
}
|
||||
|
||||
// Custom wrapper that enforces stricter rules for displaying progress input
|
||||
interface ConditionalProgressInputProps {
|
||||
task: ITaskViewModel;
|
||||
form: any; // Using any for the form as the exact type may be complex
|
||||
}
|
||||
|
||||
const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps) => {
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const hasSubTasks = task?.sub_tasks_count > 0;
|
||||
const isSubTask = !!task?.parent_task_id;
|
||||
|
||||
// Add more aggressive logging and checks
|
||||
console.log(`Task ${task.id} status: hasSubTasks=${hasSubTasks}, isSubTask=${isSubTask}, modes: time=${project?.use_time_progress}, manual=${project?.use_manual_progress}, weighted=${project?.use_weighted_progress}`);
|
||||
|
||||
// STRICT RULE: Never show progress input for parent tasks with subtasks
|
||||
// This is the most important check and must be done first
|
||||
if (hasSubTasks) {
|
||||
console.log(`Task ${task.id} has ${task.sub_tasks_count} subtasks. Hiding progress input.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only for tasks without subtasks, determine which input to show based on project mode
|
||||
if (project?.use_time_progress) {
|
||||
// In time-based mode, show progress input ONLY for tasks without subtasks
|
||||
return <TaskDrawerProgress task={{...task, sub_tasks_count: hasSubTasks ? 1 : 0}} form={form} />;
|
||||
} else if (project?.use_manual_progress) {
|
||||
// In manual mode, show progress input ONLY for tasks without subtasks
|
||||
return <TaskDrawerProgress task={{...task, sub_tasks_count: hasSubTasks ? 1 : 0}} form={form} />;
|
||||
} else if (project?.use_weighted_progress && isSubTask) {
|
||||
// In weighted mode, show weight input for subtasks
|
||||
return <TaskDrawerProgress task={{...task, sub_tasks_count: hasSubTasks ? 1 : 0}} form={form} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const [form] = Form.useForm();
|
||||
@@ -121,8 +157,11 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
|
||||
<TaskDrawerEstimation t={t} task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
|
||||
{(project?.use_manual_progress || project?.use_weighted_progress) && (taskFormViewModel?.task) && (
|
||||
<TaskDrawerProgress task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
{taskFormViewModel?.task && (
|
||||
<ConditionalProgressInput
|
||||
task={taskFormViewModel?.task as ITaskViewModel}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item name="priority" label={t('taskInfoTab.details.priority')}>
|
||||
|
||||
@@ -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) {
|
||||
// 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;
|
||||
break;
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const ProjectViewTaskList = () => {
|
||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||
<TaskListFilters position="list" />
|
||||
|
||||
{(taskGroups.length === 0 && !loadingGroups) ? (
|
||||
{(taskGroups && taskGroups.length === 0 && !loadingGroups) ? (
|
||||
<Empty description="No tasks group found" />
|
||||
) : (
|
||||
<Skeleton active loading={loadingGroups} className='mt-4 p-4'>
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
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 (
|
||||
<Tooltip title={`${task.completed_count || 0} / ${task.total_tasks_count || 0}`}>
|
||||
<Progress
|
||||
percent={task.complete_ratio || 0}
|
||||
@@ -18,6 +36,22 @@ const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// For subtasks with manual progress enabled, show the progress
|
||||
return (
|
||||
<Tooltip
|
||||
title={hasManualProgress ? `Manual: ${task.progress_value || 0}%` : `${task.progress || 0}%`}
|
||||
>
|
||||
<Progress
|
||||
percent={hasManualProgress ? (task.progress_value || 0) : (task.progress || 0)}
|
||||
type="circle"
|
||||
size={22} // Slightly smaller for subtasks
|
||||
style={{ cursor: 'default' }}
|
||||
strokeWidth={(task.progress || 0) >= 100 ? 9 : 7}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListProgressCell;
|
||||
|
||||
@@ -63,4 +63,8 @@ export enum SocketEvents {
|
||||
UPDATE_TASK_PROGRESS,
|
||||
UPDATE_TASK_WEIGHT,
|
||||
TASK_PROGRESS_UPDATED,
|
||||
|
||||
// Task subtasks count events
|
||||
GET_TASK_SUBTASKS_COUNT,
|
||||
TASK_SUBTASKS_COUNT,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ITaskStatusCategory {
|
||||
}
|
||||
|
||||
export interface IProjectTask {
|
||||
manual_progress: any;
|
||||
due_time?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
|
||||
Reference in New Issue
Block a user