Implement task completion prompt and enhance progress handling

- Added logic to prompt users to mark tasks as done when progress reaches 100%, integrating with the socket events for real-time updates.
- Updated backend functions to check task statuses and determine if a prompt is necessary based on the task's current state.
- Enhanced frontend components to display a modal for confirming task completion, improving user experience and clarity in task management.
- Refactored socket event handling to include new events for retrieving "done" statuses, ensuring accurate task status updates across the application.
This commit is contained in:
chamikaJ
2025-05-02 17:05:16 +05:30
parent a5b881c609
commit a368b979d5
9 changed files with 239 additions and 34 deletions

View File

@@ -76,6 +76,25 @@ The parent task will be approximately 39% complete, with Subtask C having the gr
- Only explicitly set weights for tasks that should have different importance
- Weights are only relevant for subtasks, not for independent tasks
### Detailed Weighted Progress Calculation Example
To understand how weighted progress works with different weight values, consider this example:
For a parent task with two subtasks:
- Subtask A: 80% complete, Weight 50%
- Subtask B: 40% complete, Weight 100%
The calculation works as follows:
1. Each subtask's contribution is: (weight × progress) ÷ (sum of all weights)
2. For Subtask A: (50 × 80%) ÷ (50 + 100) = 26.7%
3. For Subtask B: (100 × 40%) ÷ (50 + 100) = 26.7%
4. Total parent progress: 26.7% + 26.7% = 53.3%
The parent task would be approximately 53% complete.
This shows how the subtask with twice the weight (Subtask B) has twice the influence on the overall progress calculation, even though it has a lower completion percentage.
## Time-based Progress Method
### How It Works
@@ -108,6 +127,29 @@ The parent task will be approximately 29% complete, with the lengthy Subtask C p
- Setting a time estimate to 0 removes that task from progress calculations
- Time estimates serve dual purposes: scheduling/resource planning and progress weighting
### Detailed Time-based Progress Calculation Example
To understand how time-based progress works with different time estimates, consider this example:
For a parent task with three subtasks:
- Subtask A: 40% complete, Estimated Time 2.5 hours
- Subtask B: 80% complete, Estimated Time 1 hour
- Subtask C: 10% complete, Estimated Time 4 hours
The calculation works as follows:
1. Convert hours to minutes: A = 150 min, B = 60 min, C = 240 min
2. Total estimated time: 150 + 60 + 240 = 450 minutes
3. Each subtask's contribution is: (time estimate × progress) ÷ (total time)
4. For Subtask A: (150 × 40%) ÷ 450 = 13.3%
5. For Subtask B: (60 × 80%) ÷ 450 = 10.7%
6. For Subtask C: (240 × 10%) ÷ 450 = 5.3%
7. Total parent progress: 13.3% + 10.7% + 5.3% = 29.3%
The parent task would be approximately 29% complete.
This demonstrates how tasks with longer time estimates (like Subtask C) have more influence on the overall progress calculation. Even though Subtask B is 80% complete, its shorter time estimate means it contributes less to the overall progress than the partially-completed but longer Subtask A.
## Default Progress Method
If none of the special progress methods are enabled, WorkLenz uses a simple completion-based approach:

View File

@@ -221,6 +221,8 @@ 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
@@ -232,6 +234,21 @@ BEGIN
WHERE id = _parent_task_id;
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' INTO _ratio;
IF _ratio::FLOAT >= 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;

View File

@@ -0,0 +1,49 @@
import { Socket } from "socket.io";
import db from "../../config/db";
import { log_error } from "../util";
// Define a type for the callback function
type DoneStatusesCallback = (statuses: Array<{
id: string;
name: string;
sort_order: number;
color_code: string;
}>) => void;
/**
* Socket handler to get task statuses in the "done" category for a project
* Used when prompting users to mark a task as done when progress reaches 100%
*/
export async function on_get_done_statuses(
io: any,
socket: Socket,
projectId: string,
callback: DoneStatusesCallback
) {
try {
if (!projectId) {
return callback([]);
}
// Query to get all statuses in the "done" category for the project
const result = await db.query(`
SELECT ts.id, ts.name, ts.sort_order, ts.color_code
FROM task_statuses ts
INNER JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
WHERE ts.project_id = $1
AND stsc.is_done = TRUE
ORDER BY ts.sort_order ASC
`, [projectId]);
const doneStatuses = result.rows;
console.log(`Found ${doneStatuses.length} "done" statuses for project ${projectId}`);
// Use callback to return the result
callback(doneStatuses);
} catch (error) {
log_error(`Error getting "done" statuses for project ${projectId}: ${error}`);
callback([]);
}
}

