Merge pull request #99 from chamikaJ/fix/custom-progress-methods
Fix/custom progress methods
This commit is contained in:
@@ -38,5 +38,14 @@
|
||||
"createClient": "Create client",
|
||||
"searchInputPlaceholder": "Search by name or email",
|
||||
"hoursPerDayValidationMessage": "Hours per day must be a number between 1 and 24",
|
||||
"noPermission": "No permission"
|
||||
"workingDaysValidationMessage": "Working days must be a positive number",
|
||||
"manDaysValidationMessage": "Man days must be a positive number",
|
||||
"noPermission": "No permission",
|
||||
"progressSettings": "Progress Settings",
|
||||
"manualProgress": "Manual Progress",
|
||||
"manualProgressTooltip": "Allow manual progress updates for tasks without subtasks",
|
||||
"weightedProgress": "Weighted Progress",
|
||||
"weightedProgressTooltip": "Calculate progress based on subtask weights",
|
||||
"timeProgress": "Time-based Progress",
|
||||
"timeProgressTooltip": "Calculate progress based on estimated time"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,15 @@
|
||||
"hide-start-date": "Hide Start Date",
|
||||
"show-start-date": "Show Start Date",
|
||||
"hours": "Hours",
|
||||
"minutes": "Minutes"
|
||||
"minutes": "Minutes",
|
||||
"progressValue": "Progress Value",
|
||||
"progressValueTooltip": "Set the progress percentage (0-100%)",
|
||||
"progressValueRequired": "Please enter a progress value",
|
||||
"progressValueRange": "Progress must be between 0 and 100",
|
||||
"taskWeight": "Task Weight",
|
||||
"taskWeightTooltip": "Set the weight of this subtask (percentage)",
|
||||
"taskWeightRequired": "Please enter a task weight",
|
||||
"taskWeightRange": "Weight must be between 0 and 100"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Search or create",
|
||||
|
||||
@@ -38,5 +38,14 @@
|
||||
"createClient": "Crear cliente",
|
||||
"searchInputPlaceholder": "Busca por nombre o email",
|
||||
"hoursPerDayValidationMessage": "Las horas por día deben ser un número entre 1 y 24",
|
||||
"noPermission": "Sin permiso"
|
||||
"workingDaysValidationMessage": "Los días de trabajo deben ser un número positivo",
|
||||
"manDaysValidationMessage": "Los días hombre deben ser un número positivo",
|
||||
"noPermission": "Sin permiso",
|
||||
"progressSettings": "Configuración de Progreso",
|
||||
"manualProgress": "Progreso Manual",
|
||||
"manualProgressTooltip": "Permitir actualizaciones manuales de progreso para tareas sin subtareas",
|
||||
"weightedProgress": "Progreso Ponderado",
|
||||
"weightedProgressTooltip": "Calcular el progreso basado en los pesos de las subtareas",
|
||||
"timeProgress": "Progreso Basado en Tiempo",
|
||||
"timeProgressTooltip": "Calcular el progreso basado en el tiempo estimado"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,15 @@
|
||||
"hide-start-date": "Ocultar fecha de inicio",
|
||||
"show-start-date": "Mostrar fecha de inicio",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos"
|
||||
"minutes": "Minutos",
|
||||
"progressValue": "Valor de Progreso",
|
||||
"progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)",
|
||||
"progressValueRequired": "Por favor, introduce un valor de progreso",
|
||||
"progressValueRange": "El progreso debe estar entre 0 y 100",
|
||||
"taskWeight": "Peso de la Tarea",
|
||||
"taskWeightTooltip": "Establecer el peso de esta subtarea (porcentaje)",
|
||||
"taskWeightRequired": "Por favor, introduce un peso para la tarea",
|
||||
"taskWeightRange": "El peso debe estar entre 0 y 100"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Buscar o crear",
|
||||
|
||||
@@ -38,5 +38,14 @@
|
||||
"createClient": "Criar cliente",
|
||||
"searchInputPlaceholder": "Pesquise por nome ou email",
|
||||
"hoursPerDayValidationMessage": "As horas por dia devem ser um número entre 1 e 24",
|
||||
"noPermission": "Sem permissão"
|
||||
"workingDaysValidationMessage": "Os dias de trabalho devem ser um número positivo",
|
||||
"manDaysValidationMessage": "Os dias de homem devem ser um número positivo",
|
||||
"noPermission": "Sem permissão",
|
||||
"progressSettings": "Configurações de Progresso",
|
||||
"manualProgress": "Progresso Manual",
|
||||
"manualProgressTooltip": "Permitir atualizações manuais de progresso para tarefas sem subtarefas",
|
||||
"weightedProgress": "Progresso Ponderado",
|
||||
"weightedProgressTooltip": "Calcular o progresso com base nos pesos das subtarefas",
|
||||
"timeProgress": "Progresso Baseado em Tempo",
|
||||
"timeProgressTooltip": "Calcular o progresso com base no tempo estimado"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,15 @@
|
||||
"hide-start-date": "Ocultar data de início",
|
||||
"show-start-date": "Mostrar data de início",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos"
|
||||
"minutes": "Minutos",
|
||||
"progressValue": "Valor de Progresso",
|
||||
"progressValueTooltip": "Definir a porcentagem de progresso (0-100%)",
|
||||
"progressValueRequired": "Por favor, insira um valor de progresso",
|
||||
"progressValueRange": "O progresso deve estar entre 0 e 100",
|
||||
"taskWeight": "Peso da Tarefa",
|
||||
"taskWeightTooltip": "Definir o peso desta subtarefa (porcentagem)",
|
||||
"taskWeightRequired": "Por favor, insira um peso para a tarefa",
|
||||
"taskWeightRange": "O peso deve estar entre 0 e 100"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Pesquisar ou criar",
|
||||
|
||||
@@ -10,6 +10,11 @@ import { IProjectManager } from '@/types/project/projectManager.types';
|
||||
|
||||
const rootUrl = `${API_BASE_URL}/projects`;
|
||||
|
||||
interface UpdateProjectPayload {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const projectsApiService = {
|
||||
getProjects: async (
|
||||
index: number,
|
||||
@@ -78,13 +83,11 @@ export const projectsApiService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProject: async (
|
||||
id: string,
|
||||
project: IProjectViewModel
|
||||
): Promise<IServerResponse<IProjectViewModel>> => {
|
||||
updateProject: async (payload: UpdateProjectPayload): Promise<IServerResponse<IProjectViewModel>> => {
|
||||
const { id, ...data } = payload;
|
||||
const q = toQueryString({ current_project_id: id });
|
||||
const url = `${rootUrl}/${id}${q}`;
|
||||
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, project);
|
||||
const url = `${API_BASE_URL}/projects/${id}${q}`;
|
||||
const response = await apiClient.patch<IServerResponse<IProjectViewModel>>(url, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Popconfirm,
|
||||
Skeleton,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
@@ -46,7 +47,11 @@ import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse
|
||||
import { calculateTimeDifference } from '@/utils/calculate-time-difference';
|
||||
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { setProjectData, toggleProjectDrawer, setProjectId as setDrawerProjectId } from '@/features/project/project-drawer.slice';
|
||||
import {
|
||||
setProjectData,
|
||||
toggleProjectDrawer,
|
||||
setProjectId as setDrawerProjectId,
|
||||
} from '@/features/project/project-drawer.slice';
|
||||
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { evt_projects_create } from '@/shared/worklenz-analytics-events';
|
||||
@@ -60,7 +65,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
|
||||
// State
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const [selectedProjectManager, setSelectedProjectManager] = useState<ITeamMemberViewModel | null>(
|
||||
@@ -96,6 +101,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
working_days: project?.working_days || 0,
|
||||
man_days: project?.man_days || 0,
|
||||
hours_per_day: project?.hours_per_day || 8,
|
||||
use_manual_progress: project?.use_manual_progress || false,
|
||||
use_weighted_progress: project?.use_weighted_progress || false,
|
||||
use_time_progress: project?.use_time_progress || false,
|
||||
}),
|
||||
[project, projectStatuses, projectHealths]
|
||||
);
|
||||
@@ -155,6 +163,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
man_days: parseInt(values.man_days),
|
||||
hours_per_day: parseInt(values.hours_per_day),
|
||||
project_manager: selectedProjectManager,
|
||||
use_manual_progress: values.use_manual_progress || false,
|
||||
use_weighted_progress: values.use_weighted_progress || false,
|
||||
use_time_progress: values.use_time_progress || false,
|
||||
};
|
||||
|
||||
const action =
|
||||
@@ -169,7 +180,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
dispatch(toggleProjectDrawer());
|
||||
if (!editMode) {
|
||||
trackMixpanelEvent(evt_projects_create);
|
||||
navigate(`/worklenz/projects/${response.data.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
|
||||
navigate(
|
||||
`/worklenz/projects/${response.data.body.id}?tab=tasks-list&pinned_tab=tasks-list`
|
||||
);
|
||||
}
|
||||
refetchProjects();
|
||||
window.location.reload(); // Refresh the page
|
||||
@@ -184,8 +197,17 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
logger.error('Error saving project', error);
|
||||
}
|
||||
};
|
||||
const calculateWorkingDays = (startDate: dayjs.Dayjs | null, endDate: dayjs.Dayjs | null): number => {
|
||||
if (!startDate || !endDate || !startDate.isValid() || !endDate.isValid() || startDate.isAfter(endDate)) {
|
||||
const calculateWorkingDays = (
|
||||
startDate: dayjs.Dayjs | null,
|
||||
endDate: dayjs.Dayjs | null
|
||||
): number => {
|
||||
if (
|
||||
!startDate ||
|
||||
!endDate ||
|
||||
!startDate.isValid() ||
|
||||
!endDate.isValid() ||
|
||||
startDate.isAfter(endDate)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -213,7 +235,16 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
...project,
|
||||
start_date: project.start_date ? dayjs(project.start_date) : null,
|
||||
end_date: project.end_date ? dayjs(project.end_date) : null,
|
||||
working_days: form.getFieldValue('start_date') && form.getFieldValue('end_date') ? calculateWorkingDays(form.getFieldValue('start_date'), form.getFieldValue('end_date')) : project.working_days || 0,
|
||||
working_days:
|
||||
form.getFieldValue('start_date') && form.getFieldValue('end_date')
|
||||
? calculateWorkingDays(
|
||||
form.getFieldValue('start_date'),
|
||||
form.getFieldValue('end_date')
|
||||
)
|
||||
: project.working_days || 0,
|
||||
use_manual_progress: project.use_manual_progress || false,
|
||||
use_weighted_progress: project.use_weighted_progress || false,
|
||||
use_time_progress: project.use_time_progress || false,
|
||||
});
|
||||
setSelectedProjectManager(project.project_manager || null);
|
||||
setLoading(false);
|
||||
@@ -284,6 +315,49 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
setIsFormValid(isValid);
|
||||
};
|
||||
|
||||
// Progress calculation method handlers
|
||||
const handleManualProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: true,
|
||||
use_weighted_progress: false,
|
||||
use_time_progress: false,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightedProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
use_weighted_progress: true,
|
||||
use_time_progress: false,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_weighted_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
use_weighted_progress: false,
|
||||
use_time_progress: true,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_time_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
// loading={loading}
|
||||
@@ -329,12 +403,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
}
|
||||
>
|
||||
{!isEditable && (
|
||||
<Alert
|
||||
message={t('noPermission')}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Alert message={t('noPermission')} type="warning" showIcon style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
<Skeleton active paragraph={{ rows: 12 }} loading={projectLoading}>
|
||||
<Form
|
||||
@@ -395,14 +464,11 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
<Form.Item name="date" layout="horizontal">
|
||||
<Flex gap={8}>
|
||||
<Form.Item
|
||||
name="start_date"
|
||||
label={t('startDate')}
|
||||
>
|
||||
<Form.Item name="start_date" label={t('startDate')}>
|
||||
<DatePicker
|
||||
disabledDate={disabledStartDate}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onChange={(date) => {
|
||||
onChange={date => {
|
||||
const endDate = form.getFieldValue('end_date');
|
||||
if (date && endDate) {
|
||||
const days = calculateWorkingDays(date, endDate);
|
||||
@@ -411,14 +477,11 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="end_date"
|
||||
label={t('endDate')}
|
||||
>
|
||||
<Form.Item name="end_date" label={t('endDate')}>
|
||||
<DatePicker
|
||||
disabledDate={disabledEndDate}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onChange={(date) => {
|
||||
onChange={date => {
|
||||
const startDate = form.getFieldValue('start_date');
|
||||
if (startDate && date) {
|
||||
const days = calculateWorkingDays(startDate, date);
|
||||
@@ -429,22 +492,51 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
|
||||
<Form.Item
|
||||
name="working_days"
|
||||
label={t('estimateWorkingDays')}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value >= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('workingDaysValidationMessage', { min: 0 })));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" min={0} disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="man_days"
|
||||
label={t('estimateManDays')}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value >= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('manDaysValidationMessage', { min: 0 })));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
disabled // Make it read-only since it's calculated
|
||||
min={0}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onBlur={e => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (value < 0) {
|
||||
form.setFieldsValue({ man_days: 0 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="working_days" label={t('estimateWorkingDays')}>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
<Form.Item name="man_days" label={t('estimateManDays')}>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="hours_per_day"
|
||||
label={t('hoursPerDay')}
|
||||
@@ -454,12 +546,80 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
if (value === undefined || (value >= 0 && value <= 24)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('hoursPerDayValidationMessage', { min: 0, max: 24 })));
|
||||
return Promise.reject(
|
||||
new Error(t('hoursPerDayValidationMessage', { min: 0, max: 24 }))
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onBlur={e => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (value < 0) {
|
||||
form.setFieldsValue({ hours_per_day: 8 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">{t('progressSettings')}</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="use_manual_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('manualProgress')}</Typography.Text>
|
||||
<Tooltip title={t('manualProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleManualProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="use_weighted_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('weightedProgress')}</Typography.Text>
|
||||
<Tooltip title={t('weightedProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleWeightedProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="use_time_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('timeProgress')}</Typography.Text>
|
||||
<Tooltip title={t('timeProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleTimeProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -111,6 +111,32 @@ const TaskDrawerActivityLog = () => {
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
case IActivityLogAttributeTypes.PROGRESS:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="blue">
|
||||
{activity.previous || '0'}%
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="blue">
|
||||
{activity.current || '0'}%
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
case IActivityLogAttributeTypes.WEIGHT:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="purple">
|
||||
Weight: {activity.previous || '100'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="purple">
|
||||
Weight: {activity.current || '100'}
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
|
||||
// Show manual progress input only for tasks without subtasks (not parent tasks)
|
||||
// Parent tasks get their progress calculated from subtasks
|
||||
const showManualProgressInput = !hasSubTasks;
|
||||
|
||||
// Only show weight input for subtasks in weighted progress mode
|
||||
const showTaskWeightInput = project?.use_weighted_progress && isSubTask;
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for progress updates from the server
|
||||
const handleProgressUpdate = (data: any) => {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket?.on(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleProgressUpdate);
|
||||
|
||||
// When the component mounts, explicitly request the latest progress for this task
|
||||
if (connected && task.id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket?.off(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleProgressUpdate);
|
||||
};
|
||||
}, [socket, connected, task.id, form]);
|
||||
|
||||
const handleProgressChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.UPDATE_TASK_PROGRESS.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
progress_value: value,
|
||||
parent_task_id: parent_task_id,
|
||||
})
|
||||
);
|
||||
|
||||
// If this task has subtasks, request recalculation of its progress
|
||||
if (hasSubTasks) {
|
||||
setTimeout(() => {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null) {
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.UPDATE_TASK_WEIGHT.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
weight: value,
|
||||
parent_task_id: parent_task_id,
|
||||
})
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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={[
|
||||
{
|
||||
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>
|
||||
)}
|
||||
{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={[
|
||||
{
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDrawerProgress;
|
||||
@@ -210,10 +210,11 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onConfirm={() => handleDeleteSubTask(record.id)}
|
||||
onPopupClick={(e) => e.stopPropagation()}
|
||||
onConfirm={(e) => {handleDeleteSubTask(record.id)}}
|
||||
>
|
||||
<Tooltip title="Delete">
|
||||
<Button shape="default" icon={<DeleteOutlined />} size="small" />
|
||||
<Button shape="default" icon={<DeleteOutlined />} size="small" onClick={(e)=> e.stopPropagation()} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -125,7 +125,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<ReloadOutlined spin={loadingSubTasks} />}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent click from bubbling up
|
||||
fetchSubTasks();
|
||||
}}
|
||||
@@ -182,19 +182,15 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
label: <Typography.Text strong>{t('taskInfoTab.comments.title')}</Typography.Text>,
|
||||
style: panelStyle,
|
||||
className: 'custom-task-drawer-info-collapse',
|
||||
children: (
|
||||
<TaskComments
|
||||
taskId={selectedTaskId || ''}
|
||||
t={t}
|
||||
/>
|
||||
),
|
||||
children: <TaskComments taskId={selectedTaskId || ''} t={t} />,
|
||||
},
|
||||
];
|
||||
|
||||
// Filter out the 'subTasks' item if this task is a subtask
|
||||
const infoItems = taskFormViewModel?.task?.parent_task_id
|
||||
? allInfoItems.filter(item => item.key !== 'subTasks')
|
||||
: allInfoItems;
|
||||
// Filter out the 'subTasks' item if this task is more than level 2
|
||||
const infoItems =
|
||||
(taskFormViewModel?.task?.task_level ?? 0) >= 2
|
||||
? allInfoItems.filter(item => item.key !== 'subTasks')
|
||||
: allInfoItems;
|
||||
|
||||
const fetchSubTasks = async () => {
|
||||
if (!selectedTaskId || loadingSubTasks) return;
|
||||
@@ -281,7 +277,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
defaultActiveKey={[
|
||||
'details',
|
||||
'description',
|
||||
...(taskFormViewModel?.task?.parent_task_id ? [] : ['subTasks']),
|
||||
'subTasks',
|
||||
'dependencies',
|
||||
'attachments',
|
||||
'comments',
|
||||
|
||||
@@ -32,7 +32,7 @@ const TaskDrawer = () => {
|
||||
const [refreshTimeLogTrigger, setRefreshTimeLogTrigger] = useState(0);
|
||||
|
||||
const { showTaskDrawer, timeLogEditing } = useAppSelector(state => state.taskDrawerReducer);
|
||||
|
||||
const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const taskNameInputRef = useRef<InputRef>(null);
|
||||
const isClosingManually = useRef(false);
|
||||
|
||||
@@ -47,20 +47,32 @@ const TaskDrawer = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleOnClose = () => {
|
||||
// Set flag to indicate we're manually closing the drawer
|
||||
isClosingManually.current = true;
|
||||
setActiveTab('info');
|
||||
|
||||
// Explicitly clear the task parameter from URL
|
||||
clearTaskFromUrl();
|
||||
|
||||
// Update the Redux state
|
||||
const resetTaskState = () => {
|
||||
dispatch(setShowTaskDrawer(false));
|
||||
dispatch(setSelectedTaskId(null));
|
||||
dispatch(setTaskFormViewModel({}));
|
||||
dispatch(setTaskSubscribers([]));
|
||||
};
|
||||
|
||||
const handleOnClose = (
|
||||
e?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>
|
||||
) => {
|
||||
// Set flag to indicate we're manually closing the drawer
|
||||
isClosingManually.current = true;
|
||||
setActiveTab('info');
|
||||
clearTaskFromUrl();
|
||||
|
||||
const isClickOutsideDrawer =
|
||||
e?.target && (e.target as HTMLElement).classList.contains('ant-drawer-mask');
|
||||
|
||||
if (isClickOutsideDrawer || !taskFormViewModel?.task?.is_sub_task) {
|
||||
resetTaskState();
|
||||
} else {
|
||||
dispatch(setSelectedTaskId(null));
|
||||
dispatch(setTaskFormViewModel({}));
|
||||
dispatch(setTaskSubscribers([]));
|
||||
dispatch(setSelectedTaskId(taskFormViewModel?.task?.parent_task_id || null));
|
||||
}
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
isClosingManually.current = false;
|
||||
@@ -176,8 +188,8 @@ const TaskDrawer = () => {
|
||||
// Get conditional body style
|
||||
const getBodyStyle = () => {
|
||||
const baseStyle = {
|
||||
padding: '24px',
|
||||
overflow: 'auto'
|
||||
padding: '24px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
|
||||
if (activeTab === 'timeLog' && timeLogEditing.isEditing) {
|
||||
|
||||
@@ -55,10 +55,9 @@ const initialState: TaskListState = {
|
||||
|
||||
export const getProject = createAsyncThunk(
|
||||
'project/getProject',
|
||||
async (projectId: string, { rejectWithValue, dispatch }) => {
|
||||
async (projectId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await projectsApiService.getProject(projectId);
|
||||
dispatch(setProject(response.body));
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch project');
|
||||
|
||||
42
worklenz-frontend/src/features/projects/projects.slice.ts
Normal file
42
worklenz-frontend/src/features/projects/projects.slice.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { projectsApiService } from '@/api/projects/projects.api.service';
|
||||
|
||||
interface UpdateProjectPayload {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const projectsSlice = createSlice({
|
||||
name: 'projects',
|
||||
initialState: {
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Export actions
|
||||
export const { setLoading, setError } = projectsSlice.actions;
|
||||
|
||||
// Async thunks
|
||||
export const updateProject = (payload: UpdateProjectPayload) => async (dispatch: any) => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const response = await projectsApiService.updateProject(payload);
|
||||
dispatch(setLoading(false));
|
||||
return response;
|
||||
} catch (error) {
|
||||
dispatch(setError((error as Error).message));
|
||||
dispatch(setLoading(false));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default projectsSlice.reducer;
|
||||
@@ -22,7 +22,7 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { setProject, setImportTaskTemplateDrawerOpen, setRefreshTimestamp } from '@features/project/project.slice';
|
||||
import { setProject, setImportTaskTemplateDrawerOpen, setRefreshTimestamp, getProject } from '@features/project/project.slice';
|
||||
import { addTask, fetchTaskGroups, fetchTaskListColumns, IGroupBy } from '@features/tasks/tasks.slice';
|
||||
import ProjectStatusIcon from '@/components/common/project-status-icon/project-status-icon';
|
||||
import { formatDate } from '@/utils/timeUtils';
|
||||
@@ -70,6 +70,7 @@ const ProjectViewHeader = () => {
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!projectId) return;
|
||||
dispatch(getProject(projectId));
|
||||
switch (tab) {
|
||||
case 'tasks-list':
|
||||
dispatch(fetchTaskListColumns(projectId));
|
||||
|
||||
@@ -377,6 +377,41 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
||||
};
|
||||
}, [socket, dispatch]);
|
||||
|
||||
// Socket handler for task progress updates
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
const handleTaskProgressUpdated = (data: {
|
||||
task_id: string;
|
||||
progress_value?: number;
|
||||
weight?: number;
|
||||
}) => {
|
||||
if (data.progress_value !== undefined) {
|
||||
// Find the task in the task groups and update its progress
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(task => task.id === data.task_id);
|
||||
if (task) {
|
||||
dispatch(
|
||||
updateTaskProgress({
|
||||
taskId: data.task_id,
|
||||
progress: data.progress_value,
|
||||
totalTasksCount: task.total_tasks_count || 0,
|
||||
completedCount: task.completed_count || 0,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.on(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleTaskProgressUpdated);
|
||||
|
||||
return () => {
|
||||
socket.off(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleTaskProgressUpdated);
|
||||
};
|
||||
}, [socket, dispatch, taskGroups]);
|
||||
|
||||
const handleDragStart = useCallback(({ active }: DragStartEvent) => {
|
||||
setActiveId(active.id as string);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ const TaskListTaskCell = ({
|
||||
isSubTask: boolean,
|
||||
subTasksCount: number
|
||||
) => {
|
||||
if (subTasksCount > 0) {
|
||||
if (subTasksCount > 0 && !isSubTask) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleToggleExpansion(taskId)}
|
||||
@@ -112,23 +112,21 @@ const TaskListTaskCell = ({
|
||||
const renderSubtasksCountLabel = (taskId: string, isSubTask: boolean, subTasksCount: number) => {
|
||||
if (!taskId) return null;
|
||||
return (
|
||||
!isSubTask && (
|
||||
<Button
|
||||
onClick={() => handleToggleExpansion(taskId)}
|
||||
size="small"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingInline: 4,
|
||||
alignItems: 'center',
|
||||
justifyItems: 'center',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography.Text style={{ fontSize: 12, lineHeight: 1 }}>{subTasksCount}</Typography.Text>
|
||||
<DoubleRightOutlined style={{ fontSize: 10 }} />
|
||||
</Button>
|
||||
)
|
||||
<Button
|
||||
onClick={() => handleToggleExpansion(taskId)}
|
||||
size="small"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingInline: 4,
|
||||
alignItems: 'center',
|
||||
justifyItems: 'center',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography.Text style={{ fontSize: 12, lineHeight: 1 }}>{subTasksCount}</Typography.Text>
|
||||
<DoubleRightOutlined style={{ fontSize: 10 }} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -58,4 +58,9 @@ export enum SocketEvents {
|
||||
TASK_CUSTOM_COLUMN_UPDATE,
|
||||
CUSTOM_COLUMN_PINNED_CHANGE,
|
||||
TEAM_MEMBER_ROLE_CHANGE,
|
||||
|
||||
// Task progress events
|
||||
UPDATE_TASK_PROGRESS,
|
||||
UPDATE_TASK_WEIGHT,
|
||||
TASK_PROGRESS_UPDATED,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface IProjectViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
team_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
use_manual_progress: boolean;
|
||||
use_weighted_progress: boolean;
|
||||
use_time_progress: boolean;
|
||||
}
|
||||
@@ -41,4 +41,28 @@ export interface IProjectViewModel extends IProject {
|
||||
|
||||
team_member_default_view?: string;
|
||||
working_days?: number;
|
||||
|
||||
id?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
color_code?: string;
|
||||
status_id?: string;
|
||||
status_name?: string;
|
||||
status_color_dark?: string;
|
||||
health_name?: string;
|
||||
health_color?: string;
|
||||
health_color_dark?: string;
|
||||
category_color_dark?: string;
|
||||
client_id?: string;
|
||||
total_tasks?: number;
|
||||
completed_tasks?: number;
|
||||
tasks_progress?: number;
|
||||
man_days?: number;
|
||||
hours_per_day?: number;
|
||||
default_view?: string;
|
||||
task_key_prefix?: string;
|
||||
use_manual_progress?: boolean;
|
||||
use_weighted_progress?: boolean;
|
||||
use_time_progress?: boolean;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export enum IActivityLogAttributeTypes {
|
||||
ATTACHMENT = 'attachment',
|
||||
COMMENT = 'comment',
|
||||
ARCHIVE = 'archive',
|
||||
PROGRESS = 'progress',
|
||||
WEIGHT = 'weight',
|
||||
}
|
||||
|
||||
export interface IActivityLog {
|
||||
|
||||
@@ -17,42 +17,28 @@ export interface ITaskAssignee {
|
||||
}
|
||||
|
||||
export interface ITask {
|
||||
assignees?: ITaskAssignee[] | string[];
|
||||
assignees_ids?: any[];
|
||||
description?: string;
|
||||
done?: boolean;
|
||||
end?: string | Date;
|
||||
end_date?: string | Date;
|
||||
id?: string;
|
||||
name?: string;
|
||||
resize_valid?: boolean;
|
||||
start?: string | Date;
|
||||
start_date?: string | Date;
|
||||
_start?: Date;
|
||||
_end?: Date;
|
||||
color_code?: string;
|
||||
priority?: string;
|
||||
priority_id?: string;
|
||||
status?: string;
|
||||
status_id?: string;
|
||||
project_id?: string;
|
||||
reporter_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
show_handles?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
total_hours?: number;
|
||||
total_minutes?: number;
|
||||
name_color?: string;
|
||||
sub_tasks_count?: number;
|
||||
is_sub_task?: boolean;
|
||||
parent_task_name?: string;
|
||||
parent_task_id?: string;
|
||||
show_sub_tasks?: boolean;
|
||||
sub_tasks?: ISubTask[];
|
||||
archived?: boolean;
|
||||
subscribers?: IUser[];
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status_id: string;
|
||||
priority: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
total_hours: number;
|
||||
total_minutes: number;
|
||||
billable: boolean;
|
||||
phase_id: string;
|
||||
parent_task_id: string | null;
|
||||
project_id: string;
|
||||
team_id: string;
|
||||
task_key: string;
|
||||
labels: string[];
|
||||
assignees: string[];
|
||||
names: string[];
|
||||
sub_tasks_count: number;
|
||||
manual_progress: boolean;
|
||||
progress_value: number | null;
|
||||
weight: number | null;
|
||||
}
|
||||
|
||||
export interface IProjectMemberViewModel extends IProjectMember {
|
||||
@@ -65,24 +51,19 @@ export interface IProjectMemberViewModel extends IProjectMember {
|
||||
}
|
||||
|
||||
export interface ITaskViewModel extends ITask {
|
||||
task_key?: string;
|
||||
created_from_now?: string;
|
||||
updated_from_now?: string;
|
||||
reporter?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
sub_tasks_count?: number;
|
||||
is_sub_task?: boolean;
|
||||
status_color?: string;
|
||||
status_color_dark?: string;
|
||||
attachments_count?: number;
|
||||
complete_ratio?: number;
|
||||
names?: InlineMember[];
|
||||
labels?: ITaskLabel[];
|
||||
assignee_names?: InlineMember[];
|
||||
task_labels?: ITaskLabel[];
|
||||
timer_start_time?: number;
|
||||
phase_id?: string;
|
||||
billable?: boolean;
|
||||
recurring?: boolean;
|
||||
task_level?: number;
|
||||
}
|
||||
|
||||
export interface ITaskTeamMember extends ITeamMember {
|
||||
|
||||
Reference in New Issue
Block a user