diff --git a/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql b/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql index b817ddd7..7b02bef7 100644 --- a/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql +++ b/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql @@ -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(*) diff --git a/worklenz-backend/database/migrations/20250506000000-fix-multilevel-subtask-progress-calculation.sql b/worklenz-backend/database/migrations/20250506000000-fix-multilevel-subtask-progress-calculation.sql new file mode 100644 index 00000000..1a8d4cba --- /dev/null +++ b/worklenz-backend/database/migrations/20250506000000-fix-multilevel-subtask-progress-calculation.sql @@ -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; \ No newline at end of file diff --git a/worklenz-backend/database/migrations/consolidated-progress-migrations.sql b/worklenz-backend/database/migrations/consolidated-progress-migrations.sql new file mode 100644 index 00000000..7efe5b3e --- /dev/null +++ b/worklenz-backend/database/migrations/consolidated-progress-migrations.sql @@ -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 \ No newline at end of file diff --git a/worklenz-backend/src/controllers/tasks-controller-v2.ts b/worklenz-backend/src/controllers/tasks-controller-v2.ts index 286157cb..3284560f 100644 --- a/worklenz-backend/src/controllers/tasks-controller-v2.ts +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -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 { + // 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 = []; + + // First, ensure we have the latest progress values for all tasks for (const task of tasks) { - task.index = index++; - - // For tasks with subtasks, get the complete ratio from the database function + // 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 { + // 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 { + 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 { + 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 { + 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}`); + } + } } diff --git a/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts b/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts index c04d37d4..239b7ff8 100644 --- a/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts +++ b/worklenz-backend/src/socket.io/commands/on-update-task-progress.ts @@ -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); diff --git a/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts b/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts index 664d0806..7d0f65bf 100644 --- a/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts +++ b/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts @@ -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 diff --git a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx index f5b5cb98..7bae3717 100644 --- a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx +++ b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx @@ -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();