From a5b881c609071e707de98a51a193646599af0b65 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Fri, 2 May 2025 13:21:32 +0530 Subject: [PATCH] Enhance task progress calculation and UI handling - Updated task progress calculation logic to incorporate weights and time-based estimations for subtasks. - Improved SQL migrations to support new progress calculation methods and ensure accurate parent task updates. - Enhanced frontend components to conditionally display progress inputs based on task type and project settings. - Implemented socket events for real-time updates on subtask counts and progress changes, ensuring consistent UI behavior. - Added logging for progress updates and task state changes to improve debugging and user experience. --- docs/task-progress-guide-for-users.md | 14 +- docs/task-progress-methods.md | 32 +-- ...20250423000000-subtask-manual-progress.sql | 40 ++- ...50425000000-update-time-based-progress.sql | 221 ++++++++++++++ ...prove-parent-task-progress-calculation.sql | 272 ++++++++++++++++++ .../src/controllers/tasks-controller-v2.ts | 65 ++++- .../commands/on-get-task-subtasks-count.ts | 39 +++ .../commands/on-time-estimation-change.ts | 71 ++++- .../commands/on-update-task-progress.ts | 102 +++---- .../commands/on-update-task-weight.ts | 8 +- worklenz-backend/src/socket.io/events.ts | 4 + worklenz-backend/src/socket.io/index.ts | 2 + .../task-drawer-progress.tsx | 63 +++- .../shared/info-tab/task-details-form.tsx | 43 ++- .../taskList/project-view-task-list.tsx | 2 +- worklenz-frontend/src/shared/socket-events.ts | 4 + 16 files changed, 870 insertions(+), 112 deletions(-) create mode 100644 worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql create mode 100644 worklenz-backend/database/migrations/20250426000000-improve-parent-task-progress-calculation.sql create mode 100644 worklenz-backend/src/socket.io/commands/on-get-task-subtasks-count.ts diff --git a/docs/task-progress-guide-for-users.md b/docs/task-progress-guide-for-users.md index 081ca16e..350329fa 100644 --- a/docs/task-progress-guide-for-users.md +++ b/docs/task-progress-guide-for-users.md @@ -62,17 +62,17 @@ The parent task will show as 60% complete (average of 30%, 60%, and 90%). ### Example If you have a parent task with three subtasks: -- Subtask A: 50% complete, Weight 200 (critical task) -- Subtask B: 75% complete, Weight 100 (standard task) -- Subtask C: 25% complete, Weight 300 (major task) +- Subtask A: 50% complete, Weight 60% (important task) +- Subtask B: 75% complete, Weight 20% (less important task) +- Subtask C: 25% complete, Weight 100% (critical task) -The parent task will be approximately 42% complete, with Subtask C having the greatest impact due to its higher weight. +The parent task will be approximately 39% complete, with Subtask C having the greatest impact due to its higher weight. ### Important Notes About Weights -- Default weight is 100 if not specified -- You can set weights from 0 to any reasonable number (typically 1-1000) -- Setting a weight to 0 removes that task from progress calculations +- Default weight is 100% if not specified +- Weights range from 0% to 100% +- Setting a weight to 0% removes that task from progress calculations - Only explicitly set weights for tasks that should have different importance - Weights are only relevant for subtasks, not for independent tasks diff --git a/docs/task-progress-methods.md b/docs/task-progress-methods.md index 5a75ac95..b931b7f5 100644 --- a/docs/task-progress-methods.md +++ b/docs/task-progress-methods.md @@ -44,6 +44,7 @@ This mode allows assigning different weights to subtasks to reflect their relati - The UI shows a weight input for subtasks in the task drawer - Manual progress input is still required for tasks without subtasks - Default weight is 100 if not specified +- Weight values range from 0 to 100% **Calculation Logic:** - For tasks without subtasks: Uses the manually entered progress value @@ -224,10 +225,9 @@ In Weighted Progress mode, both the manual progress input and weight assignment - If a leaf task's progress is not manually set, it defaults to 0% (or 100% if completed) 2. **Weight Assignment**: - - Each task can be assigned a weight value (default 100 if not specified) + - Each task can be assigned a weight value between 0-100% (default 100% if not specified) - Higher weight values give tasks more influence in parent task progress calculations - - Weight values are typically whole numbers between 0 and 1000 - - A weight of 0 means the task doesn't contribute to the parent's progress calculation + - A weight of 0% means the task doesn't contribute to the parent's progress calculation 3. **Parent Task Calculation**: The weighted progress formula is: @@ -237,24 +237,24 @@ In Weighted Progress mode, both the manual progress input and weight assignment **Example Calculation**: Consider a parent task with three subtasks: - - Subtask A: Progress 50%, Weight 200 - - Subtask B: Progress 75%, Weight 100 - - Subtask C: Progress 25%, Weight 300 + - Subtask A: Progress 50%, Weight 60% + - Subtask B: Progress 75%, Weight 20% + - Subtask C: Progress 25%, Weight 100% Calculation: ``` - ParentProgress = ((50 * 200) + (75 * 100) + (25 * 300)) / (200 + 100 + 300) - ParentProgress = (10000 + 7500 + 7500) / 600 - ParentProgress = 25000 / 600 - ParentProgress = 41.67% + ParentProgress = ((50 * 60) + (75 * 20) + (25 * 100)) / (60 + 20 + 100) + ParentProgress = (3000 + 1500 + 2500) / 180 + ParentProgress = 7000 / 180 + ParentProgress = 38.89% ``` Notice that Subtask C, despite having the lowest progress, has a significant impact on the parent task progress due to its higher weight. 4. **Zero Weight Handling**: Tasks with zero weight are excluded from the calculation: - - Subtask A: Progress 40%, Weight 50 - - Subtask B: Progress 80%, Weight 0 + - Subtask A: Progress 40%, Weight 50% + - Subtask B: Progress 80%, Weight 0% Calculation: ``` @@ -263,13 +263,13 @@ In Weighted Progress mode, both the manual progress input and weight assignment ParentProgress = 40% ``` - In this case, only Subtask A influences the parent task progress because Subtask B has a weight of 0. + In this case, only Subtask A influences the parent task progress because Subtask B has a weight of 0%. 5. **Default Weight Behavior**: When weights aren't explicitly assigned to some tasks: - - Subtask A: Progress 30%, Weight 60 (explicitly set) - - Subtask B: Progress 70%, Weight not set (defaults to 100) - - Subtask C: Progress 90%, Weight not set (defaults to 100) + - Subtask A: Progress 30%, Weight 60% (explicitly set) + - Subtask B: Progress 70%, Weight not set (defaults to 100%) + - Subtask C: Progress 90%, Weight not set (defaults to 100%) Calculation: ``` diff --git a/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql b/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql index 91f6f639..8898e599 100644 --- a/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql +++ b/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql @@ -39,8 +39,14 @@ BEGIN INTO _use_manual_progress, _use_weighted_progress, _use_time_progress; END IF; - -- If manual progress is enabled and has a value, use it directly - IF _is_manual IS TRUE AND _manual_value IS NOT NULL THEN + -- Get all subtasks + SELECT COUNT(*) + FROM tasks + WHERE parent_task_id = _task_id AND archived IS FALSE + INTO _sub_tasks_count; + + -- If manual progress is enabled and has a value AND there are no subtasks, use it directly + IF _is_manual IS TRUE AND _manual_value IS NOT NULL AND _sub_tasks_count = 0 THEN RETURN JSON_BUILD_OBJECT( 'ratio', _manual_value, 'total_completed', 0, @@ -49,12 +55,6 @@ BEGIN ); END IF; - -- Get all subtasks - SELECT COUNT(*) - FROM tasks - WHERE parent_task_id = _task_id AND archived IS FALSE - INTO _sub_tasks_count; - -- If there are no subtasks, just use the parent task's status IF _sub_tasks_count = 0 THEN SELECT (CASE @@ -145,7 +145,7 @@ BEGIN ELSE 0 END END AS progress_value, - COALESCE(total_hours * 60 + total_minutes, 0) AS estimated_minutes + COALESCE(total_minutes, 0) AS estimated_minutes FROM tasks t WHERE t.parent_task_id = _task_id AND t.archived IS FALSE @@ -657,4 +657,26 @@ ADD COLUMN IF NOT EXISTS use_manual_progress BOOLEAN DEFAULT FALSE, ADD COLUMN IF NOT EXISTS use_weighted_progress BOOLEAN DEFAULT FALSE, ADD COLUMN IF NOT EXISTS use_time_progress BOOLEAN DEFAULT FALSE; +-- Add a trigger to reset manual progress when a task gets a new subtask +CREATE OR REPLACE FUNCTION reset_parent_task_manual_progress() RETURNS TRIGGER AS +$$ +BEGIN + -- When a task gets a new subtask (parent_task_id is set), reset the parent's manual_progress flag + IF NEW.parent_task_id IS NOT NULL THEN + UPDATE tasks + SET manual_progress = false + WHERE id = NEW.parent_task_id + AND manual_progress = true; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger on the tasks table +DROP TRIGGER IF EXISTS reset_parent_manual_progress_trigger ON tasks; +CREATE TRIGGER reset_parent_manual_progress_trigger +AFTER INSERT OR UPDATE OF parent_task_id ON tasks +FOR EACH ROW +EXECUTE FUNCTION reset_parent_task_manual_progress(); + COMMIT; \ No newline at end of file diff --git a/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql b/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql new file mode 100644 index 00000000..b817ddd7 --- /dev/null +++ b/worklenz-backend/database/migrations/20250425000000-update-time-based-progress.sql @@ -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; \ No newline at end of file diff --git a/worklenz-backend/database/migrations/20250426000000-improve-parent-task-progress-calculation.sql b/worklenz-backend/database/migrations/20250426000000-improve-parent-task-progress-calculation.sql new file mode 100644 index 00000000..7ef0015c --- /dev/null +++ b/worklenz-backend/database/migrations/20250426000000-improve-parent-task-progress-calculation.sql @@ -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; \ 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 e3992563..286157cb 100644 --- a/worklenz-backend/src/controllers/tasks-controller-v2.ts +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -97,8 +97,11 @@ export default class TasksControllerV2 extends TasksControllerBase { try { const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]); const [data] = result.rows; - data.info.ratio = +data.info.ratio.toFixed(); - return data.info; + if (data && data.info && data.info.ratio !== undefined) { + data.info.ratio = +((data.info.ratio || 0).toFixed()); + return data.info; + } + return null; } catch (error) { return null; } @@ -198,7 +201,7 @@ export default class TasksControllerV2 extends TasksControllerBase { (SELECT use_manual_progress FROM projects WHERE id = t.project_id) AS project_use_manual_progress, (SELECT use_weighted_progress FROM projects WHERE id = t.project_id) AS project_use_weighted_progress, (SELECT use_time_progress FROM projects WHERE id = t.project_id) AS project_use_time_progress, - (SELECT (get_task_complete_ratio(t.id)).ratio) AS complete_ratio, + (SELECT get_task_complete_ratio(t.id)->>'ratio') AS complete_ratio, (SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id, (SELECT name @@ -372,7 +375,7 @@ export default class TasksControllerV2 extends TasksControllerBase { const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]); const [data] = result.rows; if (data && data.info) { - task.complete_ratio = +data.info.ratio.toFixed(); + task.complete_ratio = +(data.info.ratio || 0).toFixed(); task.completed_count = data.info.total_completed; task.total_tasks_count = data.info.total_tasks; } @@ -441,7 +444,7 @@ export default class TasksControllerV2 extends TasksControllerBase { const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [task.id]); const [ratioData] = result.rows; if (ratioData && ratioData.info) { - task.complete_ratio = +ratioData.info.ratio.toFixed(); + task.complete_ratio = +(ratioData.info.ratio || 0).toFixed(); task.completed_count = ratioData.info.total_completed; task.total_tasks_count = ratioData.info.total_tasks; console.log(`Updated task ${task.id} (${task.name}) from DB: complete_ratio=${task.complete_ratio}`); @@ -483,6 +486,53 @@ export default class TasksControllerV2 extends TasksControllerBase { return res.status(200).send(new ServerResponse(true, task)); } + @HandleExceptions() + public static async resetParentTaskManualProgress(parentTaskId: string): Promise { + try { + // Check if this task has subtasks + const subTasksResult = await db.query( + "SELECT COUNT(*) as subtask_count FROM tasks WHERE parent_task_id = $1 AND archived IS FALSE", + [parentTaskId] + ); + + const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || "0"); + + // If it has subtasks, reset the manual_progress flag to false + if (subtaskCount > 0) { + await db.query( + "UPDATE tasks SET manual_progress = false WHERE id = $1", + [parentTaskId] + ); + console.log(`Reset manual progress for parent task ${parentTaskId} with ${subtaskCount} subtasks`); + + // Get the project settings to determine which calculation method to use + const projectResult = await db.query( + "SELECT project_id FROM tasks WHERE id = $1", + [parentTaskId] + ); + + const projectId = projectResult.rows[0]?.project_id; + + if (projectId) { + // Recalculate the parent task's progress based on its subtasks + const progressResult = await db.query( + "SELECT get_task_complete_ratio($1) AS ratio", + [parentTaskId] + ); + + const progressRatio = progressResult.rows[0]?.ratio?.ratio || 0; + + // Emit the updated progress value to all clients + // Note: We don't have socket context here, so we can't directly emit + // This will be picked up on the next client refresh + console.log(`Recalculated progress for parent task ${parentTaskId}: ${progressRatio}%`); + } + } + } catch (error) { + log_error(`Error resetting parent task manual progress: ${error}`); + } + } + @HandleExceptions() public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { @@ -522,6 +572,11 @@ export default class TasksControllerV2 extends TasksControllerBase { ? [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]; await db.query(q, params); + + // Reset the parent task's manual progress when converting a task to a subtask + if (req.body.parent_task_id) { + await this.resetParentTaskManualProgress(req.body.parent_task_id); + } const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]); const [data] = result.rows; diff --git a/worklenz-backend/src/socket.io/commands/on-get-task-subtasks-count.ts b/worklenz-backend/src/socket.io/commands/on-get-task-subtasks-count.ts new file mode 100644 index 00000000..ce20d5d1 --- /dev/null +++ b/worklenz-backend/src/socket.io/commands/on-get-task-subtasks-count.ts @@ -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}`); + } +} \ No newline at end of file diff --git a/worklenz-backend/src/socket.io/commands/on-time-estimation-change.ts b/worklenz-backend/src/socket.io/commands/on-time-estimation-change.ts index d6c5e606..1860260e 100644 --- a/worklenz-backend/src/socket.io/commands/on-time-estimation-change.ts +++ b/worklenz-backend/src/socket.io/commands/on-time-estimation-change.ts @@ -6,10 +6,56 @@ import { SocketEvents } from "../events"; import { log_error, notifyProjectUpdates } from "../util"; import { getTaskDetails, logTotalMinutes } from "../../services/activity-logs/activity-logs.service"; -export async function on_time_estimation_change(_io: Server, socket: Socket, data?: string) { +/** + * Recursively updates all ancestor tasks' progress when a subtask changes + * @param io Socket.io instance + * @param socket Socket instance for emitting events + * @param projectId Project ID for room broadcasting + * @param taskId The task ID to update (starts with the parent task) + */ +async function updateTaskAncestors(io: any, socket: Socket, projectId: string, taskId: string | null) { + if (!taskId) return; + + try { + // Get the current task's progress ratio + const progressRatio = await db.query( + "SELECT get_task_complete_ratio($1) as ratio", + [taskId] + ); + + const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0; + console.log(`Updated task ${taskId} progress after time estimation change: ${ratio}`); + + // Emit the updated progress + socket.emit( + SocketEvents.TASK_PROGRESS_UPDATED.toString(), + { + task_id: taskId, + progress_value: ratio + } + ); + + // Find this task's parent to continue the recursive update + const parentResult = await db.query( + "SELECT parent_task_id FROM tasks WHERE id = $1", + [taskId] + ); + + const parentTaskId = parentResult.rows[0]?.parent_task_id; + + // If there's a parent, recursively update it + if (parentTaskId) { + await updateTaskAncestors(io, socket, projectId, parentTaskId); + } + } catch (error) { + log_error(`Error updating ancestor task ${taskId}: ${error}`); + } +} + +export async function on_time_estimation_change(io: Server, socket: Socket, data?: string) { try { // (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent, - const q = `UPDATE tasks SET total_minutes = $2 WHERE id = $1 RETURNING total_minutes;`; + const q = `UPDATE tasks SET total_minutes = $2 WHERE id = $1 RETURNING total_minutes, project_id, parent_task_id;`; const body = JSON.parse(data as string); const hours = body.total_hours || 0; @@ -19,7 +65,10 @@ export async function on_time_estimation_change(_io: Server, socket: Socket, dat const task_data = await getTaskDetails(body.task_id, "total_minutes"); const result0 = await db.query(q, [body.task_id, totalMinutes]); - const [data0] = result0.rows; + const [taskData] = result0.rows; + + const projectId = taskData.project_id; + const parentTaskId = taskData.parent_task_id; const result = await db.query("SELECT SUM(time_spent) AS total_minutes_spent FROM task_work_log WHERE task_id = $1;", [body.task_id]); const [dd] = result.rows; @@ -31,6 +80,22 @@ export async function on_time_estimation_change(_io: Server, socket: Socket, dat total_minutes_spent: dd.total_minutes_spent || 0 }; socket.emit(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), TasksController.updateTaskViewModel(d)); + + // If this is a subtask in time-based mode, update parent task progress + if (parentTaskId) { + const projectSettingsResult = await db.query( + "SELECT use_time_progress FROM projects WHERE id = $1", + [projectId] + ); + + const useTimeProgress = projectSettingsResult.rows[0]?.use_time_progress; + + if (useTimeProgress) { + // Recalculate parent task progress when subtask time estimation changes + await updateTaskAncestors(io, socket, projectId, parentTaskId); + } + } + notifyProjectUpdates(socket, d.id); logTotalMinutes({ 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 ac8ebbdb..cac1cb43 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 @@ -10,6 +10,52 @@ interface UpdateTaskProgressData { parent_task_id: string | null; } +/** + * Recursively updates all ancestor tasks' progress when a subtask changes + * @param io Socket.io instance + * @param socket Socket instance for emitting events + * @param projectId Project ID for room broadcasting + * @param taskId The task ID to update (starts with the parent task) + */ +async function updateTaskAncestors(io: any, socket: Socket, projectId: string, taskId: string | null) { + if (!taskId) return; + + try { + // Get the current task's progress ratio + const progressRatio = await db.query( + "SELECT get_task_complete_ratio($1) as ratio", + [taskId] + ); + + const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0; + console.log(`Updated task ${taskId} progress: ${ratio}`); + + // Emit the updated progress + socket.emit( + SocketEvents.TASK_PROGRESS_UPDATED.toString(), + { + task_id: taskId, + progress_value: ratio + } + ); + + // Find this task's parent to continue the recursive update + const parentResult = await db.query( + "SELECT parent_task_id FROM tasks WHERE id = $1", + [taskId] + ); + + const parentTaskId = parentResult.rows[0]?.parent_task_id; + + // If there's a parent, recursively update it + if (parentTaskId) { + await updateTaskAncestors(io, socket, projectId, parentTaskId); + } + } catch (error) { + log_error(`Error updating ancestor task ${taskId}: ${error}`); + } +} + export async function on_update_task_progress(io: any, socket: Socket, data: string) { try { const parsedData = JSON.parse(data) as UpdateTaskProgressData; @@ -25,7 +71,7 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str [task_id] ); - const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || '0'); + const subtaskCount = parseInt(subTasksResult.rows[0]?.subtask_count || "0"); // If this is a parent task, we shouldn't set manual progress if (subtaskCount > 0) { @@ -53,14 +99,13 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str // Log the progress change using the activity logs service await logProgressChange({ task_id, - old_value: currentProgress !== null ? currentProgress.toString() : '0', + old_value: currentProgress !== null ? currentProgress.toString() : "0", new_value: progress_value.toString(), socket }); - if (projectId) { // Emit the update to all clients in the project room - io.to(projectId).emit( + socket.emit( SocketEvents.TASK_PROGRESS_UPDATED.toString(), { task_id, @@ -68,10 +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); // Recursively update all ancestors in the task hierarchy - await updateTaskAncestors(io, projectId, parent_task_id); + await updateTaskAncestors(io, socket, projectId, parent_task_id); // Notify that project updates are available notifyProjectUpdates(socket, task_id); @@ -80,48 +125,3 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str log_error(error); } } - -/** - * Recursively updates all ancestor tasks' progress when a subtask changes - * @param io Socket.io instance - * @param projectId Project ID for room broadcasting - * @param taskId The task ID to update (starts with the parent task) - */ -async function updateTaskAncestors(io: any, projectId: string, taskId: string | null) { - if (!taskId) return; - - try { - // Get the current task's progress ratio - const progressRatio = await db.query( - "SELECT get_task_complete_ratio($1) as ratio", - [taskId] - ); - - const ratio = progressRatio?.rows[0]?.ratio; - console.log(`Updated task ${taskId} progress: ${ratio}`); - - // Emit the updated progress - io.to(projectId).emit( - SocketEvents.TASK_PROGRESS_UPDATED.toString(), - { - task_id: taskId, - progress_value: ratio - } - ); - - // Find this task's parent to continue the recursive update - const parentResult = await db.query( - "SELECT parent_task_id FROM tasks WHERE id = $1", - [taskId] - ); - - const parentTaskId = parentResult.rows[0]?.parent_task_id; - - // If there's a parent, recursively update it - if (parentTaskId) { - await updateTaskAncestors(io, projectId, parentTaskId); - } - } catch (error) { - log_error(`Error updating ancestor task ${taskId}: ${error}`); - } -} \ No newline at end of file diff --git a/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts b/worklenz-backend/src/socket.io/commands/on-update-task-weight.ts index e6a68d1d..664d0806 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 @@ -40,14 +40,14 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin // Log the weight change using the activity logs service await logWeightChange({ task_id, - old_value: currentWeight !== null ? currentWeight.toString() : '100', + old_value: currentWeight !== null ? currentWeight.toString() : "100", new_value: weight.toString(), socket }); if (projectId) { // Emit the update to all clients in the project room - io.to(projectId).emit( + socket.emit( SocketEvents.TASK_PROGRESS_UPDATED.toString(), { task_id, @@ -63,11 +63,11 @@ export async function on_update_task_weight(io: any, socket: Socket, data: strin ); // Emit the parent task's updated progress - io.to(projectId).emit( + socket.emit( SocketEvents.TASK_PROGRESS_UPDATED.toString(), { task_id: parent_task_id, - progress_value: progressRatio?.rows[0]?.ratio + progress_value: progressRatio?.rows[0]?.ratio?.ratio || 0 } ); } diff --git a/worklenz-backend/src/socket.io/events.ts b/worklenz-backend/src/socket.io/events.ts index c59b0eff..a8e19a83 100644 --- a/worklenz-backend/src/socket.io/events.ts +++ b/worklenz-backend/src/socket.io/events.ts @@ -63,4 +63,8 @@ export enum SocketEvents { UPDATE_TASK_PROGRESS, UPDATE_TASK_WEIGHT, TASK_PROGRESS_UPDATED, + + // Task subtasks count events + GET_TASK_SUBTASKS_COUNT, + TASK_SUBTASKS_COUNT, } diff --git a/worklenz-backend/src/socket.io/index.ts b/worklenz-backend/src/socket.io/index.ts index b77a68ea..3c5e50b5 100644 --- a/worklenz-backend/src/socket.io/index.ts +++ b/worklenz-backend/src/socket.io/index.ts @@ -54,6 +54,7 @@ import { on_task_custom_column_update } from "./commands/on_custom_column_update import { on_custom_column_pinned_change } from "./commands/on_custom_column_pinned_change"; import { on_update_task_progress } from "./commands/on-update-task-progress"; import { on_update_task_weight } from "./commands/on-update-task-weight"; +import { on_get_task_subtasks_count } from "./commands/on-get-task-subtasks-count"; export function register(io: any, socket: Socket) { log(socket.id, "client registered"); @@ -110,6 +111,7 @@ export function register(io: any, socket: Socket) { socket.on(SocketEvents.CUSTOM_COLUMN_PINNED_CHANGE.toString(), data => on_custom_column_pinned_change(io, socket, data)); socket.on(SocketEvents.UPDATE_TASK_PROGRESS.toString(), data => on_update_task_progress(io, socket, data)); socket.on(SocketEvents.UPDATE_TASK_WEIGHT.toString(), data => on_update_task_weight(io, socket, data)); + socket.on(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), (taskId) => on_get_task_subtasks_count(io, socket, taskId)); // socket.io built-in event socket.on("disconnect", (reason) => on_disconnect(io, socket, reason)); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx index 07f51adc..ebb1e694 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx @@ -5,7 +5,7 @@ import { useAppSelector } from '@/hooks/useAppSelector'; import { ITaskViewModel } from '@/types/tasks/task.types'; import Flex from 'antd/lib/flex'; import { SocketEvents } from '@/shared/socket-events'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useSocket } from '@/socket/socketContext'; interface TaskDrawerProgressProps { @@ -17,16 +17,48 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { const { t } = useTranslation('task-drawer/task-drawer'); const { project } = useAppSelector(state => state.projectReducer); const { socket, connected } = useSocket(); + const [confirmedHasSubtasks, setConfirmedHasSubtasks] = useState(null); const isSubTask = !!task?.parent_task_id; - const hasSubTasks = task?.sub_tasks_count > 0; + const hasSubTasks = task?.sub_tasks_count > 0 || confirmedHasSubtasks === true; - // Show manual progress input only for tasks without subtasks (not parent tasks) - // Parent tasks get their progress calculated from subtasks + // Additional debug logging + console.log(`TaskDrawerProgress for task ${task.id} (${task.name}): hasSubTasks=${hasSubTasks}, count=${task.sub_tasks_count}, confirmedHasSubtasks=${confirmedHasSubtasks}`); + + // HIGHEST PRIORITY CHECK: Never show progress inputs for parent tasks with subtasks + // This check happens before any other logic to ensure consistency + if (hasSubTasks) { + console.error(`REJECTED: Progress input for parent task ${task.id} with ${task.sub_tasks_count} subtasks. confirmedHasSubtasks=${confirmedHasSubtasks}`); + return null; + } + + // Double-check by directly querying for subtasks from the server + useEffect(() => { + if (connected && task.id) { + socket?.emit(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), task.id); + } + + // Listen for the subtask count response + const handleSubtasksCount = (data: any) => { + if (data.task_id === task.id) { + console.log(`Received subtask count for task ${task.id}: ${data.subtask_count}, has_subtasks=${data.has_subtasks}`); + setConfirmedHasSubtasks(data.has_subtasks); + } + }; + + socket?.on(SocketEvents.TASK_SUBTASKS_COUNT.toString(), handleSubtasksCount); + + return () => { + socket?.off(SocketEvents.TASK_SUBTASKS_COUNT.toString(), handleSubtasksCount); + }; + }, [socket, connected, task.id]); + + // Never show manual progress input for parent tasks (tasks with subtasks) + // Only show progress input for tasks without subtasks const showManualProgressInput = !hasSubTasks; // Only show weight input for subtasks in weighted progress mode - const showTaskWeightInput = project?.use_weighted_progress && isSubTask; + const showTaskWeightInput = project?.use_weighted_progress && isSubTask && !hasSubTasks; useEffect(() => { // Listen for progress updates from the server @@ -53,8 +85,13 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { }; }, [socket, connected, task.id, form]); + // One last check before rendering + if (hasSubTasks) { + return null; + } + const handleProgressChange = (value: number | null) => { - if (connected && task.id && value !== null) { + if (connected && task.id && value !== null && !hasSubTasks) { // Ensure parent_task_id is not undefined const parent_task_id = task.parent_task_id || null; @@ -67,13 +104,6 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { }) ); - // If this task has subtasks, request recalculation of its progress - if (hasSubTasks) { - setTimeout(() => { - socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id); - }, 100); - } - // If this is a subtask, request the parent's progress to be updated in UI if (parent_task_id) { setTimeout(() => { @@ -84,7 +114,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { }; const handleWeightChange = (value: number | null) => { - if (connected && task.id && value !== null) { + if (connected && task.id && value !== null && !hasSubTasks) { // Ensure parent_task_id is not undefined const parent_task_id = task.parent_task_id || null; @@ -116,6 +146,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { return null; // Don't show any progress inputs if not applicable } + // Final safety check + if (hasSubTasks) { + return null; + } + return ( <> {showTaskWeightInput && ( diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx index abdb386d..fc5d66d4 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx @@ -33,6 +33,42 @@ interface TaskDetailsFormProps { taskFormViewModel?: ITaskFormViewModel | null; } +// Custom wrapper that enforces stricter rules for displaying progress input +interface ConditionalProgressInputProps { + task: ITaskViewModel; + form: any; // Using any for the form as the exact type may be complex +} + +const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps) => { + const { project } = useAppSelector(state => state.projectReducer); + const hasSubTasks = task?.sub_tasks_count > 0; + const isSubTask = !!task?.parent_task_id; + + // Add more aggressive logging and checks + console.log(`Task ${task.id} status: hasSubTasks=${hasSubTasks}, isSubTask=${isSubTask}, modes: time=${project?.use_time_progress}, manual=${project?.use_manual_progress}, weighted=${project?.use_weighted_progress}`); + + // STRICT RULE: Never show progress input for parent tasks with subtasks + // This is the most important check and must be done first + if (hasSubTasks) { + console.log(`Task ${task.id} has ${task.sub_tasks_count} subtasks. Hiding progress input.`); + return null; + } + + // Only for tasks without subtasks, determine which input to show based on project mode + if (project?.use_time_progress) { + // In time-based mode, show progress input ONLY for tasks without subtasks + return ; + } else if (project?.use_manual_progress) { + // In manual mode, show progress input ONLY for tasks without subtasks + return ; + } else if (project?.use_weighted_progress && isSubTask) { + // In weighted mode, show weight input for subtasks + return ; + } + + return null; +}; + const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => { const { t } = useTranslation('task-drawer/task-drawer'); const [form] = Form.useForm(); @@ -121,8 +157,11 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => - {(project?.use_manual_progress || project?.use_weighted_progress) && (taskFormViewModel?.task) && ( - + {taskFormViewModel?.task && ( + )} diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx index dbf0f242..ecf3f847 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx @@ -54,7 +54,7 @@ const ProjectViewTaskList = () => { - {(taskGroups.length === 0 && !loadingGroups) ? ( + {(taskGroups && taskGroups.length === 0 && !loadingGroups) ? ( ) : ( diff --git a/worklenz-frontend/src/shared/socket-events.ts b/worklenz-frontend/src/shared/socket-events.ts index f1b71d2d..33bcc0e8 100644 --- a/worklenz-frontend/src/shared/socket-events.ts +++ b/worklenz-frontend/src/shared/socket-events.ts @@ -63,4 +63,8 @@ export enum SocketEvents { UPDATE_TASK_PROGRESS, UPDATE_TASK_WEIGHT, TASK_PROGRESS_UPDATED, + + // Task subtasks count events + GET_TASK_SUBTASKS_COUNT, + TASK_SUBTASKS_COUNT, }