View File

@@ -26,12 +26,32 @@ async function updateTaskAncestors(io: any, socket: Socket, projectId: string, t
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
console.log(`Updated task ${taskId} progress after time estimation change: ${ratio}`);
// Check if this task needs a "done" status prompt
let shouldPromptForDone = false;
if (ratio >= 100) {
// Get the task's current status
const taskStatusResult = await db.query(`
SELECT ts.id, stsc.is_done
FROM tasks t
JOIN task_statuses ts ON t.status_id = ts.id
JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
WHERE t.id = $1
`, [taskId]);
// If the task isn't already in a "done" category, we should prompt the user
if (taskStatusResult.rows.length > 0 && !taskStatusResult.rows[0].is_done) {
shouldPromptForDone = true;
}
}
// Emit the updated progress
socket.emit(
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
{
task_id: taskId,
progress_value: ratio
progress_value: ratio,
should_prompt_for_done: shouldPromptForDone
}
);

View File

@@ -30,12 +30,32 @@ async function updateTaskAncestors(io: any, socket: Socket, projectId: string, t
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
console.log(`Updated task ${taskId} progress: ${ratio}`);
// Check if this task needs a "done" status prompt
let shouldPromptForDone = false;
if (ratio >= 100) {
// Get the task's current status
const taskStatusResult = await db.query(`
SELECT ts.id, stsc.is_done
FROM tasks t
JOIN task_statuses ts ON t.status_id = ts.id
JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
WHERE t.id = $1
`, [taskId]);
// If the task isn't already in a "done" category, we should prompt the user
if (taskStatusResult.rows.length > 0 && !taskStatusResult.rows[0].is_done) {
shouldPromptForDone = true;
}
}
// Emit the updated progress
socket.emit(
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
{
task_id: taskId,
progress_value: ratio
progress_value: ratio,
should_prompt_for_done: shouldPromptForDone
}
);
@@ -81,12 +101,13 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
// Get the current progress value to log the change
const currentProgressResult = await db.query(
"SELECT progress_value, project_id FROM tasks WHERE id = $1",
"SELECT progress_value, project_id, status_id FROM tasks WHERE id = $1",
[task_id]
);
const currentProgress = currentProgressResult.rows[0]?.progress_value;
const projectId = currentProgressResult.rows[0]?.project_id;
const statusId = currentProgressResult.rows[0]?.status_id;
// Update the task progress in the database
await db.query(
@@ -103,13 +124,33 @@ export async function on_update_task_progress(io: any, socket: Socket, data: str
new_value: progress_value.toString(),
socket
});
if (projectId) {
// Check if progress is 100% and the task isn't already in a "done" status category
let shouldPromptForDone = false;
if (progress_value >= 100) {
// Check if the task's current status is in a "done" category
const statusCategoryResult = await db.query(`
SELECT stsc.is_done
FROM task_statuses ts
JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
WHERE ts.id = $1
`, [statusId]);
// If the task isn't already in a "done" category, we should prompt the user
if (statusCategoryResult.rows.length > 0 && !statusCategoryResult.rows[0].is_done) {
shouldPromptForDone = true;
}
}
// Emit the update to all clients in the project room
socket.emit(
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
{
task_id,
progress_value
progress_value,
should_prompt_for_done: shouldPromptForDone
}
);

View File

@@ -67,4 +67,7 @@ export enum SocketEvents {
// Task subtasks count events
GET_TASK_SUBTASKS_COUNT,
TASK_SUBTASKS_COUNT,
// Task completion events
GET_DONE_STATUSES,
}

View File

@@ -55,6 +55,7 @@ import { on_custom_column_pinned_change } from "./commands/on_custom_column_pinn
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";
import { on_get_done_statuses } from "./commands/on-get-done-statuses";
export function register(io: any, socket: Socket) {
log(socket.id, "client registered");
@@ -72,7 +73,6 @@ export function register(io: any, socket: Socket) {
socket.on(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), data => on_time_estimation_change(io, socket, data));
socket.on(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), data => on_task_description_change(io, socket, data));
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), data => on_get_task_progress(io, socket, data));
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), data => on_get_task_progress(io, socket, data));
socket.on(SocketEvents.TASK_TIMER_START.toString(), data => on_task_timer_start(io, socket, data));
socket.on(SocketEvents.TASK_TIMER_STOP.toString(), data => on_task_timer_stop(io, socket, data));
socket.on(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), data => on_task_sort_order_change(io, socket, data));
@@ -112,6 +112,7 @@ export function register(io: any, socket: Socket) {
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.on(SocketEvents.GET_DONE_STATUSES.toString(), (projectId, callback) => on_get_done_statuses(io, socket, projectId, callback));
// socket.io built-in event
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));

