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:
@@ -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<void> {
|
||||
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<IWorkLenzResponse> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user