Merge pull request #107 from chamikaJ/fix/custom-progress-methods
Enhance task progress tracking and UI updates
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user