From 31891fae6ea836c326c5902979a3b09755c1bd77 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 4 Jul 2025 12:16:10 +0530 Subject: [PATCH 1/6] feat(task-management): integrate date picker functionality in TaskCard component - Added a date picker to the TaskCard for selecting and updating task due dates. - Implemented real-time updates for due date changes using socket communication. - Enhanced UI with a custom calendar popup and improved date selection options. - Updated localization files to include new strings related to date management in multiple languages. --- .../public/locales/alb/kanban-board.json | 7 +- .../public/locales/de/kanban-board.json | 7 +- .../public/locales/en/kanban-board.json | 7 +- .../public/locales/es/kanban-board.json | 7 +- .../public/locales/pt/kanban-board.json | 7 +- .../EnhancedKanbanBoardNativeDnD/TaskCard.tsx | 258 +++++++++++++++++- 6 files changed, 284 insertions(+), 9 deletions(-) diff --git a/worklenz-frontend/public/locales/alb/kanban-board.json b/worklenz-frontend/public/locales/alb/kanban-board.json index c7306620..def705aa 100644 --- a/worklenz-frontend/public/locales/alb/kanban-board.json +++ b/worklenz-frontend/public/locales/alb/kanban-board.json @@ -21,5 +21,10 @@ "newTaskNamePlaceholder": "Shkruaj emrin e detyrës", "newSubtaskNamePlaceholder": "Shkruaj emrin e nëndetyrës", "untitledSection": "Seksion pa titull", - "unmapped": "Pa hartë" + "unmapped": "Pa hartë", + "clickToChangeDate": "Klikoni për të ndryshuar datën", + "noDueDate": "Pa datë përfundimi", + "save": "Ruaj", + "clear": "Pastro", + "nextWeek": "Javën e ardhshme" } diff --git a/worklenz-frontend/public/locales/de/kanban-board.json b/worklenz-frontend/public/locales/de/kanban-board.json index 3c8e83ae..70e1f6ca 100644 --- a/worklenz-frontend/public/locales/de/kanban-board.json +++ b/worklenz-frontend/public/locales/de/kanban-board.json @@ -21,5 +21,10 @@ "newTaskNamePlaceholder": "Aufgabenname eingeben", "newSubtaskNamePlaceholder": "Unteraufgabenname eingeben", "untitledSection": "Unbenannter Abschnitt", - "unmapped": "Nicht zugeordnet" + "unmapped": "Nicht zugeordnet", + "clickToChangeDate": "Klicken Sie, um das Datum zu ändern", + "noDueDate": "Kein Fälligkeitsdatum", + "save": "Speichern", + "clear": "Löschen", + "nextWeek": "Nächste Woche" } diff --git a/worklenz-frontend/public/locales/en/kanban-board.json b/worklenz-frontend/public/locales/en/kanban-board.json index 20db06d0..bc9b372a 100644 --- a/worklenz-frontend/public/locales/en/kanban-board.json +++ b/worklenz-frontend/public/locales/en/kanban-board.json @@ -21,5 +21,10 @@ "newTaskNamePlaceholder": "Write a task Name", "newSubtaskNamePlaceholder": "Write a subtask Name", "untitledSection": "Untitled section", - "unmapped": "Unmapped" + "unmapped": "Unmapped", + "clickToChangeDate": "Click to change date", + "noDueDate": "No due date", + "save": "Save", + "clear": "Clear", + "nextWeek": "Next week" } diff --git a/worklenz-frontend/public/locales/es/kanban-board.json b/worklenz-frontend/public/locales/es/kanban-board.json index b143d55f..6e8d5975 100644 --- a/worklenz-frontend/public/locales/es/kanban-board.json +++ b/worklenz-frontend/public/locales/es/kanban-board.json @@ -21,5 +21,10 @@ "newTaskNamePlaceholder": "Escribe un nombre de tarea", "newSubtaskNamePlaceholder": "Escribe un nombre de subtarea", "untitledSection": "Sección sin título", - "unmapped": "Sin asignar" + "unmapped": "Sin asignar", + "clickToChangeDate": "Haz clic para cambiar la fecha", + "noDueDate": "Sin fecha de vencimiento", + "save": "Guardar", + "clear": "Limpiar", + "nextWeek": "Próxima semana" } diff --git a/worklenz-frontend/public/locales/pt/kanban-board.json b/worklenz-frontend/public/locales/pt/kanban-board.json index cb5df9e1..a2034daa 100644 --- a/worklenz-frontend/public/locales/pt/kanban-board.json +++ b/worklenz-frontend/public/locales/pt/kanban-board.json @@ -21,5 +21,10 @@ "newTaskNamePlaceholder": "Escreva um nome de tarefa", "newSubtaskNamePlaceholder": "Escreva um nome de subtarefa", "untitledSection": "Seção sem título", - "unmapped": "Não mapeado" + "unmapped": "Não mapeado", + "clickToChangeDate": "Clique para alterar a data", + "noDueDate": "Sem data de vencimento", + "save": "Salvar", + "clear": "Limpar", + "nextWeek": "Próxima semana" } diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index 237cd80b..056562a4 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useState, useRef, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '@/app/store'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; @@ -8,6 +8,17 @@ import { useTranslation } from 'react-i18next'; import AvatarGroup from '@/components/AvatarGroup'; import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector'; import { format } from 'date-fns'; +import logger from '@/utils/errorLogger'; +import { createPortal } from 'react-dom'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; +import { getUserSession } from '@/utils/session-helper'; + +// Simple Portal component +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const portalRoot = document.getElementById('portal-root') || document.body; + return createPortal(children, portalRoot); +}; interface TaskCardProps { task: IProjectTask; @@ -19,6 +30,14 @@ interface TaskCardProps { idx: number; } +function getDaysInMonth(year: number, month: number) { + return new Date(year, month + 1, 0).getDate(); +} + +function getFirstDayOfWeek(year: number, month: number) { + return new Date(year, month, 1).getDay(); +} + const TaskCard: React.FC = memo(({ task, onTaskDragStart, @@ -28,19 +47,133 @@ const TaskCard: React.FC = memo(({ isDropIndicator, idx }) => { + const { socket } = useSocket(); const themeMode = useSelector((state: RootState) => state.themeReducer.mode); + const { projectId } = useSelector((state: RootState) => state.projectReducer); const background = themeMode === 'dark' ? '#23272f' : '#fff'; const color = themeMode === 'dark' ? '#fff' : '#23272f'; const dispatch = useAppDispatch(); const { t } = useTranslation('kanban-board'); + + const [showDatePicker, setShowDatePicker] = useState(false); + const [selectedDate, setSelectedDate] = useState( + task.end_date ? new Date(task.end_date) : null + ); + const [isUpdating, setIsUpdating] = useState(false); + const datePickerRef = useRef(null); + const dateButtonRef = useRef(null); + const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null); + const [calendarMonth, setCalendarMonth] = useState(() => { + const d = selectedDate || new Date(); + return new Date(d.getFullYear(), d.getMonth(), 1); + }); + + // Close date picker when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) { + setShowDatePicker(false); + } + }; + + if (showDatePicker) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDatePicker]); + + useEffect(() => { + if (showDatePicker && dateButtonRef.current) { + const rect = dateButtonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + }); + } + }, [showDatePicker]); const handleCardClick = useCallback((e: React.MouseEvent, id: string) => { - // Prevent the event from propagating to parent elements e.stopPropagation(); dispatch(setSelectedTaskId(id)); dispatch(setShowTaskDrawer(true)); }, [dispatch]); + const handleDateClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setShowDatePicker(true); + }, []); + + const handleDateChange = useCallback( + (date: Date | null) => { + if (!task.id || !projectId) return; + setIsUpdating(true); + try { + setSelectedDate(date); + socket?.emit( + SocketEvents.TASK_END_DATE_CHANGE.toString(), + JSON.stringify({ + task_id: task.id, + end_date: date, + parent_task: task.parent_task_id, + time_zone: getUserSession()?.timezone_name + ? getUserSession()?.timezone_name + : Intl.DateTimeFormat().resolvedOptions().timeZone, + }) + ); + } catch (error) { + logger.error('Failed to update due date:', error); + } finally { + setIsUpdating(false); + setShowDatePicker(false); + } + }, + [task.id, projectId, socket] + ); + + const handleClearDate = useCallback(() => { + handleDateChange(null); + }, [handleDateChange]); + + const handleToday = useCallback(() => { + handleDateChange(new Date()); + }, [handleDateChange]); + + const handleTomorrow = useCallback(() => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + handleDateChange(tomorrow); + }, [handleDateChange]); + + const handleNextWeek = useCallback(() => { + const nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + handleDateChange(nextWeek); + }, [handleDateChange]); + + // Calendar rendering helpers + const year = calendarMonth.getFullYear(); + const month = calendarMonth.getMonth(); + const daysInMonth = getDaysInMonth(year, month); + const firstDayOfWeek = (getFirstDayOfWeek(year, month) + 6) % 7; // Make Monday first + const today = new Date(); + + const weeks: (Date | null)[][] = []; + let week: (Date | null)[] = Array(firstDayOfWeek).fill(null); + for (let day = 1; day <= daysInMonth; day++) { + week.push(new Date(year, month, day)); + if (week.length === 7) { + weeks.push(week); + week = []; + } + } + if (week.length > 0) { + while (week.length < 7) week.push(null); + weeks.push(week); + } + return ( <> {isDropIndicator && ( @@ -96,8 +229,125 @@ const TaskCard: React.FC = memo(({
-
- {task.end_date ? format(new Date(task.end_date), 'MMM d, yyyy') : ''} +
+
+ {isUpdating ? ( +
+ ) : ( + selectedDate ? format(selectedDate, 'MMM d, yyyy') : t('noDueDate') + )} +
+ {/* Custom Calendar Popup */} + {showDatePicker && dropdownPosition && ( + +
e.stopPropagation()} + > +
+ + + {calendarMonth.toLocaleString('default', { month: 'long' })} {year} + + +
+
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => ( +
{d}
+ ))} + {weeks.map((week, i) => ( + + {week.map((date, j) => { + const isSelected = date && selectedDate && date.toDateString() === selectedDate.toDateString(); + const isToday = date && date.toDateString() === today.toDateString(); + return ( + + ); + })} + + ))} +
+
+ + +
+
+ + +
+
+
+ )}
Date: Fri, 4 Jul 2025 12:22:01 +0530 Subject: [PATCH 2/6] style(TaskCard): refine date picker UI and adjust styling for better usability - Reduced dimensions and padding of the date picker for a more compact design. - Updated button sizes and text styles for improved readability and interaction. - Adjusted layout spacing to enhance overall visual consistency and user experience. --- .../EnhancedKanbanBoardNativeDnD/TaskCard.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index 056562a4..2755abbd 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -253,7 +253,7 @@ const TaskCard: React.FC = memo(({ {showDatePicker && dropdownPosition && (
= memo(({ ref={datePickerRef} onClick={e => e.stopPropagation()} > -
+
- + {calendarMonth.toLocaleString('default', { month: 'long' })} {year}
-
+
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => (
{d}
))} @@ -294,7 +294,7 @@ const TaskCard: React.FC = memo(({
-
+
-
+
+ + {calendarMonth.toLocaleString('default', { month: 'long' })} {year} + + +
+
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => ( +
{d}
+ ))} + {weeks.map((week, i) => ( + + {week.map((date, j) => { + const isSelected = date && selectedDate && date.toDateString() === selectedDate.toDateString(); + const isToday = date && date.toDateString() === today.toDateString(); + return ( + + ); + })} + + ))} +
+
+ + +
+
+ + +
+
+ )}
- {/* Custom Calendar Popup */} - {showDatePicker && dropdownPosition && ( - -
+ + + {(task.sub_tasks_count ?? 0) > 0 && ( + - - {calendarMonth.toLocaleString('default', { month: 'long' })} {year} - - -
-
- {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => ( -
{d}
- ))} - {weeks.map((week, i) => ( - - {week.map((date, j) => { - const isSelected = date && selectedDate && date.toDateString() === selectedDate.toDateString(); - const isToday = date && date.toDateString() === today.toDateString(); - return ( - - ); - })} - - ))} -
-
- - -
-
- - -
-
- - )} -
-
- - + {/* Fork/branch icon */} + + + + + + + + {task.sub_tasks_count ?? 0} + {/* Caret icon */} + {task.show_sub_tasks ? ( + + + + ) : ( + + + + )} + + )} +
+ {task.show_sub_tasks && ( +
+ {/* Loading state */} + {task.sub_tasks_loading && ( +
Loading...
+ )} + {/* Loaded subtasks */} + {!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && ( +
    + {task.sub_tasks.map(sub => ( +
  • + {sub.name} + {sub.names && sub.names.length > 0 && ( + + )} +
  • + ))} +
+ )} + {/* Empty state */} + {!task.sub_tasks_loading && (!Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) && ( +
{t('noSubtasks', 'No subtasks')}
+ )} +
+ )}
); From 9e1798cc3e99a5e968fa85685f4cae2e2ada6326 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 4 Jul 2025 15:41:52 +0530 Subject: [PATCH 5/6] fix(TaskCard): improve UI and interaction for subtasks - Adjusted styling for task and subtask elements to enhance visual consistency. - Updated subtask rendering to include priority color indicators based on theme mode. - Added click handling for subtask items to improve user interaction. - Refined layout and spacing for better usability and readability. --- .../EnhancedKanbanBoardNativeDnD/TaskCard.tsx | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index bfabf8ba..3dcda695 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -214,7 +214,7 @@ const TaskCard: React.FC = memo(({ onDrop={e => onTaskDrop(e, groupId, idx)} /> )} -
+
onTaskDragStart(e, task.id!, groupId)} @@ -246,10 +246,10 @@ const TaskCard: React.FC = memo(({ ))}
-
+
{task.name}
@@ -408,11 +408,11 @@ const TaskCard: React.FC = memo(({ {task.sub_tasks_count ?? 0} + fontSize: 10, + color: '#888', + whiteSpace: 'nowrap', + display: 'inline-block', + }}>{task.sub_tasks_count ?? 0} {/* Caret icon */} {task.show_sub_tasks ? ( @@ -439,8 +439,19 @@ 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"> + {sub.priority_color || sub.priority_color_dark ? ( + + ) : null} {sub.name} + + {sub.end_date ? format(new Date(sub.end_date), 'MMM d, yyyy') : ''} + {sub.names && sub.names.length > 0 && ( Date: Fri, 4 Jul 2025 17:13:56 +0530 Subject: [PATCH 6/6] feat(AssigneeSelector): add kanbanMode prop for enhanced task assignment - Introduced kanbanMode prop to AssigneeSelector and LazyAssigneeSelectorWrapper for improved functionality in Kanban view. - Updated EnhancedKanbanTaskCard and TaskCard components to utilize the new kanbanMode prop. - Adjusted task sorting logic to handle cases where sort_order may be undefined, ensuring robust behavior during task updates. --- worklenz-frontend/src/components/AssigneeSelector.tsx | 5 +++++ .../EnhancedKanbanBoardNativeDnD.tsx | 9 ++++----- .../EnhancedKanbanBoardNativeDnD/TaskCard.tsx | 2 +- .../enhanced-kanban/EnhancedKanbanTaskCard.tsx | 6 +----- .../task-management/lazy-assignee-selector.tsx | 4 +++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index 1ca0cdf8..d0467552 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -22,12 +22,14 @@ interface AssigneeSelectorProps { task: IProjectTask; groupId?: string | null; isDarkMode?: boolean; + kanbanMode?: boolean; // <-- Add this prop } const AssigneeSelector: React.FC = ({ task, groupId = null, isDarkMode = false, + kanbanMode = false, // <-- Default to false }) => { const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); @@ -212,6 +214,9 @@ const AssigneeSelector: React.FC = ({ assigneeIds: newAssigneeIds, assigneeNames: updatedAssigneeNames, })); + if (kanbanMode) { + dispatch(updateEnhancedKanbanTaskAssignees(data)); + } }); // Remove from pending changes after a short delay (optimistic) diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx index f9600aa8..e8068e07 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx @@ -216,18 +216,17 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project toSortOrder = -1; toLastIndex = true; } else if (targetGroup.tasks[insertIdx]) { - toSortOrder = typeof targetGroup.tasks[insertIdx].sort_order === 'number' - ? targetGroup.tasks[insertIdx].sort_order - : -1; + 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 : -1; + toSortOrder = typeof lastSortOrder === 'number' ? lastSortOrder : 0; toLastIndex = false; } socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { project_id: projectId, - from_index: movedTask.sort_order, + from_index: movedTask.sort_order ?? 0, to_index: toSortOrder, to_last_index: toLastIndex, from_group: sourceGroup.id, diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index 3dcda695..3a538463 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -381,7 +381,7 @@ const TaskCard: React.FC = memo(({ isDarkMode={themeMode === 'dark'} size={24} /> - + {(task.sub_tasks_count ?? 0) > 0 && (