Merge pull request #107 from chamikaJ/fix/custom-progress-methods

Enhance task progress tracking and UI updates
This commit is contained in:
Chamika J
2025-05-05 16:58:51 +05:30
committed by GitHub
10 changed files with 105 additions and 103 deletions

View File

@@ -354,14 +354,19 @@ BEGIN
VALUES (_project_id, _team_id, _project_created_log) VALUES (_project_id, _team_id, _project_created_log)
RETURNING id INTO _project_created_log_id; RETURNING id INTO _project_created_log_id;
-- add the team member in the project as a user -- insert the project creator as a project member
INSERT INTO project_members (project_id, team_member_id, project_access_level_id) INSERT INTO project_members (team_member_id, project_access_level_id, project_id, role_id)
VALUES (_project_id, _team_member_id, VALUES (_team_member_id, (SELECT id FROM project_access_levels WHERE key = 'ADMIN'),
(SELECT id FROM project_access_levels WHERE key = 'MEMBER')); _project_id,
(SELECT id FROM roles WHERE team_id = _team_id AND default_role IS TRUE));
-- register the project log -- insert statuses
INSERT INTO project_logs (project_id, team_id, description) INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
VALUES (_project_id, _team_id, _project_member_added_log); 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 -- insert default project columns
PERFORM insert_task_list_columns(_project_id); PERFORM insert_task_list_columns(_project_id);

View File

@@ -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}`); 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) { } catch (error) {
log_error(`Error getting subtask count for task ${taskId}: ${error}`); log_error(`Error getting subtask count for task ${taskId}: ${error}`);
} }

View File

@@ -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 (
<Drawer
open={isDrawerOpen}
onClose={handleClose}
width={900}
destroyOnClose
title={
selectedMember && (
<Flex align="center" justify="space-between">
<Flex gap={8} align="center" style={{ fontWeight: 500 }}>
<Typography.Text>{selectedMember.name}</Typography.Text>
</Flex>
<Space>
<TimeWiseFilter />
<Dropdown
menu={{
items: [
{ key: '1', label: t('timeLogsButton'), onClick: exportTimeLogs },
{ key: '2', label: t('activityLogsButton') },
{ key: '3', label: t('tasksButton') },
],
}}
>
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('exportButton')}
</Button>
</Dropdown>
</Space>
</Flex>
)
}
>
{selectedMember && <MembersReportsDrawerTabs memberId={selectedMember.id} />}
{selectedMember && <MembersOverviewTasksStatsDrawer memberId={selectedMember.id} />}
{selectedMember && <MembersOverviewProjectsStatsDrawer memberId={selectedMember.id} />}
</Drawer>
);
};
export default MembersReportsDrawer;

View File

@@ -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 (
<Flex vertical gap={24}>
<BillableFilter billable={billable} onBillableChange={setBillable} />
<button onClick={exportTimeLogs} disabled={exporting}>
{exporting ? t('exporting') : t('exportTimeLogs')}
</button>
<Skeleton active loading={exporting} paragraph={{ rows: 10 }}>
{timeLogsData.length > 0 ? (
<Flex vertical gap={24}>
{timeLogsData.map((logs, index) => (
<TimeLogCard key={index} data={logs} />
))}
</Flex>
) : (
<EmptyListPlaceholder text={t('timeLogsEmptyPlaceholder')} />
)}
</Skeleton>
{createPortal(<TaskDrawer />, document.body)}
<MembersReportsDrawer memberId={/* pass the memberId here */} exportTimeLogs={exportTimeLogs} />
</Flex>
);
};
export default MembersReportsTimeLogsTab;

View File

@@ -13,7 +13,7 @@ import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.ty
import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice'; import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateBoardTaskStatus } from '@/features/board/board-slice'; 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'; import useTabSearchParam from '@/hooks/useTabSearchParam';
interface TaskDrawerProgressProps { 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 this is a subtask, request the parent's progress to be updated in UI
if (parent_task_id) { if (parent_task_id) {
setTimeout(() => { setTimeout(() => {
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), parent_task_id); socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), parent_task_id);
}, 100); }, 500);
} }
} }
}; };

View File

@@ -28,6 +28,7 @@ import TaskDrawerPrioritySelector from './details/task-drawer-priority-selector/
import TaskDrawerBillable from './details/task-drawer-billable/task-drawer-billable'; import TaskDrawerBillable from './details/task-drawer-billable/task-drawer-billable';
import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress'; import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import logger from '@/utils/errorLogger';
interface TaskDetailsFormProps { interface TaskDetailsFormProps {
taskFormViewModel?: ITaskFormViewModel | null; taskFormViewModel?: ITaskFormViewModel | null;
@@ -45,12 +46,12 @@ const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps)
const isSubTask = !!task?.parent_task_id; const isSubTask = !!task?.parent_task_id;
// Add more aggressive logging and checks // 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 // STRICT RULE: Never show progress input for parent tasks with subtasks
// This is the most important check and must be done first // This is the most important check and must be done first
if (hasSubTasks) { 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; return null;
} }

View File

@@ -21,6 +21,7 @@ import { ITaskLabel, ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response'; import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response';
import { produce } from 'immer'; import { produce } from 'immer';
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service'; import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
import { SocketEvents } from '@/shared/socket-events';
export enum IGroupBy { export enum IGroupBy {
STATUS = 'status', STATUS = 'status',
@@ -192,6 +193,20 @@ export const fetchSubTasks = createAsyncThunk(
return []; 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 const selectedMembers = taskReducer.taskAssignees
.filter(member => member.selected) .filter(member => member.selected)
.map(member => member.id) .map(member => member.id)
@@ -577,6 +592,7 @@ const taskSlice = createSlice({
for (const task of tasks) { for (const task of tasks) {
if (task.id === taskId) { if (task.id === taskId) {
task.complete_ratio = progress; task.complete_ratio = progress;
task.progress_value = progress;
task.total_tasks_count = totalTasksCount; task.total_tasks_count = totalTasksCount;
task.completed_count = completedCount; task.completed_count = completedCount;
return true; return true;

View File

@@ -10,7 +10,7 @@ type TaskListProgressCellProps = {
const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => { const TaskListProgressCell = ({ task }: TaskListProgressCellProps) => {
const { project } = useAppSelector(state => state.projectReducer); 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 isSubtask = task.is_sub_task;
const hasManualProgress = task.manual_progress; const hasManualProgress = task.manual_progress;

View File

@@ -1548,7 +1548,6 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
}; };
const handleCustomColumnSettings = (columnKey: string) => { const handleCustomColumnSettings = (columnKey: string) => {
console.log('columnKey', columnKey);
if (!columnKey) return; if (!columnKey) return;
setEditColumnKey(columnKey); setEditColumnKey(columnKey);
dispatch(setCustomColumnModalAttributes({modalType: 'edit', columnId: columnKey})); dispatch(setCustomColumnModalAttributes({modalType: 'edit', columnId: columnKey}));

View File

@@ -90,6 +90,10 @@ export interface IProjectTask {
isVisible?: boolean; isVisible?: boolean;
estimated_string?: string; estimated_string?: string;
custom_column_values?: Record<string, any>; custom_column_values?: Record<string, any>;
progress_value?: number;
project_use_manual_progress?: boolean;
project_use_time_progress?: boolean;
project_use_weighted_progress?: boolean;
} }
export interface IProjectTasksViewModel { export interface IProjectTasksViewModel {