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.
This commit is contained in:
chamikaJ
2025-05-02 13:21:32 +05:30
parent 8f913b0f4e
commit a5b881c609
16 changed files with 870 additions and 112 deletions

View File

@@ -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}`);
}
}

View File

@@ -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({

View File

@@ -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}`);
}
}

View File

@@ -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
}
);
}

View File

@@ -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,
}

View File

@@ -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));