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
|
### Example
|
||||||
|
|
||||||
If you have a parent task with three subtasks:
|
If you have a parent task with three subtasks:
|
||||||
- Subtask A: 50% complete, Weight 200 (critical task)
|
- Subtask A: 50% complete, Weight 60% (important task)
|
||||||
- Subtask B: 75% complete, Weight 100 (standard task)
|
- Subtask B: 75% complete, Weight 20% (less important task)
|
||||||
- Subtask C: 25% complete, Weight 300 (major 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
|
### Important Notes About Weights
|
||||||
|
|
||||||
- Default weight is 100 if not specified
|
- Default weight is 100% if not specified
|
||||||
- You can set weights from 0 to any reasonable number (typically 1-1000)
|
- Weights range from 0% to 100%
|
||||||
- Setting a weight to 0 removes that task from progress calculations
|
- Setting a weight to 0% removes that task from progress calculations
|
||||||
- Only explicitly set weights for tasks that should have different importance
|
- Only explicitly set weights for tasks that should have different importance
|
||||||
- Weights are only relevant for subtasks, not for independent tasks
|
- 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
|
- The UI shows a weight input for subtasks in the task drawer
|
||||||
- Manual progress input is still required for tasks without subtasks
|
- Manual progress input is still required for tasks without subtasks
|
||||||
- Default weight is 100 if not specified
|
- Default weight is 100 if not specified
|
||||||
|
- Weight values range from 0 to 100%
|
||||||
|
|
||||||
**Calculation Logic:**
|
**Calculation Logic:**
|
||||||
- For tasks without subtasks: Uses the manually entered progress value
|
- 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)
|
- If a leaf task's progress is not manually set, it defaults to 0% (or 100% if completed)
|
||||||
|
|
||||||
2. **Weight Assignment**:
|
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
|
- 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**:
|
3. **Parent Task Calculation**:
|
||||||
The weighted progress formula is:
|
The weighted progress formula is:
|
||||||
@@ -237,24 +237,24 @@ In Weighted Progress mode, both the manual progress input and weight assignment
|
|||||||
|
|
||||||
**Example Calculation**:
|
**Example Calculation**:
|
||||||
Consider a parent task with three subtasks:
|
Consider a parent task with three subtasks:
|
||||||
- Subtask A: Progress 50%, Weight 200
|
- Subtask A: Progress 50%, Weight 60%
|
||||||
- Subtask B: Progress 75%, Weight 100
|
- Subtask B: Progress 75%, Weight 20%
|
||||||
- Subtask C: Progress 25%, Weight 300
|
- Subtask C: Progress 25%, Weight 100%
|
||||||
|
|
||||||
Calculation:
|
Calculation:
|
||||||
```
|
```
|
||||||
ParentProgress = ((50 * 200) + (75 * 100) + (25 * 300)) / (200 + 100 + 300)
|
ParentProgress = ((50 * 60) + (75 * 20) + (25 * 100)) / (60 + 20 + 100)
|
||||||
ParentProgress = (10000 + 7500 + 7500) / 600
|
ParentProgress = (3000 + 1500 + 2500) / 180
|
||||||
ParentProgress = 25000 / 600
|
ParentProgress = 7000 / 180
|
||||||
ParentProgress = 41.67%
|
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.
|
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**:
|
4. **Zero Weight Handling**:
|
||||||
Tasks with zero weight are excluded from the calculation:
|
Tasks with zero weight are excluded from the calculation:
|
||||||
- Subtask A: Progress 40%, Weight 50
|
- Subtask A: Progress 40%, Weight 50%
|
||||||
- Subtask B: Progress 80%, Weight 0
|
- Subtask B: Progress 80%, Weight 0%
|
||||||
|
|
||||||
Calculation:
|
Calculation:
|
||||||
```
|
```
|
||||||
@@ -263,13 +263,13 @@ In Weighted Progress mode, both the manual progress input and weight assignment
|
|||||||
ParentProgress = 40%
|
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**:
|
5. **Default Weight Behavior**:
|
||||||
When weights aren't explicitly assigned to some tasks:
|
When weights aren't explicitly assigned to some tasks:
|
||||||
- Subtask A: Progress 30%, Weight 60 (explicitly set)
|
- Subtask A: Progress 30%, Weight 60% (explicitly set)
|
||||||
- Subtask B: Progress 70%, Weight not set (defaults to 100)
|
- Subtask B: Progress 70%, Weight not set (defaults to 100%)
|
||||||
- Subtask C: Progress 90%, Weight not set (defaults to 100)
|
- Subtask C: Progress 90%, Weight not set (defaults to 100%)
|
||||||
|
|
||||||
Calculation:
|
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;
|
INTO _use_manual_progress, _use_weighted_progress, _use_time_progress;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- If manual progress is enabled and has a value, use it directly
|
-- Get all subtasks
|
||||||
IF _is_manual IS TRUE AND _manual_value IS NOT NULL THEN
|
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(
|
RETURN JSON_BUILD_OBJECT(
|
||||||
'ratio', _manual_value,
|
'ratio', _manual_value,
|
||||||
'total_completed', 0,
|
'total_completed', 0,
|
||||||
@@ -49,12 +55,6 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END IF;
|
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 there are no subtasks, just use the parent task's status
|
||||||
IF _sub_tasks_count = 0 THEN
|
IF _sub_tasks_count = 0 THEN
|
||||||
SELECT (CASE
|
SELECT (CASE
|
||||||
@@ -145,7 +145,7 @@ BEGIN
|
|||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
END AS progress_value,
|
END AS progress_value,
|
||||||
COALESCE(total_hours * 60 + total_minutes, 0) AS estimated_minutes
|
COALESCE(total_minutes, 0) AS estimated_minutes
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
WHERE t.parent_task_id = _task_id
|
WHERE t.parent_task_id = _task_id
|
||||||
AND t.archived IS FALSE
|
AND t.archived IS FALSE
|
||||||
@@ -350,8 +350,8 @@ BEGIN
|
|||||||
RETURNING id INTO _project_id;
|
RETURNING id INTO _project_id;
|
||||||
|
|
||||||
-- register the project log
|
-- register the project log
|
||||||
INSERT INTO project_logs (project_id, team_id, team_member_id, description)
|
INSERT INTO project_logs (project_id, team_id, description)
|
||||||
VALUES (_project_id, _team_id, _team_member_id, _project_created_log)
|
VALUES (_project_id, _team_id, _project_created_log)
|
||||||
RETURNING id INTO _project_created_log_id;
|
RETURNING id INTO _project_created_log_id;
|
||||||
|
|
||||||
-- add the team member in the project as a user
|
-- add the team member in the project as a user
|
||||||
@@ -360,8 +360,8 @@ BEGIN
|
|||||||
(SELECT id FROM project_access_levels WHERE key = 'MEMBER'));
|
(SELECT id FROM project_access_levels WHERE key = 'MEMBER'));
|
||||||
|
|
||||||
-- register the project log
|
-- register the project log
|
||||||
INSERT INTO project_logs (project_id, team_id, team_member_id, description)
|
INSERT INTO project_logs (project_id, team_id, description)
|
||||||
VALUES (_project_id, _team_id, _team_member_id, _project_member_added_log);
|
VALUES (_project_id, _team_id, _project_member_added_log);
|
||||||
|
|
||||||
-- insert default project columns
|
-- insert default project columns
|
||||||
PERFORM insert_task_list_columns(_project_id);
|
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_weighted_progress BOOLEAN DEFAULT FALSE,
|
||||||
ADD COLUMN IF NOT EXISTS use_time_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;
|
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 {
|
try {
|
||||||
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
|
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
data.info.ratio = +data.info.ratio.toFixed();
|
if (data && data.info && data.info.ratio !== undefined) {
|
||||||
return data.info;
|
data.info.ratio = +((data.info.ratio || 0).toFixed());
|
||||||
|
return data.info;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
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_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_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 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 phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
|
||||||
(SELECT name
|
(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 result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
if (data && data.info) {
|
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.completed_count = data.info.total_completed;
|
||||||
task.total_tasks_count = data.info.total_tasks;
|
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 result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]);
|
||||||
const [ratioData] = result.rows;
|
const [ratioData] = result.rows;
|
||||||
if (ratioData && ratioData.info) {
|
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.completed_count = ratioData.info.total_completed;
|
||||||
task.total_tasks_count = ratioData.info.total_tasks;
|
task.total_tasks_count = ratioData.info.total_tasks;
|
||||||
console.log(`Updated task ${task.id} (${task.name}) from DB: complete_ratio=${task.complete_ratio}`);
|
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));
|
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()
|
@HandleExceptions()
|
||||||
public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
|
||||||
@@ -521,6 +572,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
? [req.body.id, req.body.to_group_id]
|
? [req.body.id, req.body.to_group_id]
|
||||||
: [req.body.id, req.body.project_id, req.body.parent_task_id, req.body.to_group_id];
|
: [req.body.id, req.body.project_id, req.body.parent_task_id, req.body.to_group_id];
|
||||||
await db.query(q, params);
|
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 result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
|
|||||||
@@ -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 { log_error, notifyProjectUpdates } from "../util";
|
||||||
import { getTaskDetails, logTotalMinutes } from "../../services/activity-logs/activity-logs.service";
|
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 {
|
try {
|
||||||
// (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
|
// (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 body = JSON.parse(data as string);
|
||||||
|
|
||||||
const hours = body.total_hours || 0;
|
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 task_data = await getTaskDetails(body.task_id, "total_minutes");
|
||||||
|
|
||||||
const result0 = await db.query(q, [body.task_id, totalMinutes]);
|
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 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;
|
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
|
total_minutes_spent: dd.total_minutes_spent || 0
|
||||||
};
|
};
|
||||||
socket.emit(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), TasksController.updateTaskViewModel(d));
|
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);
|
notifyProjectUpdates(socket, d.id);
|
||||||
|
|
||||||
logTotalMinutes({
|
logTotalMinutes({
|
||||||
|
|||||||
@@ -10,6 +10,52 @@ interface UpdateTaskProgressData {
|
|||||||
parent_task_id: string | null;
|
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) {
|
export async function on_update_task_progress(io: any, socket: Socket, data: string) {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(data) as UpdateTaskProgressData;
|
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]
|
[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 this is a parent task, we shouldn't set manual progress
|
||||||
if (subtaskCount > 0) {
|
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
|
// Get the current progress value to log the change
|
||||||
const currentProgressResult = await db.query(
|
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]
|
[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
|
// Log the progress change using the activity logs service
|
||||||
await logProgressChange({
|
await logProgressChange({
|
||||||
task_id,
|
task_id,
|
||||||
old_value: currentProgress !== null ? currentProgress.toString() : '0',
|
old_value: currentProgress !== null ? currentProgress.toString() : "0",
|
||||||
new_value: progress_value.toString(),
|
new_value: progress_value.toString(),
|
||||||
socket
|
socket
|
||||||
});
|
});
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// Emit the update to all clients in the project room
|
// Emit the update to all clients in the project room
|
||||||
io.to(projectId).emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id,
|
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
|
// Recursively update all ancestors in the task hierarchy
|
||||||
if (parent_task_id) {
|
await updateTaskAncestors(io, socket, projectId, 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
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify that project updates are available
|
// Notify that project updates are available
|
||||||
notifyProjectUpdates(socket, task_id);
|
notifyProjectUpdates(socket, task_id);
|
||||||
@@ -95,4 +124,4 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error(error);
|
log_error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Log the weight change using the activity logs service
|
||||||
await logWeightChange({
|
await logWeightChange({
|
||||||
task_id,
|
task_id,
|
||||||
old_value: currentWeight !== null ? currentWeight.toString() : '100',
|
old_value: currentWeight !== null ? currentWeight.toString() : "100",
|
||||||
new_value: weight.toString(),
|
new_value: weight.toString(),
|
||||||
socket
|
socket
|
||||||
});
|
});
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// Emit the update to all clients in the project room
|
// Emit the update to all clients in the project room
|
||||||
io.to(projectId).emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id,
|
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
|
// Emit the parent task's updated progress
|
||||||
io.to(projectId).emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id: parent_task_id,
|
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_PROGRESS,
|
||||||
UPDATE_TASK_WEIGHT,
|
UPDATE_TASK_WEIGHT,
|
||||||
TASK_PROGRESS_UPDATED,
|
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_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_progress } from "./commands/on-update-task-progress";
|
||||||
import { on_update_task_weight } from "./commands/on-update-task-weight";
|
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) {
|
export function register(io: any, socket: Socket) {
|
||||||
log(socket.id, "client registered");
|
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.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_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.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.io built-in event
|
||||||
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));
|
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 { ITaskViewModel } from '@/types/tasks/task.types';
|
||||||
import Flex from 'antd/lib/flex';
|
import Flex from 'antd/lib/flex';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
|
||||||
interface TaskDrawerProgressProps {
|
interface TaskDrawerProgressProps {
|
||||||
@@ -17,16 +17,48 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
const { t } = useTranslation('task-drawer/task-drawer');
|
const { t } = useTranslation('task-drawer/task-drawer');
|
||||||
const { project } = useAppSelector(state => state.projectReducer);
|
const { project } = useAppSelector(state => state.projectReducer);
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
|
const [confirmedHasSubtasks, setConfirmedHasSubtasks] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const isSubTask = !!task?.parent_task_id;
|
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)
|
// Additional debug logging
|
||||||
// Parent tasks get their progress calculated from subtasks
|
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;
|
const showManualProgressInput = !hasSubTasks;
|
||||||
|
|
||||||
// Only show weight input for subtasks in weighted progress mode
|
// 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(() => {
|
useEffect(() => {
|
||||||
// Listen for progress updates from the server
|
// Listen for progress updates from the server
|
||||||
@@ -53,8 +85,13 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
};
|
};
|
||||||
}, [socket, connected, task.id, form]);
|
}, [socket, connected, task.id, form]);
|
||||||
|
|
||||||
|
// One last check before rendering
|
||||||
|
if (hasSubTasks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleProgressChange = (value: number | 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
|
// Ensure parent_task_id is not undefined
|
||||||
const parent_task_id = task.parent_task_id || null;
|
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 this is a subtask, request the parent's progress to be updated in UI
|
||||||
if (parent_task_id) {
|
if (parent_task_id) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -84,7 +114,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleWeightChange = (value: number | null) => {
|
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
|
// Ensure parent_task_id is not undefined
|
||||||
const parent_task_id = task.parent_task_id || null;
|
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
|
return null; // Don't show any progress inputs if not applicable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final safety check
|
||||||
|
if (hasSubTasks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTaskWeightInput && (
|
{showTaskWeightInput && (
|
||||||
|
|||||||
@@ -33,6 +33,42 @@ interface TaskDetailsFormProps {
|
|||||||
taskFormViewModel?: ITaskFormViewModel | null;
|
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 TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => {
|
||||||
const { t } = useTranslation('task-drawer/task-drawer');
|
const { t } = useTranslation('task-drawer/task-drawer');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -121,8 +157,11 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
|||||||
|
|
||||||
<TaskDrawerEstimation t={t} task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
<TaskDrawerEstimation t={t} task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||||
|
|
||||||
{(project?.use_manual_progress || project?.use_weighted_progress) && (taskFormViewModel?.task) && (
|
{taskFormViewModel?.task && (
|
||||||
<TaskDrawerProgress task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
<ConditionalProgressInput
|
||||||
|
task={taskFormViewModel?.task as ITaskViewModel}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item name="priority" label={t('taskInfoTab.details.priority')}>
|
<Form.Item name="priority" label={t('taskInfoTab.details.priority')}>
|
||||||
|
|||||||
@@ -572,14 +572,29 @@ const taskSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
const { taskId, progress, totalTasksCount, completedCount } = action.payload;
|
const { taskId, progress, totalTasksCount, completedCount } = action.payload;
|
||||||
|
|
||||||
for (const group of state.taskGroups) {
|
// Helper function to find and update a task at any nesting level
|
||||||
const task = group.tasks.find(task => task.id === taskId);
|
const findAndUpdateTask = (tasks: IProjectTask[]) => {
|
||||||
if (task) {
|
for (const task of tasks) {
|
||||||
task.complete_ratio = progress;
|
if (task.id === taskId) {
|
||||||
task.total_tasks_count = totalTasksCount;
|
task.complete_ratio = progress;
|
||||||
task.completed_count = completedCount;
|
task.total_tasks_count = totalTasksCount;
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const ProjectViewTaskList = () => {
|
|||||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
<TaskListFilters position="list" />
|
<TaskListFilters position="list" />
|
||||||
|
|
||||||
{(taskGroups.length === 0 && !loadingGroups) ? (
|
{(taskGroups && taskGroups.length === 0 && !loadingGroups) ? (
|
||||||
<Empty description="No tasks group found" />
|
<Empty description="No tasks group found" />
|
||||||
) : (
|
) : (
|
||||||
<Skeleton active loading={loadingGroups} className='mt-4 p-4'>
|
<Skeleton active loading={loadingGroups} className='mt-4 p-4'>
|
||||||
|
|||||||
@@ -1,20 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Progress, Tooltip } from 'antd';
|
import { Progress, Tooltip } from 'antd';
|
||||||
import './task-list-progress-cell.css';
|
import './task-list-progress-cell.css';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
type TaskListProgressCellProps = {
|
type TaskListProgressCellProps = {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => {
|
const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => {
|
||||||
return task.is_sub_task ? null : (
|
const { project } = useAppSelector(state => state.projectReducer);
|
||||||
<Tooltip title={`${task.completed_count || 0} / ${task.total_tasks_count || 0}`}>
|
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}
|
||||||
|
type="circle"
|
||||||
|
size={24}
|
||||||
|
style={{ cursor: 'default' }}
|
||||||
|
strokeWidth={(task.complete_ratio || 0) >= 100 ? 9 : 7}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subtasks with manual progress enabled, show the progress
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={hasManualProgress ? `Manual: ${task.progress_value || 0}%` : `${task.progress || 0}%`}
|
||||||
|
>
|
||||||
<Progress
|
<Progress
|
||||||
percent={task.complete_ratio || 0}
|
percent={hasManualProgress ? (task.progress_value || 0) : (task.progress || 0)}
|
||||||
type="circle"
|
type="circle"
|
||||||
size={24}
|
size={22} // Slightly smaller for subtasks
|
||||||
style={{ cursor: 'default' }}
|
style={{ cursor: 'default' }}
|
||||||
strokeWidth={(task.complete_ratio || 0) >= 100 ? 9 : 7}
|
strokeWidth={(task.progress || 0) >= 100 ? 9 : 7}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,4 +63,8 @@ export enum SocketEvents {
|
|||||||
UPDATE_TASK_PROGRESS,
|
UPDATE_TASK_PROGRESS,
|
||||||
UPDATE_TASK_WEIGHT,
|
UPDATE_TASK_WEIGHT,
|
||||||
TASK_PROGRESS_UPDATED,
|
TASK_PROGRESS_UPDATED,
|
||||||
|
|
||||||
|
// Task subtasks count events
|
||||||
|
GET_TASK_SUBTASKS_COUNT,
|
||||||
|
TASK_SUBTASKS_COUNT,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface ITaskStatusCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectTask {
|
export interface IProjectTask {
|
||||||
|
manual_progress: any;
|
||||||
due_time?: string;
|
due_time?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user