diff --git a/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql b/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql
index 8898e599..b4650dc7 100644
--- a/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql
+++ b/worklenz-backend/database/migrations/20250423000000-subtask-manual-progress.sql
@@ -354,14 +354,19 @@ BEGIN
VALUES (_project_id, _team_id, _project_created_log)
RETURNING id INTO _project_created_log_id;
- -- add the team member in the project as a user
- INSERT INTO project_members (project_id, team_member_id, project_access_level_id)
- VALUES (_project_id, _team_member_id,
- (SELECT id FROM project_access_levels WHERE key = 'MEMBER'));
+ -- insert the project creator as a project member
+ INSERT INTO project_members (team_member_id, project_access_level_id, project_id, role_id)
+ VALUES (_team_member_id, (SELECT id FROM project_access_levels WHERE key = 'ADMIN'),
+ _project_id,
+ (SELECT id FROM roles WHERE team_id = _team_id AND default_role IS TRUE));
- -- register the project log
- INSERT INTO project_logs (project_id, team_id, description)
- VALUES (_project_id, _team_id, _project_member_added_log);
+ -- insert statuses
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('To Do', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE), 0);
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('Doing', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE), 1);
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('Done', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE), 2);
-- insert default project columns
PERFORM insert_task_list_columns(_project_id);
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
index ce20d5d1..c0c14cfe 100644
--- 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
@@ -33,6 +33,56 @@ export async function on_get_task_subtasks_count(io: any, socket: Socket, taskId
console.log(`Emitted subtask count for task ${taskId}: ${subtaskCount}`);
+ // If there are subtasks, also get their progress information
+ if (subtaskCount > 0) {
+ // Get all subtasks for this parent task with their progress information
+ const subtasksResult = await db.query(`
+ SELECT
+ t.id,
+ t.progress_value,
+ t.manual_progress,
+ t.weight,
+ CASE
+ WHEN t.manual_progress = TRUE THEN t.progress_value
+ ELSE COALESCE(
+ (SELECT (CASE WHEN tl.total_minutes > 0 THEN
+ (tl.total_minutes_spent / tl.total_minutes * 100)
+ ELSE 0 END)
+ FROM (
+ SELECT
+ t2.id,
+ t2.total_minutes,
+ COALESCE(SUM(twl.time_spent), 0) as total_minutes_spent
+ FROM tasks t2
+ LEFT JOIN task_work_log twl ON t2.id = twl.task_id
+ WHERE t2.id = t.id
+ GROUP BY t2.id, t2.total_minutes
+ ) tl
+ ), 0)
+ END as calculated_progress
+ FROM tasks t
+ WHERE t.parent_task_id = $1 AND t.archived IS FALSE
+ `, [taskId]);
+
+ // Emit progress updates for each subtask
+ for (const subtask of subtasksResult.rows) {
+ const progressValue = subtask.manual_progress ?
+ subtask.progress_value :
+ Math.floor(subtask.calculated_progress);
+
+ socket.emit(
+ SocketEvents.TASK_PROGRESS_UPDATED.toString(),
+ {
+ task_id: subtask.id,
+ progress_value: progressValue,
+ weight: subtask.weight
+ }
+ );
+ }
+
+ console.log(`Emitted progress updates for ${subtasksResult.rows.length} subtasks of task ${taskId}`);
+ }
+
} catch (error) {
log_error(`Error getting subtask count for task ${taskId}: ${error}`);
}
diff --git a/worklenz-frontend/path/to/members-reports-drawer.tsx b/worklenz-frontend/path/to/members-reports-drawer.tsx
deleted file mode 100644
index b9671dc1..00000000
--- a/worklenz-frontend/path/to/members-reports-drawer.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import MembersReportsTimeLogsTab from './members-reports-time-logs-tab';
-
-type MembersReportsDrawerProps = {
- memberId: string | null;
- exportTimeLogs: () => void;
-};
-
-const MembersReportsDrawer = ({ memberId, exportTimeLogs }: MembersReportsDrawerProps) => {
- return (
-
-
- {selectedMember.name}
-
-
-
-
-
- } iconPosition="end">
- {t('exportButton')}
-
-
-
-
- )
- }
- >
- {selectedMember && }
- {selectedMember && }
- {selectedMember && }
-
- );
-};
-
-export default MembersReportsDrawer;
\ No newline at end of file
diff --git a/worklenz-frontend/path/to/members-reports-time-logs-tab.tsx b/worklenz-frontend/path/to/members-reports-time-logs-tab.tsx
deleted file mode 100644
index a86c66ba..00000000
--- a/worklenz-frontend/path/to/members-reports-time-logs-tab.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { useState } from 'react';
-import { Flex, Skeleton } from 'antd';
-import { useTranslation } from 'react-i18next';
-import { useTimeLogs } from '../contexts/TimeLogsContext';
-import { BillableFilter } from './BillableFilter';
-import { TimeLogCard } from './TimeLogCard';
-import { EmptyListPlaceholder } from './EmptyListPlaceholder';
-import { TaskDrawer } from './TaskDrawer';
-import MembersReportsDrawer from './members-reports-drawer';
-
-const MembersReportsTimeLogsTab: React.FC = () => {
- const { t } = useTranslation();
- const { timeLogsData, billable, setBillable, exportTimeLogs, exporting } = useTimeLogs();
-
- return (
-
-
-
-
-
-
- {timeLogsData.length > 0 ? (
-
- {timeLogsData.map((logs, index) => (
-
- ))}
-
- ) : (
-
- )}
-
-
- {createPortal(, document.body)}
-
-
- );
-};
-
-export default MembersReportsTimeLogsTab;
\ No newline at end of file
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 f260800e..2b588dbf 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
@@ -13,7 +13,7 @@ import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.ty
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 { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
interface TaskDrawerProgressProps {
@@ -101,11 +101,28 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
})
);
+ socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: any) => {
+ dispatch(
+ updateTaskProgress({
+ taskId: task.id,
+ progress: data.complete_ratio,
+ totalTasksCount: data.total_tasks_count,
+ completedCount: data.completed_count,
+ })
+ );
+ });
+
+ if (task.id) {
+ setTimeout(() => {
+ socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
+ }, 500);
+ }
+
// If this is a subtask, request the parent's progress to be updated in UI
if (parent_task_id) {
setTimeout(() => {
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), parent_task_id);
- }, 100);
+ }, 500);
}
}
};
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 fc5d66d4..f9792485 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
@@ -28,6 +28,7 @@ import TaskDrawerPrioritySelector from './details/task-drawer-priority-selector/
import TaskDrawerBillable from './details/task-drawer-billable/task-drawer-billable';
import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress';
import { useAppSelector } from '@/hooks/useAppSelector';
+import logger from '@/utils/errorLogger';
interface TaskDetailsFormProps {
taskFormViewModel?: ITaskFormViewModel | null;
@@ -45,12 +46,12 @@ const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps)
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}`);
+ logger.debug(`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.`);
+ logger.debug(`Task ${task.id} has ${task.sub_tasks_count} subtasks. Hiding progress input.`);
return null;
}
diff --git a/worklenz-frontend/src/features/tasks/tasks.slice.ts b/worklenz-frontend/src/features/tasks/tasks.slice.ts
index 320a5cd1..cd443dbf 100644
--- a/worklenz-frontend/src/features/tasks/tasks.slice.ts
+++ b/worklenz-frontend/src/features/tasks/tasks.slice.ts
@@ -21,6 +21,7 @@ import { ITaskLabel, ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response';
import { produce } from 'immer';
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
+import { SocketEvents } from '@/shared/socket-events';
export enum IGroupBy {
STATUS = 'status',
@@ -192,6 +193,20 @@ export const fetchSubTasks = createAsyncThunk(
return [];
}
+ // Request subtask progress data when expanding the task
+ // This will trigger the socket to emit TASK_PROGRESS_UPDATED events for all subtasks
+ try {
+ // Get access to the socket from the state
+ const socket = (getState() as any).socketReducer?.socket;
+ if (socket?.connected) {
+ // Request subtask count and progress information
+ socket.emit(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), taskId);
+ }
+ } catch (error) {
+ console.error('Error requesting subtask progress:', error);
+ // Non-critical error, continue with fetching subtasks
+ }
+
const selectedMembers = taskReducer.taskAssignees
.filter(member => member.selected)
.map(member => member.id)
@@ -577,6 +592,7 @@ const taskSlice = createSlice({
for (const task of tasks) {
if (task.id === taskId) {
task.complete_ratio = progress;
+ task.progress_value = progress;
task.total_tasks_count = totalTasksCount;
task.completed_count = completedCount;
return true;
diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx
index 1db3a56c..96d7b05b 100644
--- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx
+++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-progress-cell/task-list-progress-cell.tsx
@@ -10,7 +10,7 @@ type TaskListProgressCellProps = {
const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => {
const { project } = useAppSelector(state => state.projectReducer);
- const isManualProgressEnabled = project?.use_manual_progress;
+ const isManualProgressEnabled = (task.project_use_manual_progress || task.project_use_weighted_progress || task.project_use_time_progress);;
const isSubtask = task.is_sub_task;
const hasManualProgress = task.manual_progress;
diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx
index c47ecd75..059454c3 100644
--- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx
+++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx
@@ -1548,7 +1548,6 @@ const TaskListTable: React.FC = ({ taskList, tableId, active
};
const handleCustomColumnSettings = (columnKey: string) => {
- console.log('columnKey', columnKey);
if (!columnKey) return;
setEditColumnKey(columnKey);
dispatch(setCustomColumnModalAttributes({modalType: 'edit', columnId: columnKey}));
diff --git a/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts b/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts
index 4ab36c27..ec30a0e3 100644
--- a/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts
+++ b/worklenz-frontend/src/types/project/projectTasksViewModel.types.ts
@@ -90,6 +90,10 @@ export interface IProjectTask {
isVisible?: boolean;
estimated_string?: string;
custom_column_values?: Record;
+ progress_value?: number;
+ project_use_manual_progress?: boolean;
+ project_use_time_progress?: boolean;
+ project_use_weighted_progress?: boolean;
}
export interface IProjectTasksViewModel {