diff --git a/worklenz-frontend/public/locales/alb/kanban-board.json b/worklenz-frontend/public/locales/alb/kanban-board.json index def705aa..48987f10 100644 --- a/worklenz-frontend/public/locales/alb/kanban-board.json +++ b/worklenz-frontend/public/locales/alb/kanban-board.json @@ -10,6 +10,17 @@ "deleteConfirmationOk": "Po", "deleteConfirmationCancel": "Anulo", + "deleteTaskTitle": "Fshi Detyrën", + "deleteTaskContent": "Jeni i sigurt që doni të fshini këtë detyrë? Kjo veprim nuk mund të zhbëhet.", + "deleteTaskConfirm": "Fshi", + "deleteTaskCancel": "Anulo", + + "deleteStatusTitle": "Fshi Statusin", + "deleteStatusContent": "Jeni i sigurt që doni të fshini këtë status? Kjo veprim nuk mund të zhbëhet.", + + "deletePhaseTitle": "Fshi Fazen", + "deletePhaseContent": "Jeni i sigurt që doni të fshini këtë fazë? Kjo veprim nuk mund të zhbëhet.", + "dueDate": "Data e përfundimit", "cancel": "Anulo", diff --git a/worklenz-frontend/public/locales/de/kanban-board.json b/worklenz-frontend/public/locales/de/kanban-board.json index 70e1f6ca..9d748e30 100644 --- a/worklenz-frontend/public/locales/de/kanban-board.json +++ b/worklenz-frontend/public/locales/de/kanban-board.json @@ -10,6 +10,17 @@ "deleteConfirmationOk": "Ja", "deleteConfirmationCancel": "Abbrechen", + "deleteTaskTitle": "Aufgabe löschen", + "deleteTaskContent": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteTaskConfirm": "Löschen", + "deleteTaskCancel": "Abbrechen", + + "deleteStatusTitle": "Status löschen", + "deleteStatusContent": "Sind Sie sicher, dass Sie diesen Status löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + + "deletePhaseTitle": "Phase löschen", + "deletePhaseContent": "Sind Sie sicher, dass Sie diese Phase löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "dueDate": "Fälligkeitsdatum", "cancel": "Abbrechen", diff --git a/worklenz-frontend/public/locales/en/kanban-board.json b/worklenz-frontend/public/locales/en/kanban-board.json index e295a6c6..018d52a9 100644 --- a/worklenz-frontend/public/locales/en/kanban-board.json +++ b/worklenz-frontend/public/locales/en/kanban-board.json @@ -10,6 +10,17 @@ "deleteConfirmationOk": "Yes", "deleteConfirmationCancel": "Cancel", + "deleteTaskTitle": "Delete Task", + "deleteTaskContent": "Are you sure you want to delete this task? This action cannot be undone.", + "deleteTaskConfirm": "Delete", + "deleteTaskCancel": "Cancel", + + "deleteStatusTitle": "Delete Status", + "deleteStatusContent": "Are you sure you want to delete this status? This action cannot be undone.", + + "deletePhaseTitle": "Delete Phase", + "deletePhaseContent": "Are you sure you want to delete this phase? This action cannot be undone.", + "dueDate": "Due date", "cancel": "Cancel", diff --git a/worklenz-frontend/public/locales/es/kanban-board.json b/worklenz-frontend/public/locales/es/kanban-board.json index 6e8d5975..c49a02ca 100644 --- a/worklenz-frontend/public/locales/es/kanban-board.json +++ b/worklenz-frontend/public/locales/es/kanban-board.json @@ -10,6 +10,17 @@ "deleteConfirmationOk": "Sí", "deleteConfirmationCancel": "Cancelar", + "deleteTaskTitle": "Eliminar tarea", + "deleteTaskContent": "¿Estás seguro de que deseas eliminar esta tarea? Esta acción no se puede deshacer.", + "deleteTaskConfirm": "Eliminar", + "deleteTaskCancel": "Cancelar", + + "deleteStatusTitle": "Eliminar estado", + "deleteStatusContent": "¿Estás seguro de que deseas eliminar este estado? Esta acción no se puede deshacer.", + + "deletePhaseTitle": "Eliminar fase", + "deletePhaseContent": "¿Estás seguro de que deseas eliminar esta fase? Esta acción no se puede deshacer.", + "dueDate": "Fecha de vencimiento", "cancel": "Cancelar", diff --git a/worklenz-frontend/public/locales/pt/kanban-board.json b/worklenz-frontend/public/locales/pt/kanban-board.json index a2034daa..f290258e 100644 --- a/worklenz-frontend/public/locales/pt/kanban-board.json +++ b/worklenz-frontend/public/locales/pt/kanban-board.json @@ -10,6 +10,17 @@ "deleteConfirmationOk": "Sim", "deleteConfirmationCancel": "Cancelar", + "deleteTaskTitle": "Excluir Tarefa", + "deleteTaskContent": "Tem certeza de que deseja excluir esta tarefa? Esta ação não pode ser desfeita.", + "deleteTaskConfirm": "Excluir", + "deleteTaskCancel": "Cancelar", + + "deleteStatusTitle": "Excluir Status", + "deleteStatusContent": "Tem certeza de que deseja excluir este status? Esta ação não pode ser desfeita.", + + "deletePhaseTitle": "Excluir Fase", + "deletePhaseContent": "Tem certeza de que deseja excluir esta fase? Esta ação não pode ser desfeita.", + "dueDate": "Data de vencimento", "cancel": "Cancelar", diff --git a/worklenz-frontend/public/locales/zh/kanban-board.json b/worklenz-frontend/public/locales/zh/kanban-board.json index 7b72c5d5..4c51dfa8 100644 --- a/worklenz-frontend/public/locales/zh/kanban-board.json +++ b/worklenz-frontend/public/locales/zh/kanban-board.json @@ -15,5 +15,13 @@ "assignToMe": "分配给我", "archive": "归档", "newTaskNamePlaceholder": "写一个任务名称", - "newSubtaskNamePlaceholder": "写一个子任务名称" + "newSubtaskNamePlaceholder": "写一个子任务名称", + "deleteTaskTitle": "删除任务", + "deleteTaskContent": "您确定要删除此任务吗?此操作无法撤销。", + "deleteTaskConfirm": "删除", + "deleteTaskCancel": "取消", + "deleteStatusTitle": "删除状态", + "deleteStatusContent": "您确定要删除此状态吗?此操作无法撤销。", + "deletePhaseTitle": "删除阶段", + "deletePhaseContent": "您确定要删除此阶段吗?此操作无法撤销。" } \ No newline at end of file diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx index ae1e3c35..8431073f 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx @@ -118,6 +118,26 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project setDragType(null); }; + // Utility to recalculate all task orders for all groups + function getAllTaskUpdates(allGroups, groupBy) { + const taskUpdates = []; + let currentSortOrder = 0; + for (const group of allGroups) { + for (const task of group.tasks) { + const update = { + task_id: task.id, + sort_order: currentSortOrder, + }; + if (groupBy === 'status') update.status_id = group.id; + else if (groupBy === 'priority') update.priority_id = group.id; + else if (groupBy === 'phase') update.phase_id = group.id; + taskUpdates.push(update); + currentSortOrder++; + } + } + return taskUpdates; + } + // Task drag handlers const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => { setDraggedTaskId(taskId); @@ -168,6 +188,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project let insertIdx = hoveredTaskIdx; // Handle same group reordering + let newTaskGroups = [...taskGroups]; if (sourceGroup.id === targetGroup.id) { // Create a single updated array for the same group const updatedTasks = [...sourceGroup.tasks]; @@ -201,6 +222,8 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project updatedSourceTasks: updatedTasks, updatedTargetTasks: updatedTasks, }) as any); + // Update newTaskGroups for socket emit + newTaskGroups = newTaskGroups.map(g => g.id === sourceGroup.id ? { ...g, tasks: updatedTasks } : g); } else { // Handle cross-group reordering const updatedSourceTasks = [...sourceGroup.tasks]; @@ -229,34 +252,33 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project updatedSourceTasks, updatedTargetTasks, }) as any); + // Update newTaskGroups for socket emit + newTaskGroups = newTaskGroups.map(g => { + if (g.id === sourceGroup.id) return { ...g, tasks: updatedSourceTasks }; + if (g.id === targetGroup.id) return { ...g, tasks: updatedTargetTasks }; + return g; + }); } - // Socket emit for task order + // Socket emit for full task order if (socket && projectId && teamId && movedTask) { - let toSortOrder = -1; - let toLastIndex = false; - if (insertIdx === targetGroup.tasks.length) { - toSortOrder = -1; - toLastIndex = true; - } else if (targetGroup.tasks[insertIdx]) { - const sortOrder = targetGroup.tasks[insertIdx].sort_order; - toSortOrder = typeof sortOrder === 'number' ? sortOrder : 0; - toLastIndex = false; - } else if (targetGroup.tasks.length > 0) { - const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order; - toSortOrder = typeof lastSortOrder === 'number' ? lastSortOrder : 0; - toLastIndex = false; - } + const taskUpdates = getAllTaskUpdates(newTaskGroups, groupBy); socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { project_id: projectId, - from_index: movedTask.sort_order ?? 0, - to_index: toSortOrder, - to_last_index: toLastIndex, + group_by: groupBy || 'status', + task_updates: taskUpdates, from_group: sourceGroup.id, to_group: targetGroup.id, - group_by: groupBy || 'status', - task: movedTask, team_id: teamId, + from_index: taskIdx, + to_index: insertIdx, + to_last_index: insertIdx === (targetGroup.id === sourceGroup.id ? newTaskGroups.find(g => g.id === targetGroup.id)?.tasks.length - 1 : targetGroup.tasks.length), + task: { + id: movedTask.id, + project_id: movedTask.project_id || projectId, + status: movedTask.status || '', + priority: movedTask.priority || '', + } }); // Emit progress update if status changed diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx index f5e443d2..9338b48b 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx @@ -25,6 +25,7 @@ import { IGroupBy, } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import { createPortal } from 'react-dom'; +import { Modal } from 'antd'; // Simple Portal component const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -218,7 +219,42 @@ const KanbanGroup: React.FC = memo(({ }; const handleDelete = () => { - setShowDeleteConfirm(true); + if (groupBy === IGroupBy.STATUS) { + Modal.confirm({ + title: t('deleteStatusTitle'), + content: t('deleteStatusContent'), + okText: t('deleteTaskConfirm'), + okType: 'danger', + cancelText: t('deleteTaskCancel'), + centered: true, + onOk: async () => { + await handleDeleteSection(); + }, + }); + } else if (groupBy === IGroupBy.PHASE) { + Modal.confirm({ + title: t('deletePhaseTitle'), + content: t('deletePhaseContent'), + okText: t('deleteTaskConfirm'), + okType: 'danger', + cancelText: t('deleteTaskCancel'), + centered: true, + onOk: async () => { + await handleDeleteSection(); + }, + }); + } else { + Modal.confirm({ + title: t('deleteConfirmationTitle'), + okText: t('deleteTaskConfirm'), + okType: 'danger', + cancelText: t('deleteTaskCancel'), + centered: true, + onOk: async () => { + await handleDeleteSection(); + }, + }); + } setShowDropdown(false); }; @@ -419,56 +455,7 @@ const KanbanGroup: React.FC = memo(({ {/* Simple Delete Confirmation */} - {showDeleteConfirm && ( - -
setShowDeleteConfirm(false)} - > -
e.stopPropagation()} - > -
-
-
- - - -
-
-

