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} - - - - - - - - - - ) - } - > - {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 {