diff --git a/worklenz-frontend/public/locales/alb/task-management.json b/worklenz-frontend/public/locales/alb/task-management.json index a156ef3f..d7ae3f13 100644 --- a/worklenz-frontend/public/locales/alb/task-management.json +++ b/worklenz-frontend/public/locales/alb/task-management.json @@ -17,5 +17,22 @@ "renamePhase": "Riemërto Fazën", "changeCategory": "Ndrysho Kategorinë", "clickToEditGroupName": "Kliko për të ndryshuar emrin e grupit", - "enterGroupName": "Shkruani emrin e grupit" + "enterGroupName": "Shkruani emrin e grupit", + "todo": "Për t'u bërë", + "inProgress": "Në progres", + "done": "E kryer", + + "indicators": { + "tooltips": { + "subtasks": "{{count}} nën-detyrë", + "subtasks_plural": "{{count}} nën-detyra", + "comments": "{{count}} koment", + "comments_plural": "{{count}} komente", + "attachments": "{{count}} bashkëngjitje", + "attachments_plural": "{{count}} bashkëngjitje", + "subscribers": "Detyra ka abonentë", + "dependencies": "Detyra ka varësi", + "recurring": "Detyrë përsëritëse" + } + } } diff --git a/worklenz-frontend/public/locales/de/task-management.json b/worklenz-frontend/public/locales/de/task-management.json index b20d94a4..1bbdf7c9 100644 --- a/worklenz-frontend/public/locales/de/task-management.json +++ b/worklenz-frontend/public/locales/de/task-management.json @@ -17,5 +17,22 @@ "renamePhase": "Phase umbenennen", "changeCategory": "Kategorie ändern", "clickToEditGroupName": "Klicken Sie, um den Gruppennamen zu bearbeiten", - "enterGroupName": "Gruppennamen eingeben" + "enterGroupName": "Gruppennamen eingeben", + "todo": "Zu erledigen", + "inProgress": "In Bearbeitung", + "done": "Erledigt", + + "indicators": { + "tooltips": { + "subtasks": "{{count}} Unteraufgabe", + "subtasks_plural": "{{count}} Unteraufgaben", + "comments": "{{count}} Kommentar", + "comments_plural": "{{count}} Kommentare", + "attachments": "{{count}} Anhang", + "attachments_plural": "{{count}} Anhänge", + "subscribers": "Aufgabe hat Abonnenten", + "dependencies": "Aufgabe hat Abhängigkeiten", + "recurring": "Wiederkehrende Aufgabe" + } + } } diff --git a/worklenz-frontend/public/locales/en/task-management.json b/worklenz-frontend/public/locales/en/task-management.json index 2d21c746..99b8b0d5 100644 --- a/worklenz-frontend/public/locales/en/task-management.json +++ b/worklenz-frontend/public/locales/en/task-management.json @@ -18,6 +18,9 @@ "changeCategory": "Change Category", "clickToEditGroupName": "Click to edit group name", "enterGroupName": "Enter group name", + "todo": "To Do", + "inProgress": "Doing", + "done": "Done", "indicators": { "tooltips": { diff --git a/worklenz-frontend/public/locales/es/task-management.json b/worklenz-frontend/public/locales/es/task-management.json index 1c80304c..e28569d1 100644 --- a/worklenz-frontend/public/locales/es/task-management.json +++ b/worklenz-frontend/public/locales/es/task-management.json @@ -17,5 +17,22 @@ "renamePhase": "Renombrar Fase", "changeCategory": "Cambiar Categoría", "clickToEditGroupName": "Haz clic para editar el nombre del grupo", - "enterGroupName": "Ingresa el nombre del grupo" + "enterGroupName": "Ingresa el nombre del grupo", + "todo": "Por Hacer", + "inProgress": "En Progreso", + "done": "Hecho", + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtarea", + "subtasks_plural": "{{count}} subtareas", + "comments": "{{count}} comentario", + "comments_plural": "{{count}} comentarios", + "attachments": "{{count}} adjunto", + "attachments_plural": "{{count}} adjuntos", + "subscribers": "La tarea tiene suscriptores", + "dependencies": "La tarea tiene dependencias", + "recurring": "Tarea recurrente" + } + } } diff --git a/worklenz-frontend/public/locales/pt/task-management.json b/worklenz-frontend/public/locales/pt/task-management.json index 946b3162..24beb53c 100644 --- a/worklenz-frontend/public/locales/pt/task-management.json +++ b/worklenz-frontend/public/locales/pt/task-management.json @@ -17,5 +17,22 @@ "renamePhase": "Renomear Fase", "changeCategory": "Alterar Categoria", "clickToEditGroupName": "Clique para editar o nome do grupo", - "enterGroupName": "Digite o nome do grupo" + "enterGroupName": "Digite o nome do grupo", + "todo": "A Fazer", + "inProgress": "Em Andamento", + "done": "Concluído", + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtarefa", + "subtasks_plural": "{{count}} subtarefas", + "comments": "{{count}} comentário", + "comments_plural": "{{count}} comentários", + "attachments": "{{count}} anexo", + "attachments_plural": "{{count}} anexos", + "subscribers": "Tarefa tem assinantes", + "dependencies": "Tarefa tem dependências", + "recurring": "Tarefa recorrente" + } + } } diff --git a/worklenz-frontend/public/locales/zh/task-management.json b/worklenz-frontend/public/locales/zh/task-management.json index 341ecc64..7f185e34 100644 --- a/worklenz-frontend/public/locales/zh/task-management.json +++ b/worklenz-frontend/public/locales/zh/task-management.json @@ -18,6 +18,9 @@ "changeCategory": "更改类别", "clickToEditGroupName": "点击编辑组名称", "enterGroupName": "输入组名称", + "todo": "待办", + "inProgress": "进行中", + "done": "已完成", "indicators": { "tooltips": { diff --git a/worklenz-frontend/src/components/task-list-v2/GroupProgressBar.tsx b/worklenz-frontend/src/components/task-list-v2/GroupProgressBar.tsx index fd280bdf..a8623d27 100644 --- a/worklenz-frontend/src/components/task-list-v2/GroupProgressBar.tsx +++ b/worklenz-frontend/src/components/task-list-v2/GroupProgressBar.tsx @@ -38,26 +38,26 @@ const GroupProgressBar: React.FC = ({ {/* Compact progress bar */}
- {/* Todo section - light gray */} + {/* Todo section - light green */} {todoProgress > 0 && (
)} - {/* Doing section - blue */} + {/* Doing section - medium green */} {doingProgress > 0 && (
)} - {/* Done section - green */} + {/* Done section - dark green */} {doneProgress > 0 && (
@@ -69,19 +69,19 @@ const GroupProgressBar: React.FC = ({
{todoProgress > 0 && (
)} {doingProgress > 0 && (
)} {doneProgress > 0 && (
)} diff --git a/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx b/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx index 0b25be2e..d3f2e5b7 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskGroupHeader.tsx @@ -9,7 +9,7 @@ import { getContrastColor } from '@/utils/colorUtils'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { selectSelectedTaskIds, selectTask, deselectTask } from '@/features/task-management/selection.slice'; -import { selectGroups, fetchTasksV3 } from '@/features/task-management/task-management.slice'; +import { selectGroups, fetchTasksV3, selectAllTasksArray } from '@/features/task-management/task-management.slice'; import { selectCurrentGrouping } from '@/features/task-management/grouping.slice'; import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service'; @@ -43,8 +43,9 @@ const TaskGroupHeader: React.FC = ({ group, isCollapsed, o const dispatch = useAppDispatch(); const selectedTaskIds = useAppSelector(selectSelectedTaskIds); const groups = useAppSelector(selectGroups); + const allTasks = useAppSelector(selectAllTasksArray); const currentGrouping = useAppSelector(selectCurrentGrouping); - const { statusCategories } = useAppSelector(state => state.taskStatusReducer); + const { statusCategories, status: statusList } = useAppSelector(state => state.taskStatusReducer); const { trackMixpanelEvent } = useMixpanelTracking(); const { isOwnerOrAdmin } = useAuthService(); @@ -67,6 +68,74 @@ const TaskGroupHeader: React.FC = ({ group, isCollapsed, o return currentGroup?.taskIds || []; }, [currentGroup]); + // Calculate group progress values dynamically + const groupProgressValues = useMemo(() => { + if (!currentGroup || !allTasks.length) { + return { todoProgress: 0, doingProgress: 0, doneProgress: 0 }; + } + + const tasksInCurrentGroup = currentGroup.taskIds + .map(taskId => allTasks.find(task => task.id === taskId)) + .filter(task => task !== undefined); + + if (tasksInCurrentGroup.length === 0) { + return { todoProgress: 0, doingProgress: 0, doneProgress: 0 }; + } + + // If we're grouping by status, show progress based on task completion + if (currentGrouping === 'status') { + // For status grouping, calculate based on task progress values + const progressStats = tasksInCurrentGroup.reduce((acc, task) => { + const progress = task.progress || 0; + if (progress === 0) { + acc.todo += 1; + } else if (progress === 100) { + acc.done += 1; + } else { + acc.doing += 1; + } + return acc; + }, { todo: 0, doing: 0, done: 0 }); + + const totalTasks = tasksInCurrentGroup.length; + + return { + todoProgress: totalTasks > 0 ? Math.round((progressStats.todo / totalTasks) * 100) : 0, + doingProgress: totalTasks > 0 ? Math.round((progressStats.doing / totalTasks) * 100) : 0, + doneProgress: totalTasks > 0 ? Math.round((progressStats.done / totalTasks) * 100) : 0, + }; + } else { + // For priority/phase grouping, show progress based on status distribution + // Use a simplified approach based on status names and common patterns + const statusCounts = tasksInCurrentGroup.reduce((acc, task) => { + // Find the status by ID first + const statusInfo = statusList.find(s => s.id === task.status); + const statusName = statusInfo?.name?.toLowerCase() || task.status?.toLowerCase() || ''; + + // Categorize based on common status name patterns + if (statusName.includes('todo') || statusName.includes('to do') || statusName.includes('pending') || statusName.includes('open') || statusName.includes('backlog')) { + acc.todo += 1; + } else if (statusName.includes('doing') || statusName.includes('progress') || statusName.includes('active') || statusName.includes('working') || statusName.includes('development')) { + acc.doing += 1; + } else if (statusName.includes('done') || statusName.includes('completed') || statusName.includes('finished') || statusName.includes('closed') || statusName.includes('resolved')) { + acc.done += 1; + } else { + // Default unknown statuses to "doing" (in progress) + acc.doing += 1; + } + return acc; + }, { todo: 0, doing: 0, done: 0 }); + + const totalTasks = tasksInCurrentGroup.length; + + return { + todoProgress: totalTasks > 0 ? Math.round((statusCounts.todo / totalTasks) * 100) : 0, + doingProgress: totalTasks > 0 ? Math.round((statusCounts.doing / totalTasks) * 100) : 0, + doneProgress: totalTasks > 0 ? Math.round((statusCounts.done / totalTasks) * 100) : 0, + }; + } + }, [currentGroup, allTasks, statusList, currentGrouping]); + // Calculate selection state for this group const { isAllSelected, isPartiallySelected } = useMemo(() => { if (tasksInGroup.length === 0) { @@ -369,7 +438,7 @@ const TaskGroupHeader: React.FC = ({ group, isCollapsed, o {/* Progress Bar - sticky to the right edge during horizontal scroll */} {(currentGrouping === 'priority' || currentGrouping === 'phase') && - (group.todo_progress || group.doing_progress || group.done_progress) && ( + (groupProgressValues.todoProgress || groupProgressValues.doingProgress || groupProgressValues.doneProgress) && (
= ({ group, isCollapsed, o }} >
diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx index 7ef747ed..624ff623 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx @@ -65,6 +65,7 @@ import CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-t import AddTaskRow from './components/AddTaskRow'; import { AddCustomColumnButton, CustomColumnHeader } from './components/CustomColumnComponents'; import TaskListSkeleton from './components/TaskListSkeleton'; +import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-subtask-drawer/convert-to-subtask-drawer'; // Hooks and utilities import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; @@ -766,6 +767,9 @@ const TaskListV2Section: React.FC = () => { {/* Custom Column Modal */} {createPortal(, document.body, 'custom-column-modal')} + + {/* Convert To Subtask Drawer */} + {createPortal(, document.body, 'convert-to-subtask-drawer')}
); diff --git a/worklenz-frontend/src/components/task-list-v2/components/TaskContextMenu.tsx b/worklenz-frontend/src/components/task-list-v2/components/TaskContextMenu.tsx index 0982dafa..28b0e119 100644 --- a/worklenz-frontend/src/components/task-list-v2/components/TaskContextMenu.tsx +++ b/worklenz-frontend/src/components/task-list-v2/components/TaskContextMenu.tsx @@ -16,8 +16,8 @@ import { toggleTaskExpansion, updateTaskAssignees, } from '@/features/task-management/task-management.slice'; -import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice'; -import { setConvertToSubtaskDrawerOpen } from '@/features/task-drawer/task-drawer.slice'; +import { deselectAll, selectTasks } from '@/features/projects/bulkActions/bulkActionSlice'; +import { setConvertToSubtaskDrawerOpen } from '@/features/tasks/tasks.slice'; import { useTranslation } from 'react-i18next'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { @@ -412,7 +412,45 @@ const TaskContextMenu: React.FC = ({ key: 'convertToSubTask', label: (