Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions worklenz-backend/src/socket.io/commands/on-task-status-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
FROM tasks
WHERE id = $1
`, [body.task_id]);

const currentProgress = progressResult.rows[0]?.progress_value;
const isManualProgress = progressResult.rows[0]?.manual_progress;

// Only update if not already 100%
if (currentProgress !== 100) {
// Update progress to 100%
Expand All @@ -70,24 +70,41 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
SET progress_value = 100, manual_progress = TRUE
WHERE id = $1
`, [body.task_id]);

log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null);

// Log the progress change to activity logs
await logProgressChange({
task_id: body.task_id,
old_value: currentProgress !== null ? currentProgress.toString() : "0",
new_value: "100",
socket
});

// If this is a subtask, update parent task progress
if (body.parent_task) {
setTimeout(() => {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task);
}, 100);
}
}
} else {
// Task is moving from "done" to "todo" or "doing" - reset manual_progress to FALSE
// so progress can be recalculated based on subtasks
await db.query(`
UPDATE tasks
SET manual_progress = FALSE
WHERE id = $1
`, [body.task_id]);

log(`Task ${body.task_id} moved from done status - manual_progress reset to FALSE`, null);

// If this is a subtask, update parent task progress
if (body.parent_task) {
setTimeout(() => {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task);
}, 100);
}
}

const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
import ImprovedTaskFilters from '../task-management/improved-task-filters';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';

// Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
Expand All @@ -74,6 +75,9 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl

// Load filter data
useFilterDataLoader();

// Set up socket event handlers for real-time updates
useTaskSocketHandlers();

// Local state for drag overlay
const [activeTask, setActiveTask] = useState<any>(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Flex, Input, InputRef } from 'antd';
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { useAppSelector } from '@/hooks/useAppSelector';
import { getCurrentGroup } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { useAuthService } from '@/hooks/useAuth';
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
import { useParams } from 'react-router-dom';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import logger from '@/utils/errorLogger';

type EnhancedKanbanCreateSubtaskCardProps = {
sectionId: string;
parentTaskId: string;
setShowNewSubtaskCard: (x: boolean) => void;
};

const EnhancedKanbanCreateSubtaskCard = ({
sectionId,
parentTaskId,
setShowNewSubtaskCard,
}: EnhancedKanbanCreateSubtaskCardProps) => {
const { socket, connected } = useSocket();
const dispatch = useAppDispatch();

const [creatingTask, setCreatingTask] = useState<boolean>(false);
const [newSubtaskName, setNewSubtaskName] = useState<string>('');
const [isEnterKeyPressed, setIsEnterKeyPressed] = useState<boolean>(false);

const cardRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<InputRef>(null);

const { t } = useTranslation('kanban-board');

const themeMode = useAppSelector(state => state.themeReducer.mode);
const { projectId } = useParams();
const currentSession = useAuthService().getCurrentSession();

const createRequestBody = (): ITaskCreateRequest | null => {
if (!projectId || !currentSession) return null;
const body: ITaskCreateRequest = {
project_id: projectId,
name: newSubtaskName,
reporter_id: currentSession.id,
team_id: currentSession.team_id,
};

const groupBy = getCurrentGroup();
if (groupBy === 'status') {
body.status_id = sectionId || undefined;
} else if (groupBy === 'priority') {
body.priority_id = sectionId || undefined;
} else if (groupBy === 'phase') {
body.phase_id = sectionId || undefined;
}

if (parentTaskId) {
body.parent_task_id = parentTaskId;
}
return body;
};

const handleAddSubtask = () => {
if (creatingTask || !projectId || !currentSession || newSubtaskName.trim() === '' || !connected)
return;

try {
setCreatingTask(true);
const body = createRequestBody();
if (!body) return;

socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
if (!task) return;
setCreatingTask(false);
setNewSubtaskName('');
setTimeout(() => {
inputRef.current?.focus();
}, 0);
if (task.parent_task_id) {
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: {
id: string;
complete_ratio: number;
completed_count: number;
total_tasks_count: number;
parent_task: string;
}) => {
if (!data.parent_task) data.parent_task = task.parent_task_id || '';
dispatch(updateEnhancedKanbanTaskProgress({
id: task.id || '',
complete_ratio: data.complete_ratio,
completed_count: data.completed_count,
total_tasks_count: data.total_tasks_count,
parent_task: data.parent_task,
}));
});
}
});
} catch (error) {
logger.error('Error adding task:', error);
setCreatingTask(false);
}
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setIsEnterKeyPressed(true);
handleAddSubtask();
}
};

const handleInputBlur = () => {
if (!isEnterKeyPressed && newSubtaskName.length > 0) {
handleAddSubtask();
}
setIsEnterKeyPressed(false);
};

const handleCancelNewCard = (e: React.FocusEvent<HTMLDivElement>) => {
if (cardRef.current && !cardRef.current.contains(e.relatedTarget)) {
setNewSubtaskName('');
setShowNewSubtaskCard(false);
}
};

