Implement manual and weighted progress features for tasks
- Added SQL migrations to support manual progress and weighted progress calculations in tasks. - Updated the `get_task_complete_ratio` function to consider manual progress and subtask weights. - Enhanced the project model to include flags for manual, weighted, and time-based progress. - Integrated new progress settings in the project drawer and task drawer components. - Implemented socket events for real-time updates on task progress and weight changes. - Updated frontend localization files to include new progress-related terms and tooltips.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { Form, InputNumber, Tooltip } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import Flex from 'antd/lib/flex';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useEffect } from 'react';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
|
||||
interface TaskDrawerProgressProps {
|
||||
task: ITaskViewModel;
|
||||
form: any;
|
||||
}
|
||||
|
||||
const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
const isSubTask = !!task?.parent_task_id;
|
||||
const hasSubTasks = task?.sub_tasks_count > 0;
|
||||
|
||||
// Determine which progress input to show based on project settings
|
||||
const showManualProgressInput = project?.use_manual_progress && !hasSubTasks && !isSubTask;
|
||||
const showTaskWeightInput = project?.use_weighted_progress && isSubTask;
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for progress updates from the server
|
||||
socket?.on(SocketEvents.TASK_PROGRESS_UPDATED.toString(), (data) => {
|
||||
if (data.task_id === task.id) {
|
||||
if (data.progress_value !== undefined) {
|
||||
form.setFieldsValue({ progress_value: data.progress_value });
|
||||
}
|
||||
if (data.weight !== undefined) {
|
||||
form.setFieldsValue({ weight: data.weight });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket?.off(SocketEvents.TASK_PROGRESS_UPDATED.toString());
|
||||
};
|
||||
}, [socket, task.id, form]);
|
||||
|
||||
const handleProgressChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
socket?.emit(SocketEvents.UPDATE_TASK_PROGRESS.toString(), JSON.stringify({
|
||||
task_id: task.id,
|
||||
progress_value: value,
|
||||
parent_task_id: task.parent_task_id
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
socket?.emit(SocketEvents.UPDATE_TASK_WEIGHT.toString(), JSON.stringify({
|
||||
task_id: task.id,
|
||||
weight: value,
|
||||
parent_task_id: task.parent_task_id
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const percentFormatter = (value: number | undefined) => value ? `${value}%` : '0%';
|
||||
const percentParser = (value: string | undefined) => {
|
||||
const parsed = parseInt(value?.replace('%', '') || '0', 10);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
if (!showManualProgressInput && !showTaskWeightInput) {
|
||||
return null; // Don't show any progress inputs if not applicable
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showManualProgressInput && (
|
||||
<Form.Item
|
||||
name="progress_value"
|
||||
label={
|
||||
<Flex align="center" gap={4}>
|
||||
{t('taskInfoTab.details.progressValue')}
|
||||
<Tooltip title={t('taskInfoTab.details.progressValueTooltip')}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('taskInfoTab.details.progressValueRequired'),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
message: t('taskInfoTab.details.progressValueRange'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={percentFormatter}
|
||||
parser={percentParser}
|
||||
onBlur={(e) => {
|
||||
const value = percentParser(e.target.value);
|
||||
handleProgressChange(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{showTaskWeightInput && (
|
||||
<Form.Item
|
||||
name="weight"
|
||||
label={
|
||||
<Flex align="center" gap={4}>
|
||||
{t('taskInfoTab.details.taskWeight')}
|
||||
<Tooltip title={t('taskInfoTab.details.taskWeightTooltip')}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('taskInfoTab.details.taskWeightRequired'),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
message: t('taskInfoTab.details.taskWeightRange'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={percentFormatter}
|
||||
parser={percentParser}
|
||||
onBlur={(e) => {
|
||||
const value = percentParser(e.target.value);
|
||||
handleWeightChange(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDrawerProgress;
|
||||
@@ -26,6 +26,8 @@ import TaskDrawerDueDate from './details/task-drawer-due-date/task-drawer-due-da
|
||||
import TaskDrawerEstimation from './details/task-drawer-estimation/task-drawer-estimation';
|
||||
import TaskDrawerPrioritySelector from './details/task-drawer-priority-selector/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';
|
||||
|
||||
interface TaskDetailsFormProps {
|
||||
taskFormViewModel?: ITaskFormViewModel | null;
|
||||
@@ -34,6 +36,7 @@ interface TaskDetailsFormProps {
|
||||
const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const [form] = Form.useForm();
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskFormViewModel) {
|
||||
@@ -53,6 +56,8 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
labels: task?.labels || [],
|
||||
billable: task?.billable || false,
|
||||
notify: [],
|
||||
progress_value: task?.progress_value || null,
|
||||
weight: task?.weight || null,
|
||||
});
|
||||
}, [taskFormViewModel, form]);
|
||||
|
||||
@@ -89,6 +94,8 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
billable: false,
|
||||
progress_value: null,
|
||||
weight: null,
|
||||
}}
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
@@ -103,7 +110,7 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
|
||||
<Form.Item name="assignees" label={t('taskInfoTab.details.assignees')}>
|
||||
<Flex gap={4} align="center">
|
||||
<Avatars members={taskFormViewModel?.task?.names || []} />
|
||||
<Avatars members={taskFormViewModel?.task?.assignee_names || []} />
|
||||
<TaskDrawerAssigneeSelector
|
||||
task={(taskFormViewModel?.task as ITaskViewModel) || null}
|
||||
/>
|
||||
@@ -114,6 +121,10 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
|
||||
<TaskDrawerEstimation t={t} task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
|
||||
{(project?.use_manual_progress || project?.use_weighted_progress) && (taskFormViewModel?.task) && (
|
||||
<TaskDrawerProgress task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
)}
|
||||
|
||||
<Form.Item name="priority" label={t('taskInfoTab.details.priority')}>
|
||||
<TaskDrawerPrioritySelector task={taskFormViewModel?.task as ITaskViewModel} />
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user