View File

@@ -1,11 +1,11 @@
import { Form, InputNumber, Tooltip } from 'antd';
import { Form, InputNumber, Tooltip, Modal } from 'antd';
import { useTranslation } from 'react-i18next';
import { QuestionCircleOutlined } from '@ant-design/icons';
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, useState } from 'react';
import { useState, useEffect } from 'react';
import { useSocket } from '@/socket/socketContext';
interface TaskDrawerProgressProps {
@@ -17,42 +17,20 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
const { t } = useTranslation('task-drawer/task-drawer');
const { project } = useAppSelector(state => state.projectReducer);
const { socket, connected } = useSocket();
const [confirmedHasSubtasks, setConfirmedHasSubtasks] = useState<boolean | null>(null);
const [isCompletionModalVisible, setIsCompletionModalVisible] = useState(false);
const isSubTask = !!task?.parent_task_id;
const hasSubTasks = task?.sub_tasks_count > 0 || confirmedHasSubtasks === true;
// Safe handling of sub_tasks_count which might be undefined in some cases
const hasSubTasks = (task?.sub_tasks_count || 0) > 0;
// Additional debug logging
console.log(`TaskDrawerProgress for task ${task.id} (${task.name}): hasSubTasks=${hasSubTasks}, count=${task.sub_tasks_count}, confirmedHasSubtasks=${confirmedHasSubtasks}`);
// Log task details for debugging
console.log(`TaskDrawerProgress: task=${task?.id}, sub_tasks_count=${task?.sub_tasks_count}, hasSubTasks=${hasSubTasks}`);
// 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;
@@ -70,6 +48,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
if (data.weight !== undefined) {
form.setFieldsValue({ weight: data.weight });
}
// Check if we should prompt the user to mark the task as done
if (data.should_prompt_for_done) {
setIsCompletionModalVisible(true);
}
}
};
@@ -92,6 +75,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
const handleProgressChange = (value: number | null) => {
if (connected && task.id && value !== null && !hasSubTasks) {
// Check if progress is set to 100% to show completion confirmation
if (value === 100) {
setIsCompletionModalVisible(true);
}
// Ensure parent_task_id is not undefined
const parent_task_id = task.parent_task_id || null;
@@ -136,6 +124,36 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
}
};
const handleMarkTaskAsComplete = () => {
// Close the modal
setIsCompletionModalVisible(false);
// Find a "Done" status for this project
if (connected && task.id) {
// Emit socket event to get "done" category statuses
socket?.emit(SocketEvents.GET_DONE_STATUSES.toString(), task.project_id, (doneStatuses: any[]) => {
if (doneStatuses && doneStatuses.length > 0) {
// Use the first "done" status
const doneStatusId = doneStatuses[0].id;
// Emit socket event to update the task status
socket?.emit(
SocketEvents.TASK_STATUS_CHANGE.toString(),
JSON.stringify({
task_id: task.id,
status_id: doneStatusId,
project_id: task.project_id
})
);
console.log(`Task ${task.id} marked as done with status ${doneStatusId}`);
} else {
console.error(`No "done" statuses found for project ${task.project_id}`);
}
});
}
};
const percentFormatter = (value: number | undefined) => (value ? `${value}%` : '0%');
const percentParser = (value: string | undefined) => {
const parsed = parseInt(value?.replace('%', '') || '0', 10);
@@ -217,6 +235,17 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
/>
</Form.Item>
)}
<Modal
title="Mark Task as Done?"
open={isCompletionModalVisible}
onOk={handleMarkTaskAsComplete}
onCancel={() => setIsCompletionModalVisible(false)}
okText="Yes, mark as done"
cancelText="No, keep current status"
>
<p>You've set the progress to 100%. Would you like to update the task status to "Done"?</p>
</Modal>
</>
);
};

View File

@@ -67,4 +67,7 @@ export enum SocketEvents {
// Task subtasks count events
GET_TASK_SUBTASKS_COUNT,
TASK_SUBTASKS_COUNT,
// Task completion events
GET_DONE_STATUSES,
}