return (
<Flex
ref={cardRef}
vertical
gap={4}
style={{
width: '100%',
padding: 2,
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
borderRadius: 6,
cursor: 'pointer',
overflow: 'hidden',
}}
// className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
onBlur={handleCancelNewCard}
>
<Input
ref={inputRef}
value={newSubtaskName}
onClick={e => e.stopPropagation()}
onChange={e => setNewSubtaskName(e.target.value)}
onKeyDown={e => {
e.stopPropagation();
if (e.key === 'Enter') {
setIsEnterKeyPressed(true);
handleAddSubtask();
}
}}
onKeyUp={e => e.stopPropagation()}
onKeyPress={e => e.stopPropagation()}
onBlur={handleInputBlur}
placeholder={t('kanbanBoard.addSubTaskPlaceholder')}
className={`enhanced-kanban-create-subtask-input ${themeMode === 'dark' ? 'dark' : ''}`}
disabled={creatingTask}
autoFocus
/>
</Flex>
);
};

export default EnhancedKanbanCreateSubtaskCard;
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import { ForkOutlined } from '@ant-design/icons';
import { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { fetchBoardSubTasks, toggleTaskExpansion } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from 'antd';
import { List } from 'antd';
import { Skeleton } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card';
import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card';
import { useTranslation } from 'react-i18next';
import EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard';

interface EnhancedKanbanTaskCardProps {
task: IProjectTask;
Expand Down Expand Up @@ -58,7 +59,7 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
const [dueDate, setDueDate] = useState<Dayjs | null>(
task?.end_date ? dayjs(task?.end_date) : null
);
const [isSubTaskShow, setIsSubTaskShow] = useState(false);

const projectId = useAppSelector(state => state.projectReducer.projectId);
const {
attributes,
Expand Down Expand Up @@ -115,13 +116,17 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo

const handleSubTaskExpand = useCallback(() => {
if (task && task.id && projectId) {
if (task.show_sub_tasks) {
// Check if subtasks are already loaded and we have subtask data
if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
// If subtasks are already loaded, just toggle visibility
setIsSubTaskShow(prev => !prev);
} else {
// If subtasks need to be fetched, show the section first with loading state
setIsSubTaskShow(true);
dispatch(toggleTaskExpansion(task.id));
} else if (task.sub_tasks_count > 0) {
// If we have a subtask count but no loaded subtasks, fetch them
dispatch(toggleTaskExpansion(task.id));
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
} else {
// If no subtasks exist, just toggle visibility (will show empty state)
dispatch(toggleTaskExpansion(task.id));
}
}
}, [task, projectId, dispatch]);
Expand Down Expand Up @@ -199,13 +204,13 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count}</span>
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
</Flex>
</Flex>
<Flex vertical gap={8}>
{isSubTaskShow && (
{task.show_sub_tasks && (
<Flex vertical>
<Divider style={{ marginBlock: 0 }} />
<List>
Expand All @@ -215,13 +220,21 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</List.Item>
)}

{!task.sub_tasks_loading && task?.sub_tasks &&
task?.sub_tasks.map((subtask: any) => (
{!task.sub_tasks_loading && task?.sub_tasks && task.sub_tasks.length > 0 &&
task.sub_tasks.map((subtask: any) => (
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
))}

{!task.sub_tasks_loading && (!task?.sub_tasks || task.sub_tasks.length === 0) && task.sub_tasks_count === 0 && (
<List.Item>
<div style={{ padding: '8px 0', color: '#999', fontSize: '12px' }}>
{t('noSubtasks', 'No subtasks')}
</div>
</List.Item>
)}

{showNewSubtaskCard && (
<BoardCreateSubtaskCard
<EnhancedKanbanCreateSubtaskCard
sectionId={sectionId}
parentTaskId={task.id || ''}
setShowNewSubtaskCard={setShowNewSubtaskCard}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { setTaskAssignee } from '@/features/task-drawer/task-drawer.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
import { updateTaskAssignees as updateBoardTaskAssignees } from '@/features/board/board-slice';
import { updateTaskAssignees as updateTasksListTaskAssignees } from '@/features/tasks/tasks.slice';
import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
interface TaskDrawerAssigneeSelectorProps {
task: ITaskViewModel;
}
Expand Down Expand Up @@ -88,12 +89,12 @@ const TaskDrawerAssigneeSelector = ({ task }: TaskDrawerAssigneeSelectorProps) =
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
(data: ITaskAssigneesUpdateResponse) => {
dispatch(setTaskAssignee(data));
// if (tab === 'tasks-list') {
// dispatch(updateTasksListTaskAssignees(data));
// }
// if (tab === 'board') {
// dispatch(updateBoardTaskAssignees(data));
// }
if (tab === 'tasks-list') {
dispatch(updateTasksListTaskAssignees(data));
}
if (tab === 'board') {
dispatch(updateEnhancedKanbanTaskAssignees(data));
}
}
);
} catch (error) {
Expand Down
Loading