- {t('deleteConfirmationTitle')} -

-
-
-
- - -
-
-
-
-
- )} + {/* Portal-based confirmation removed, now handled by Modal.confirm */}
{/* Create card at top */} {showNewCardTop && ( diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index bf499a12..daa8793c 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -14,8 +14,11 @@ import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import { getUserSession } from '@/utils/session-helper'; import { themeWiseColor } from '@/utils/themeWiseColor'; -import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice'; +import { toggleTaskExpansion, fetchBoardSubTasks, deleteTask as deleteKanbanTask, updateEnhancedKanbanSubtask } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import TaskProgressCircle from './TaskProgressCircle'; +import { Button, Modal } from 'antd'; +import { DeleteOutlined } from '@ant-design/icons'; +import { tasksApiService } from '@/api/tasks/tasks.api.service'; // Simple Portal component const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -70,6 +73,9 @@ const TaskCard: React.FC = memo(({ const d = selectedDate || new Date(); return new Date(d.getFullYear(), d.getMonth(), 1); }); + const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number }>({ visible: false, x: 0, y: 0 }); + const contextMenuRef = useRef(null); + const [selectedTask, setSelectedTask] = useState(null); useEffect(() => { setSelectedDate(task.end_date ? new Date(task.end_date) : null); @@ -102,6 +108,21 @@ const TaskCard: React.FC = memo(({ } }, [showDatePicker]); + // Hide context menu on click elsewhere + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { + setContextMenu({ ...contextMenu, visible: false }); + } + }; + if (contextMenu.visible) { + document.addEventListener('mousedown', handleClick); + } + return () => { + document.removeEventListener('mousedown', handleClick); + }; + }, [contextMenu]); + const handleCardClick = useCallback((e: React.MouseEvent, id: string) => { e.stopPropagation(); dispatch(setSelectedTaskId(id)); @@ -178,6 +199,48 @@ const TaskCard: React.FC = memo(({ handleSubTaskExpand(); }, [handleSubTaskExpand]); + // Delete logic (similar to task-drawer-header) + const handleDeleteTask = async (task: IProjectTask | null) => { + if (!task || !task.id) return; + Modal.confirm({ + title: t('deleteTaskTitle'), + content: t('deleteTaskContent'), + okText: t('deleteTaskConfirm'), + okType: 'danger', + cancelText: t('deleteTaskCancel'), + centered: true, + onOk: async () => { + if (!task.id) return; + const res = await tasksApiService.deleteTask(task.id); + if (res.done) { + dispatch(setSelectedTaskId(null)); + if (task.is_sub_task) { + dispatch(updateEnhancedKanbanSubtask({ + sectionId: '', + subtask: { id: task.id , parent_task_id: task.parent_task_id || '', manual_progress: false }, + mode: 'delete', + })); + } else { + dispatch(deleteKanbanTask(task.id)); + } + dispatch(setShowTaskDrawer(false)); + if (task.parent_task_id) { + socket?.emit( + SocketEvents.GET_TASK_PROGRESS.toString(), + task.parent_task_id + ); + } + } + setContextMenu({ visible: false, x: 0, y: 0 }); + setSelectedTask(null); + }, + onCancel: () => { + setContextMenu({ visible: false, x: 0, y: 0 }); + setSelectedTask(null); + }, + }); + }; + // Calendar rendering helpers const year = calendarMonth.getFullYear(); const month = calendarMonth.getMonth(); @@ -202,7 +265,37 @@ const TaskCard: React.FC = memo(({ return ( <> -
+ {/* Context menu for delete */} + {contextMenu.visible && ( +
+ +
+ )} +
{/* Progress circle at top right */}
@@ -221,6 +314,11 @@ const TaskCard: React.FC = memo(({ onDrop={e => onTaskDrop(e, groupId, idx)} onDragEnd={onDragEnd} // <-- add this onClick={e => handleCardClick(e, task.id!)} + onContextMenu={e => { + e.preventDefault(); + setContextMenu({ visible: true, x: e.clientX, y: e.clientY }); + setSelectedTask(task); + }} >
@@ -447,7 +545,14 @@ const TaskCard: React.FC = memo(({ {!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
    {task.sub_tasks.map(sub => ( -
  • handleCardClick(e, sub.id!)} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800"> +
  • handleCardClick(e, sub.id!)} + className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800" + onContextMenu={e => { + e.preventDefault(); + setContextMenu({ visible: true, x: e.clientX, y: e.clientY }); + setSelectedTask(sub); + }}> {sub.priority_color || sub.priority_color_dark ? (