Merge pull request #106 from chamikaJ/fix/custom-progress-methods
Fix/custom progress methods
This commit is contained in:
@@ -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
|
- Only explicitly set weights for tasks that should have different importance
|
||||||
- Weights are only relevant for subtasks, not for independent tasks
|
- 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
|
## Time-based Progress Method
|
||||||
|
|
||||||
### How It Works
|
### 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
|
- Setting a time estimate to 0 removes that task from progress calculations
|
||||||
- Time estimates serve dual purposes: scheduling/resource planning and progress weighting
|
- 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
|
## Default Progress Method
|
||||||
|
|
||||||
If none of the special progress methods are enabled, WorkLenz uses a simple completion-based approach:
|
If none of the special progress methods are enabled, WorkLenz uses a simple completion-based approach:
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ CREATE OR REPLACE FUNCTION update_parent_task_progress() RETURNS TRIGGER AS
|
|||||||
$$
|
$$
|
||||||
DECLARE
|
DECLARE
|
||||||
_parent_task_id UUID;
|
_parent_task_id UUID;
|
||||||
|
_project_id UUID;
|
||||||
|
_ratio FLOAT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Check if this is a subtask
|
-- Check if this is a subtask
|
||||||
IF NEW.parent_task_id IS NOT NULL THEN
|
IF NEW.parent_task_id IS NOT NULL THEN
|
||||||
@@ -232,6 +234,21 @@ BEGIN
|
|||||||
WHERE id = _parent_task_id;
|
WHERE id = _parent_task_id;
|
||||||
END IF;
|
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;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|||||||
@@ -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, stsc.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([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@ import db from "../../config/db";
|
|||||||
import {NotificationsService} from "../../services/notifications/notifications.service";
|
import {NotificationsService} from "../../services/notifications/notifications.service";
|
||||||
import {TASK_STATUS_COLOR_ALPHA} from "../../shared/constants";
|
import {TASK_STATUS_COLOR_ALPHA} from "../../shared/constants";
|
||||||
import {SocketEvents} from "../events";
|
import {SocketEvents} from "../events";
|
||||||
import {getLoggedInUserIdFromSocket, log_error, notifyProjectUpdates} from "../util";
|
import {getLoggedInUserIdFromSocket, log, log_error, notifyProjectUpdates} from "../util";
|
||||||
import TasksControllerV2 from "../../controllers/tasks-controller-v2";
|
import TasksControllerV2 from "../../controllers/tasks-controller-v2";
|
||||||
import {getTaskDetails, logStatusChange} from "../../services/activity-logs/activity-logs.service";
|
import {getTaskDetails, logProgressChange, logStatusChange} from "../../services/activity-logs/activity-logs.service";
|
||||||
import { assignMemberIfNot } from "./on-quick-assign-or-remove";
|
import { assignMemberIfNot } from "./on-quick-assign-or-remove";
|
||||||
|
import logger from "../../utils/logger";
|
||||||
|
|
||||||
export async function on_task_status_change(_io: Server, socket: Socket, data?: string) {
|
export async function on_task_status_change(_io: Server, socket: Socket, data?: string) {
|
||||||
try {
|
try {
|
||||||
@@ -49,6 +50,46 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the new status is in a "done" category
|
||||||
|
if (changeResponse.status_category?.is_done) {
|
||||||
|
// Get current progress value
|
||||||
|
const progressResult = await db.query(`
|
||||||
|
SELECT progress_value, manual_progress
|
||||||
|
FROM tasks
|
||||||
|
WHERE id = $1
|
||||||
|
`, [body.task_id]);
|
||||||
|
|
||||||
|
const currentProgress = progressResult.rows[0]?.progress_value;
|
||||||
|
const isManualProgress = progressResult.rows[0]?.manual_progress;
|
||||||
|
|
||||||
|
// Only update if not already 100%
|
||||||
|
if (currentProgress !== 100) {
|
||||||
|
// Update progress to 100%
|
||||||
|
await db.query(`
|
||||||
|
UPDATE tasks
|
||||||
|
SET progress_value = 100, manual_progress = TRUE
|
||||||
|
WHERE id = $1
|
||||||
|
`, [body.task_id]);
|
||||||
|
|
||||||
|
log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null);
|
||||||
|
|
||||||
|
// Log the progress change to activity logs
|
||||||
|
await logProgressChange({
|
||||||
|
task_id: body.task_id,
|
||||||
|
old_value: currentProgress !== null ? currentProgress.toString() : "0",
|
||||||
|
new_value: "100",
|
||||||
|
socket
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this is a subtask, update parent task progress
|
||||||
|
if (body.parent_task) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
|
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
|
||||||
|
|
||||||
socket.emit(SocketEvents.TASK_STATUS_CHANGE.toString(), {
|
socket.emit(SocketEvents.TASK_STATUS_CHANGE.toString(), {
|
||||||
|
|||||||
@@ -26,12 +26,32 @@ async function updateTaskAncestors(io: any, socket: Socket, projectId: string, t
|
|||||||
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
|
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
|
||||||
console.log(`Updated task ${taskId} progress after time estimation change: ${ratio}`);
|
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
|
// Emit the updated progress
|
||||||
socket.emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
progress_value: ratio
|
progress_value: ratio,
|
||||||
|
should_prompt_for_done: shouldPromptForDone
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,32 @@ async function updateTaskAncestors(io: any, socket: Socket, projectId: string, t
|
|||||||
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
|
const ratio = progressRatio?.rows[0]?.ratio?.ratio || 0;
|
||||||
console.log(`Updated task ${taskId} progress: ${ratio}`);
|
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
|
// Emit the updated progress
|
||||||
socket.emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id: taskId,
|
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
|
// Get the current progress value to log the change
|
||||||
const currentProgressResult = await db.query(
|
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]
|
[task_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentProgress = currentProgressResult.rows[0]?.progress_value;
|
const currentProgress = currentProgressResult.rows[0]?.progress_value;
|
||||||
const projectId = currentProgressResult.rows[0]?.project_id;
|
const projectId = currentProgressResult.rows[0]?.project_id;
|
||||||
|
const statusId = currentProgressResult.rows[0]?.status_id;
|
||||||
|
|
||||||
// Update the task progress in the database
|
// Update the task progress in the database
|
||||||
await db.query(
|
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(),
|
new_value: progress_value.toString(),
|
||||||
socket
|
socket
|
||||||
});
|
});
|
||||||
|
|
||||||
if (projectId) {
|
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
|
// Emit the update to all clients in the project room
|
||||||
socket.emit(
|
socket.emit(
|
||||||
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
SocketEvents.TASK_PROGRESS_UPDATED.toString(),
|
||||||
{
|
{
|
||||||
task_id,
|
task_id,
|
||||||
progress_value
|
progress_value,
|
||||||
|
should_prompt_for_done: shouldPromptForDone
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -67,4 +67,7 @@ export enum SocketEvents {
|
|||||||
// Task subtasks count events
|
// Task subtasks count events
|
||||||
GET_TASK_SUBTASKS_COUNT,
|
GET_TASK_SUBTASKS_COUNT,
|
||||||
TASK_SUBTASKS_COUNT,
|
TASK_SUBTASKS_COUNT,
|
||||||
|
|
||||||
|
// Task completion events
|
||||||
|
GET_DONE_STATUSES,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_progress } from "./commands/on-update-task-progress";
|
||||||
import { on_update_task_weight } from "./commands/on-update-task-weight";
|
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_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) {
|
export function register(io: any, socket: Socket) {
|
||||||
log(socket.id, "client registered");
|
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_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.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.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_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_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));
|
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_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.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_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.io built-in event
|
||||||
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));
|
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { Form, InputNumber, Tooltip } from 'antd';
|
import { Form, InputNumber, Tooltip, Modal } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||||
import Flex from 'antd/lib/flex';
|
import Flex from 'antd/lib/flex';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
|
||||||
|
import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { updateBoardTaskStatus } from '@/features/board/board-slice';
|
||||||
|
import { updateTaskStatus } from '@/features/tasks/tasks.slice';
|
||||||
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
|
||||||
interface TaskDrawerProgressProps {
|
interface TaskDrawerProgressProps {
|
||||||
task: ITaskViewModel;
|
task: ITaskViewModel;
|
||||||
@@ -15,44 +23,23 @@ interface TaskDrawerProgressProps {
|
|||||||
|
|
||||||
const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||||
const { t } = useTranslation('task-drawer/task-drawer');
|
const { t } = useTranslation('task-drawer/task-drawer');
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tab } = useTabSearchParam();
|
||||||
|
|
||||||
const { project } = useAppSelector(state => state.projectReducer);
|
const { project } = useAppSelector(state => state.projectReducer);
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
const [confirmedHasSubtasks, setConfirmedHasSubtasks] = useState<boolean | null>(null);
|
const [isCompletionModalVisible, setIsCompletionModalVisible] = useState(false);
|
||||||
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
|
|
||||||
const isSubTask = !!task?.parent_task_id;
|
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}`);
|
|
||||||
|
|
||||||
// HIGHEST PRIORITY CHECK: Never show progress inputs for parent tasks with subtasks
|
// HIGHEST PRIORITY CHECK: Never show progress inputs for parent tasks with subtasks
|
||||||
// This check happens before any other logic to ensure consistency
|
|
||||||
if (hasSubTasks) {
|
if (hasSubTasks) {
|
||||||
console.error(`REJECTED: Progress input for parent task ${task.id} with ${task.sub_tasks_count} subtasks. confirmedHasSubtasks=${confirmedHasSubtasks}`);
|
|
||||||
return null;
|
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)
|
// Never show manual progress input for parent tasks (tasks with subtasks)
|
||||||
// Only show progress input for tasks without subtasks
|
// Only show progress input for tasks without subtasks
|
||||||
const showManualProgressInput = !hasSubTasks;
|
const showManualProgressInput = !hasSubTasks;
|
||||||
@@ -70,6 +57,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
if (data.weight !== undefined) {
|
if (data.weight !== undefined) {
|
||||||
form.setFieldsValue({ weight: data.weight });
|
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 +84,11 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
|
|
||||||
const handleProgressChange = (value: number | null) => {
|
const handleProgressChange = (value: number | null) => {
|
||||||
if (connected && task.id && value !== null && !hasSubTasks) {
|
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
|
// Ensure parent_task_id is not undefined
|
||||||
const parent_task_id = task.parent_task_id || null;
|
const parent_task_id = task.parent_task_id || null;
|
||||||
|
|
||||||
@@ -126,7 +123,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
parent_task_id: parent_task_id,
|
parent_task_id: parent_task_id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// If this is a subtask, request the parent's progress to be updated in UI
|
// If this is a subtask, request the parent's progress to be updated in UI
|
||||||
if (parent_task_id) {
|
if (parent_task_id) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -136,6 +133,55 @@ 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,
|
||||||
|
team_id: currentSession?.team_id,
|
||||||
|
parent_task: task.parent_task_id || null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
socket?.once(
|
||||||
|
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||||
|
(data: ITaskListStatusChangeResponse) => {
|
||||||
|
dispatch(setTaskStatus(data));
|
||||||
|
|
||||||
|
if (tab === 'tasks-list') {
|
||||||
|
dispatch(updateTaskStatus(data));
|
||||||
|
}
|
||||||
|
if (tab === 'board') {
|
||||||
|
dispatch(updateBoardTaskStatus(data));
|
||||||
|
}
|
||||||
|
if (data.parent_task)
|
||||||
|
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), data.parent_task);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(`No "done" statuses found for project ${task.project_id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const percentFormatter = (value: number | undefined) => (value ? `${value}%` : '0%');
|
const percentFormatter = (value: number | undefined) => (value ? `${value}%` : '0%');
|
||||||
const percentParser = (value: string | undefined) => {
|
const percentParser = (value: string | undefined) => {
|
||||||
const parsed = parseInt(value?.replace('%', '') || '0', 10);
|
const parsed = parseInt(value?.replace('%', '') || '0', 10);
|
||||||
@@ -217,6 +263,17 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,4 +67,7 @@ export enum SocketEvents {
|
|||||||
// Task subtasks count events
|
// Task subtasks count events
|
||||||
GET_TASK_SUBTASKS_COUNT,
|
GET_TASK_SUBTASKS_COUNT,
|
||||||
TASK_SUBTASKS_COUNT,
|
TASK_SUBTASKS_COUNT,
|
||||||
|
|
||||||
|
// Task completion events
|
||||||
|
GET_DONE_STATUSES,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user