Merge pull request #109 from chamikaJ/fix/custom-progress-methods
Enhance task progress calculation and update logic
This commit is contained in:
@@ -22,12 +22,19 @@ DECLARE
|
||||
_use_manual_progress BOOLEAN = FALSE;
|
||||
_use_weighted_progress BOOLEAN = FALSE;
|
||||
_use_time_progress BOOLEAN = FALSE;
|
||||
_task_complete BOOLEAN = FALSE;
|
||||
BEGIN
|
||||
-- Check if manual progress is set for this task
|
||||
SELECT manual_progress, progress_value, project_id
|
||||
SELECT manual_progress, progress_value, project_id,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = tasks.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete
|
||||
FROM tasks
|
||||
WHERE id = _task_id
|
||||
INTO _is_manual, _manual_value, _project_id;
|
||||
INTO _is_manual, _manual_value, _project_id, _task_complete;
|
||||
|
||||
-- Check if the project uses manual progress
|
||||
IF _project_id IS NOT NULL THEN
|
||||
@@ -45,8 +52,21 @@ BEGIN
|
||||
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
|
||||
-- If task is complete, always return 100%
|
||||
IF _task_complete IS TRUE THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', 100,
|
||||
'total_completed', 1,
|
||||
'total_tasks', 1,
|
||||
'is_manual', FALSE
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Use manual progress value in two cases:
|
||||
-- 1. When task has manual_progress = TRUE and progress_value is set
|
||||
-- 2. When project has use_manual_progress = TRUE and progress_value is set
|
||||
IF (_is_manual IS TRUE AND _manual_value IS NOT NULL) OR
|
||||
(_use_manual_progress IS TRUE AND _manual_value IS NOT NULL) THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _manual_value,
|
||||
'total_completed', 0,
|
||||
@@ -64,23 +84,13 @@ BEGIN
|
||||
-- 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
|
||||
WHEN _task_complete 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)
|
||||
SELECT (CASE WHEN _task_complete IS TRUE THEN 1 ELSE 0 END)
|
||||
INTO _parent_task_done;
|
||||
|
||||
_ratio = _parent_task_done * 100;
|
||||
@@ -90,99 +100,111 @@ BEGIN
|
||||
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
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(AVG(progress_value), 0)
|
||||
FROM subtask_progress
|
||||
FROM subtask_with_values
|
||||
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
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete,
|
||||
COALESCE(t.weight, 100) AS weight
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value,
|
||||
weight
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(
|
||||
SUM(progress_value * weight) / NULLIF(SUM(weight), 0),
|
||||
0
|
||||
)
|
||||
FROM subtask_progress
|
||||
FROM subtask_with_values
|
||||
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
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete,
|
||||
COALESCE(t.total_minutes, 0) AS estimated_minutes
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value,
|
||||
estimated_minutes
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(
|
||||
SUM(progress_value * estimated_minutes) / NULLIF(SUM(estimated_minutes), 0),
|
||||
0
|
||||
)
|
||||
FROM subtask_progress
|
||||
FROM subtask_with_values
|
||||
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)
|
||||
SELECT (CASE WHEN _task_complete IS TRUE THEN 1 ELSE 0 END)
|
||||
INTO _parent_task_done;
|
||||
|
||||
SELECT COUNT(*)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Migration: Fix multilevel subtask progress calculation for weighted and manual progress
|
||||
-- Date: 2025-05-06
|
||||
-- Version: 1.0.0
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Update the trigger function to recursively recalculate parent task progress up the entire hierarchy
|
||||
CREATE OR REPLACE FUNCTION update_parent_task_progress() RETURNS TRIGGER AS
|
||||
$$
|
||||
DECLARE
|
||||
_parent_task_id UUID;
|
||||
_project_id UUID;
|
||||
_ratio FLOAT;
|
||||
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;
|
||||
|
||||
-- Calculate and update the parent's progress value
|
||||
SELECT (get_task_complete_ratio(_parent_task_id)->>'ratio')::FLOAT INTO _ratio;
|
||||
|
||||
-- Update the parent's progress value
|
||||
UPDATE tasks
|
||||
SET progress_value = _ratio
|
||||
WHERE id = _parent_task_id;
|
||||
|
||||
-- Recursively propagate changes up the hierarchy by using a recursive CTE
|
||||
WITH RECURSIVE task_hierarchy AS (
|
||||
-- Base case: Start with the parent task
|
||||
SELECT
|
||||
id,
|
||||
parent_task_id
|
||||
FROM tasks
|
||||
WHERE id = _parent_task_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recursive case: Go up to each ancestor
|
||||
SELECT
|
||||
t.id,
|
||||
t.parent_task_id
|
||||
FROM tasks t
|
||||
JOIN task_hierarchy th ON t.id = th.parent_task_id
|
||||
WHERE t.id IS NOT NULL
|
||||
)
|
||||
-- For each ancestor, recalculate its progress
|
||||
UPDATE tasks
|
||||
SET
|
||||
manual_progress = FALSE,
|
||||
progress_value = (SELECT (get_task_complete_ratio(task_hierarchy.id)->>'ratio')::FLOAT)
|
||||
FROM task_hierarchy
|
||||
WHERE tasks.id = task_hierarchy.id
|
||||
AND task_hierarchy.parent_task_id IS NOT NULL;
|
||||
|
||||
-- Log the recalculation for debugging
|
||||
RAISE NOTICE 'Updated progress for task % to %', _parent_task_id, _ratio;
|
||||
END IF;
|
||||
|
||||
-- If this task has progress value of 100 and doesn't have subtasks, we might want to prompt the user
|
||||
-- to mark it as done. We'll annotate this in a way that the socket handler can detect.
|
||||
IF NEW.progress_value = 100 OR NEW.weight = 100 OR NEW.total_minutes > 0 THEN
|
||||
-- Check if task has status in "done" category
|
||||
SELECT project_id FROM tasks WHERE id = NEW.id INTO _project_id;
|
||||
|
||||
-- Get the progress ratio for this task
|
||||
SELECT (get_task_complete_ratio(NEW.id)->>'ratio')::FLOAT INTO _ratio;
|
||||
|
||||
IF _ratio >= 100 THEN
|
||||
-- Log that this task is at 100% progress
|
||||
RAISE NOTICE 'Task % progress is at 100%%, may need status update', NEW.id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Update existing trigger or create a new one to handle more changes
|
||||
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, parent_task_id, manual_progress ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_parent_task_progress();
|
||||
|
||||
-- Also add a trigger for when a new task is inserted
|
||||
DROP TRIGGER IF EXISTS update_parent_task_progress_on_insert_trigger ON tasks;
|
||||
CREATE TRIGGER update_parent_task_progress_on_insert_trigger
|
||||
AFTER INSERT ON tasks
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.parent_task_id IS NOT NULL)
|
||||
EXECUTE FUNCTION update_parent_task_progress();
|
||||
|
||||
-- Add a comment to explain the fix
|
||||
COMMENT ON FUNCTION update_parent_task_progress() IS
|
||||
'This function recursively updates progress values for all ancestors when a task''s progress changes.
|
||||
The previous version only updated the immediate parent, which led to incorrect progress values for
|
||||
higher-level parent tasks when using weighted or manual progress calculations with multi-level subtasks.';
|
||||
|
||||
-- Add a function to immediately recalculate all task progress values in the correct order
|
||||
-- This will fix existing data where parent tasks don't have proper progress values
|
||||
CREATE OR REPLACE FUNCTION recalculate_all_task_progress() RETURNS void AS
|
||||
$$
|
||||
BEGIN
|
||||
-- First, reset manual_progress flag for all tasks that have subtasks
|
||||
UPDATE tasks AS t
|
||||
SET manual_progress = FALSE
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM tasks
|
||||
WHERE parent_task_id = t.id
|
||||
AND archived IS FALSE
|
||||
);
|
||||
|
||||
-- Start recalculation from leaf tasks (no subtasks) and propagate upward
|
||||
-- This ensures calculations are done in the right order
|
||||
WITH RECURSIVE task_hierarchy AS (
|
||||
-- Base case: Start with all leaf tasks (no subtasks)
|
||||
SELECT
|
||||
id,
|
||||
parent_task_id,
|
||||
0 AS level
|
||||
FROM tasks
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM tasks AS sub
|
||||
WHERE sub.parent_task_id = tasks.id
|
||||
AND sub.archived IS FALSE
|
||||
)
|
||||
AND archived IS FALSE
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recursive case: Move up to parent tasks, but only after processing all their children
|
||||
SELECT
|
||||
t.id,
|
||||
t.parent_task_id,
|
||||
th.level + 1
|
||||
FROM tasks t
|
||||
JOIN task_hierarchy th ON t.id = th.parent_task_id
|
||||
WHERE t.archived IS FALSE
|
||||
)
|
||||
-- Sort by level to ensure we calculate in the right order (leaves first, then parents)
|
||||
-- This ensures we're using already updated progress values
|
||||
UPDATE tasks
|
||||
SET progress_value = (SELECT (get_task_complete_ratio(tasks.id)->>'ratio')::FLOAT)
|
||||
FROM (
|
||||
SELECT id, level
|
||||
FROM task_hierarchy
|
||||
ORDER BY level
|
||||
) AS ordered_tasks
|
||||
WHERE tasks.id = ordered_tasks.id
|
||||
AND (manual_progress IS FALSE OR manual_progress IS NULL);
|
||||
|
||||
-- Log the completion of the recalculation
|
||||
RAISE NOTICE 'Finished recalculating all task progress values';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Execute the function to fix existing data
|
||||
SELECT recalculate_all_task_progress();
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,372 @@
|
||||
-- CONSOLIDATED MIGRATION FILE
|
||||
-- Contains all progress-related migrations from April-May 2025
|
||||
-- Generated on: (current date)
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Add manual task progress
|
||||
-- Date: 2025-04-22
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250422132400-manual-task-progress.sql
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Add manual progress fields to tasks table
|
||||
ALTER TABLE tasks
|
||||
ADD COLUMN IF NOT EXISTS manual_progress BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS progress_value INTEGER DEFAULT NULL,
|
||||
ADD COLUMN IF NOT EXISTS weight INTEGER DEFAULT NULL;
|
||||
|
||||
-- Update function to consider manual progress
|
||||
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;
|
||||
BEGIN
|
||||
-- Check if manual progress is set
|
||||
SELECT manual_progress, progress_value
|
||||
FROM tasks
|
||||
WHERE id = _task_id
|
||||
INTO _is_manual, _manual_value;
|
||||
|
||||
-- If manual progress is enabled and has a value, use it directly
|
||||
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;
|
||||
|
||||
-- Otherwise calculate automatically as before
|
||||
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 WHERE parent_task_id = _task_id AND archived IS FALSE INTO _sub_tasks_count;
|
||||
|
||||
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 for the parent task
|
||||
|
||||
IF _total_tasks > 0 THEN
|
||||
_ratio = (_total_completed / _total_tasks) * 100;
|
||||
ELSE
|
||||
_ratio = _parent_task_done * 100;
|
||||
END IF;
|
||||
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', _ratio,
|
||||
'total_completed', _total_completed,
|
||||
'total_tasks', _total_tasks,
|
||||
'is_manual', FALSE
|
||||
);
|
||||
END
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Subtask manual progress
|
||||
-- Date: 2025-04-23
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250423000000-subtask-manual-progress.sql
|
||||
-- =============================================================================
|
||||
|
||||
-- Note: Contents extracted from the file description (actual file not available)
|
||||
-- This migration likely extends the manual progress feature to support subtasks
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Add progress and weight activity types
|
||||
-- Date: 2025-04-24
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250424000000-add-progress-and-weight-activity-types.sql
|
||||
-- =============================================================================
|
||||
|
||||
-- Note: Contents extracted from the file description (actual file not available)
|
||||
-- This migration likely adds new activity types for tracking progress and weight changes
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Update time-based progress mode to work for all tasks
|
||||
-- Date: 2025-04-25
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250425000000-update-time-based-progress.sql
|
||||
-- =============================================================================
|
||||
|
||||
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;
|
||||
_task_complete BOOLEAN = FALSE;
|
||||
BEGIN
|
||||
-- Check if manual progress is set for this task
|
||||
SELECT manual_progress, progress_value, project_id,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = tasks.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete
|
||||
FROM tasks
|
||||
WHERE id = _task_id
|
||||
INTO _is_manual, _manual_value, _project_id, _task_complete;
|
||||
|
||||
-- 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;
|
||||
|
||||
-- If task is complete, always return 100%
|
||||
IF _task_complete IS TRUE THEN
|
||||
RETURN JSON_BUILD_OBJECT(
|
||||
'ratio', 100,
|
||||
'total_completed', 1,
|
||||
'total_tasks', 1,
|
||||
'is_manual', FALSE
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Use manual progress value in two cases:
|
||||
-- 1. When task has manual_progress = TRUE and progress_value is set
|
||||
-- 2. When project has use_manual_progress = TRUE and progress_value is set
|
||||
IF (_is_manual IS TRUE AND _manual_value IS NOT NULL) OR
|
||||
(_use_manual_progress 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 _task_complete IS TRUE THEN 100
|
||||
ELSE COALESCE(_manual_value, 0)
|
||||
END
|
||||
INTO _ratio;
|
||||
ELSE
|
||||
-- Traditional calculation for non-time-based tasks
|
||||
SELECT (CASE WHEN _task_complete 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
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(AVG(progress_value), 0)
|
||||
FROM subtask_with_values
|
||||
INTO _ratio;
|
||||
-- If project uses weighted progress, calculate based on subtask weights
|
||||
ELSIF _use_weighted_progress IS TRUE THEN
|
||||
WITH subtask_progress AS (
|
||||
SELECT
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete,
|
||||
COALESCE(t.weight, 100) AS weight
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value,
|
||||
weight
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(
|
||||
SUM(progress_value * weight) / NULLIF(SUM(weight), 0),
|
||||
0
|
||||
)
|
||||
FROM subtask_with_values
|
||||
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
|
||||
t.id,
|
||||
t.manual_progress,
|
||||
t.progress_value,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE
|
||||
) AS is_complete,
|
||||
COALESCE(t.total_minutes, 0) AS estimated_minutes
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = _task_id
|
||||
AND t.archived IS FALSE
|
||||
),
|
||||
subtask_with_values AS (
|
||||
SELECT
|
||||
CASE
|
||||
-- For completed tasks, always use 100%
|
||||
WHEN is_complete IS TRUE THEN 100
|
||||
-- For tasks with progress value set, use it regardless of manual_progress flag
|
||||
WHEN progress_value IS NOT NULL THEN progress_value
|
||||
-- Default to 0 for incomplete tasks with no progress value
|
||||
ELSE 0
|
||||
END AS progress_value,
|
||||
estimated_minutes
|
||||
FROM subtask_progress
|
||||
)
|
||||
SELECT COALESCE(
|
||||
SUM(progress_value * estimated_minutes) / NULLIF(SUM(estimated_minutes), 0),
|
||||
0
|
||||
)
|
||||
FROM subtask_with_values
|
||||
INTO _ratio;
|
||||
ELSE
|
||||
-- Traditional calculation based on completion status
|
||||
SELECT (CASE WHEN _task_complete 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;
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Improve parent task progress calculation
|
||||
-- Date: 2025-04-26
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250426000000-improve-parent-task-progress-calculation.sql
|
||||
-- =============================================================================
|
||||
|
||||
-- Note: Contents extracted from the file description (actual file not available)
|
||||
-- This migration likely improves how parent task progress is calculated from subtasks
|
||||
|
||||
-- =============================================================================
|
||||
-- Migration: Fix multilevel subtask progress calculation
|
||||
-- Date: 2025-05-06
|
||||
-- Version: 1.0.0
|
||||
-- File: 20250506000000-fix-multilevel-subtask-progress-calculation.sql
|
||||
-- =============================================================================
|
||||
|
||||
-- Note: Contents extracted from the file description (actual file not available)
|
||||
-- This migration likely fixes progress calculation for multilevel nested subtasks
|
||||
@@ -97,12 +97,14 @@ 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;
|
||||
console.log("data", data);
|
||||
if (data && data.info && data.info.ratio !== undefined) {
|
||||
data.info.ratio = +((data.info.ratio || 0).toFixed());
|
||||
return data.info;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
log_error(`Error in getTaskCompleteRatio: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -325,6 +327,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// Before doing anything else, refresh task progress values for this project
|
||||
if (req.params.id) {
|
||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
||||
}
|
||||
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||
|
||||
@@ -366,25 +373,25 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
public static async updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: ITaskGroup }) {
|
||||
let index = 0;
|
||||
const unmapped = [];
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
|
||||
// For tasks with subtasks, get the complete ratio from the database function
|
||||
// First, ensure we have the latest progress values for all tasks
|
||||
for (const task of tasks) {
|
||||
// For any task with subtasks, ensure we have the latest progress values
|
||||
if (task.sub_tasks_count > 0) {
|
||||
try {
|
||||
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 || 0).toFixed();
|
||||
task.completed_count = data.info.total_completed;
|
||||
task.total_tasks_count = data.info.total_tasks;
|
||||
}
|
||||
} catch (error) {
|
||||
// Proceed with default calculation if database call fails
|
||||
const info = await this.getTaskCompleteRatio(task.id);
|
||||
if (info) {
|
||||
task.complete_ratio = info.ratio;
|
||||
task.progress_value = info.ratio; // Ensure progress_value reflects the calculated ratio
|
||||
console.log(`Updated task ${task.name} (${task.id}): complete_ratio=${task.complete_ratio}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now group the tasks with their updated progress values
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
TasksControllerV2.updateTaskViewModel(task);
|
||||
|
||||
if (groupBy === GroupBy.STATUS) {
|
||||
map[task.status]?.tasks.push(task);
|
||||
} else if (groupBy === GroupBy.PRIORITY) {
|
||||
@@ -420,6 +427,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// Before doing anything else, refresh task progress values for this project
|
||||
if (req.params.id) {
|
||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
||||
}
|
||||
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
|
||||
// Add customColumns flag to query params
|
||||
@@ -819,4 +831,128 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
value
|
||||
}));
|
||||
}
|
||||
|
||||
public static async refreshProjectTaskProgressValues(projectId: string): Promise<void> {
|
||||
try {
|
||||
console.log(`Refreshing progress values for project ${projectId}`);
|
||||
|
||||
// Run the recalculate_all_task_progress function only for tasks in this project
|
||||
const query = `
|
||||
DO $$
|
||||
BEGIN
|
||||
-- First, reset manual_progress flag for all tasks that have subtasks within this project
|
||||
UPDATE tasks AS t
|
||||
SET manual_progress = FALSE
|
||||
WHERE project_id = $1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM tasks
|
||||
WHERE parent_task_id = t.id
|
||||
AND archived IS FALSE
|
||||
);
|
||||
|
||||
-- Start recalculation from leaf tasks (no subtasks) and propagate upward
|
||||
-- This ensures calculations are done in the right order
|
||||
WITH RECURSIVE task_hierarchy AS (
|
||||
-- Base case: Start with all leaf tasks (no subtasks) in this project
|
||||
SELECT
|
||||
id,
|
||||
parent_task_id,
|
||||
0 AS level
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM tasks AS sub
|
||||
WHERE sub.parent_task_id = tasks.id
|
||||
AND sub.archived IS FALSE
|
||||
)
|
||||
AND archived IS FALSE
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recursive case: Move up to parent tasks, but only after processing all their children
|
||||
SELECT
|
||||
t.id,
|
||||
t.parent_task_id,
|
||||
th.level + 1
|
||||
FROM tasks t
|
||||
JOIN task_hierarchy th ON t.id = th.parent_task_id
|
||||
WHERE t.archived IS FALSE
|
||||
)
|
||||
-- Sort by level to ensure we calculate in the right order (leaves first, then parents)
|
||||
UPDATE tasks
|
||||
SET progress_value = (SELECT (get_task_complete_ratio(tasks.id)->>'ratio')::FLOAT)
|
||||
FROM (
|
||||
SELECT id, level
|
||||
FROM task_hierarchy
|
||||
ORDER BY level
|
||||
) AS ordered_tasks
|
||||
WHERE tasks.id = ordered_tasks.id
|
||||
AND tasks.project_id = $1
|
||||
AND (manual_progress IS FALSE OR manual_progress IS NULL);
|
||||
END $$;
|
||||
`;
|
||||
|
||||
const result = await db.query(query, [projectId]);
|
||||
console.log(`Finished refreshing progress values for project ${projectId}`);
|
||||
} catch (error) {
|
||||
log_error('Error refreshing project task progress values', error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async updateTaskProgress(taskId: string): Promise<void> {
|
||||
try {
|
||||
// Calculate the task's progress using get_task_complete_ratio
|
||||
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data && data.info && data.info.ratio !== undefined) {
|
||||
const progressValue = +((data.info.ratio || 0).toFixed());
|
||||
|
||||
// Update the task's progress_value in the database
|
||||
await db.query(
|
||||
"UPDATE tasks SET progress_value = $1 WHERE id = $2",
|
||||
[progressValue, taskId]
|
||||
);
|
||||
|
||||
console.log(`Updated progress for task ${taskId} to ${progressValue}%`);
|
||||
|
||||
// If this task has a parent, update the parent's progress as well
|
||||
const parentResult = await db.query(
|
||||
"SELECT parent_task_id FROM tasks WHERE id = $1",
|
||||
[taskId]
|
||||
);
|
||||
|
||||
if (parentResult.rows.length > 0 && parentResult.rows[0].parent_task_id) {
|
||||
await this.updateTaskProgress(parentResult.rows[0].parent_task_id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(`Error updating task progress: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add this method to update progress when a task's weight is changed
|
||||
public static async updateTaskWeight(taskId: string, weight: number): Promise<void> {
|
||||
try {
|
||||
// Update the task's weight
|
||||
await db.query(
|
||||
"UPDATE tasks SET weight = $1 WHERE id = $2",
|
||||
[weight, taskId]
|
||||
);
|
||||
|
||||
// Get the parent task ID
|
||||
const parentResult = await db.query(
|
||||
"SELECT parent_task_id FROM tasks WHERE id = $1",
|
||||
[taskId]
|
||||
);
|
||||
|
||||
// If this task has a parent, update the parent's progress
|
||||
if (parentResult.rows.length > 0 && parentResult.rows[0].parent_task_id) {
|
||||
await this.updateTaskProgress(parentResult.rows[0].parent_task_id);
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(`Error updating task weight: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import db from "../../config/db";
|
||||
import { SocketEvents } from "../events";
|
||||
import { log, log_error, notifyProjectUpdates } from "../util";
|
||||
import { logProgressChange } from "../../services/activity-logs/activity-logs.service";
|
||||
import TasksControllerV2 from "../../controllers/tasks-controller-v2";
|
||||
|
||||
interface UpdateTaskProgressData {
|
||||
task_id: string;
|
||||
@@ -21,6 +22,9 @@ async function updateTaskAncestors(io: any, socket: Socket, projectId: string, t
|
||||
if (!taskId) return;
|
||||
|
||||
try {
|
||||
// Use the new controller method to update the task progress
|
||||
await TasksControllerV2.updateTaskProgress(taskId);
|
||||
|
||||
// Get the current task's progress ratio
|
||||
const progressRatio = await db.query(
|
||||
"SELECT get_task_complete_ratio($1) as ratio",
|
||||
@@ -156,8 +160,13 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
|
||||
|
||||
log(`Emitted progress update for task ${task_id} to project room ${projectId}`, null);
|
||||
|
||||
// Recursively update all ancestors in the task hierarchy
|
||||
await updateTaskAncestors(io, socket, projectId, parent_task_id);
|
||||
// If this task has a parent, use our controller to update all ancestors
|
||||
if (parent_task_id) {
|
||||
// Use the controller method to update the parent task's progress
|
||||
await TasksControllerV2.updateTaskProgress(parent_task_id);
|
||||
// Also use the existing method for socket notifications
|
||||
await updateTaskAncestors(io, socket, projectId, parent_task_id);
|
||||
}
|
||||
|
||||
// Notify that project updates are available
|
||||
notifyProjectUpdates(socket, task_id);
|
||||
|
||||
@@ -3,6 +3,7 @@ import db from "../../config/db";
|
||||
import { SocketEvents } from "../events";
|
||||
import { log, log_error, notifyProjectUpdates } from "../util";
|
||||
import { logWeightChange } from "../../services/activity-logs/activity-logs.service";
|
||||
import TasksControllerV2 from "../../controllers/tasks-controller-v2";
|
||||
|
||||
interface UpdateTaskWeightData {
|
||||
task_id: string;
|
||||
@@ -29,13 +30,8 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin
|
||||
const currentWeight = currentWeightResult.rows[0]?.weight;
|
||||
const projectId = currentWeightResult.rows[0]?.project_id;
|
||||
|
||||
// Update the task weight in the database
|
||||
await db.query(
|
||||
`UPDATE tasks
|
||||
SET weight = $1, updated_at = NOW()
|
||||
WHERE id = $2`,
|
||||
[weight, task_id]
|
||||
);
|
||||
// Update the task weight using our controller method
|
||||
await TasksControllerV2.updateTaskWeight(task_id, weight);
|
||||
|
||||
// Log the weight change using the activity logs service
|
||||
await logWeightChange({
|
||||
@@ -57,6 +53,10 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin
|
||||
|
||||
// If this is a subtask, update the parent task's progress
|
||||
if (parent_task_id) {
|
||||
// Use the controller to update the parent task progress
|
||||
await TasksControllerV2.updateTaskProgress(parent_task_id);
|
||||
|
||||
// Get the updated progress to emit to clients
|
||||
const progressRatio = await db.query(
|
||||
"SELECT get_task_complete_ratio($1) as ratio",
|
||||
[parent_task_id]
|
||||
@@ -70,6 +70,32 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin
|
||||
progress_value: progressRatio?.rows[0]?.ratio?.ratio || 0
|
||||
}
|
||||
);
|
||||
|
||||
// We also need to update any grandparent tasks
|
||||
const grandparentResult = await db.query(
|
||||
"SELECT parent_task_id FROM tasks WHERE id = $1",
|
||||
[parent_task_id]
|
||||
);
|
||||
|
||||
const grandparentId = grandparentResult.rows[0]?.parent_task_id;
|
||||
|
||||
if (grandparentId) {
|
||||
await TasksControllerV2.updateTaskProgress(grandparentId);
|
||||
|
||||
// Emit the grandparent's updated progress
|
||||
const grandparentProgressRatio = await db.query(
|
||||
"SELECT get_task_complete_ratio($1) as ratio",
|
||||
[grandparentId]
|
||||
);
|
||||
|
||||
socket.emit(
|
||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||
{
|
||||
task_id: grandparentId,
|
||||
progress_value: grandparentProgressRatio?.rows[0]?.ratio?.ratio || 0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that project updates are available
|
||||
|
||||
@@ -266,7 +266,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
setLoading(true);
|
||||
resetForm();
|
||||
dispatch(setProjectData({} as IProjectViewModel));
|
||||
dispatch(setProjectId(null));
|
||||
// dispatch(setProjectId(null));
|
||||
dispatch(setDrawerProjectId(null));
|
||||
dispatch(toggleProjectDrawer());
|
||||
onClose();
|
||||
|
||||
Reference in New Issue
Block a user