import React, { memo, useMemo, useState, useRef, useEffect } from 'react'; import { useAppSelector } from '@/hooks/useAppSelector'; import { ITaskListGroup } from '@/types/tasks/taskList.types'; import TaskCard from './TaskCard'; import { themeWiseColor } from '@/utils/themeWiseColor'; import EnhancedKanbanCreateTaskCard from '../EnhancedKanbanCreateTaskCard'; import { useTranslation } from 'react-i18next'; import { useAuthService } from '@/hooks/useAuth'; import useIsProjectManager from '@/hooks/useIsProjectManager'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types'; import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; import { fetchStatuses } from '@/features/taskAttributes/taskStatusSlice'; import logger from '@/utils/errorLogger'; import { evt_project_board_column_setting_click } from '@/shared/worklenz-analytics-events'; import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service'; import { ITaskPhase } from '@/types/tasks/taskPhase.types'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { deleteStatusToggleDrawer, seletedStatusCategory, } from '@/features/projects/status/DeleteStatusSlice'; import { fetchEnhancedKanbanGroups, IGroupBy, } from '@/features/enhanced-kanban/enhanced-kanban.slice'; interface KanbanGroupProps { group: ITaskListGroup; onGroupDragStart: (e: React.DragEvent, groupId: string) => void; onGroupDragOver: (e: React.DragEvent) => void; onGroupDrop: (e: React.DragEvent, groupId: string) => void; onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; hoveredTaskIdx: number | null; hoveredGroupId: string | null; } const KanbanGroup: React.FC = memo(({ group, onGroupDragStart, onGroupDragOver, onGroupDrop, onTaskDragStart, onTaskDragOver, onTaskDrop, hoveredTaskIdx, hoveredGroupId }) => { const [isHover, setIsHover] = useState(false); const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin(); const [isEditable, setIsEditable] = useState(false); const isProjectManager = useIsProjectManager(); const [isLoading, setIsLoading] = useState(false); const [name, setName] = useState(group.name); const inputRef = useRef(null); const [editName, setEdit] = useState(group.name); const [isEllipsisActive, setIsEllipsisActive] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const dropdownRef = useRef(null); const themeMode = useAppSelector(state => state.themeReducer.mode); const dispatch = useAppDispatch(); const { projectId } = useAppSelector(state => state.projectReducer); const { groupBy } = useAppSelector(state => state.enhancedKanbanReducer); const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer); const { trackMixpanelEvent } = useMixpanelTracking(); const [showNewCardTop, setShowNewCardTop] = useState(false); const [showNewCardBottom, setShowNewCardBottom] = useState(false); const { t } = useTranslation('kanban-board'); const headerBackgroundColor = useMemo(() => { if (themeMode === 'dark') { return group.color_code_dark || group.color_code || '#1e1e1e'; } return group.color_code || '#f5f5f5'; }, [themeMode, group.color_code, group.color_code_dark]); const getUniqueSectionName = (baseName: string): string => { // Check if the base name already exists const existingNames = status.map(status => status.name?.toLowerCase()); if (!existingNames.includes(baseName.toLowerCase())) { return baseName; } // If the base name exists, add a number suffix let counter = 1; let newName = `${baseName.trim()} (${counter})`; while (existingNames.includes(newName.toLowerCase())) { counter++; newName = `${baseName.trim()} (${counter})`; } return newName; }; const updateStatus = async (category = group.category_id ?? null) => { if (!category || !projectId || !group.id) return; const sectionName = getUniqueSectionName(name); const body: ITaskStatusUpdateModel = { name: sectionName, project_id: projectId, category_id: category, }; const res = await statusApiService.updateStatus(group.id, body, projectId); if (res.done) { dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchStatuses(projectId)); setName(sectionName); } else { setName(editName); logger.error('Error updating status', res.message); } }; const handleChange = async (e: React.ChangeEvent) => { const taskName = e.target.value; setName(taskName); }; const handleBlur = async () => { setIsEditable(false); if (name === editName) return; if (name === t('untitledSection')) { dispatch(fetchEnhancedKanbanGroups(projectId ?? '')); } if (!projectId || !group.id) return; if (groupBy === IGroupBy.STATUS) { await updateStatus(); } if (groupBy === IGroupBy.PHASE) { const body = { id: group.id, name: name, }; const res = await phasesApiService.updateNameOfPhase( group.id, body as ITaskPhase, projectId ); if (res.done) { trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' }); dispatch(fetchEnhancedKanbanGroups(projectId)); } } }; const handlePressEnter = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { setShowNewCardTop(true); setShowNewCardBottom(false); handleBlur(); } }; const handleDeleteSection = async () => { if (!projectId || !group.id) return; try { if (groupBy === IGroupBy.STATUS) { const replacingStatusId = ''; const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId); if (res.message === 'At least one status should exists under each category.') return; if (res.done) { dispatch(fetchEnhancedKanbanGroups(projectId)); } else { dispatch( seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '', }) ); dispatch(deleteStatusToggleDrawer()); } } else if (groupBy === IGroupBy.PHASE) { const res = await phasesApiService.deletePhaseOption(group.id, projectId); if (res.done) { dispatch(fetchEnhancedKanbanGroups(projectId)); } } } catch (error) { logger.error('Error deleting section', error); } }; const handleRename = () => { setIsEditable(true); setShowDropdown(false); setTimeout(() => { inputRef.current?.focus(); }, 100); }; const handleCategoryChange = (categoryId: string) => { updateStatus(categoryId); setShowDropdown(false); }; const handleDelete = () => { setShowDeleteConfirm(true); setShowDropdown(false); }; // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setShowDropdown(false); } }; if (showDropdown) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showDropdown]); return (
{/* Background layer - z-index 0 */}
{ e.preventDefault(); onTaskDragOver(e, group.id, null); }} onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, null); }} /> {/* Content layer - z-index 1 */}
onGroupDragStart(e, group.id)} onDragOver={onGroupDragOver} onDrop={e => onGroupDrop(e, group.id)} >
setIsHover(true)} onMouseLeave={() => setIsHover(false)} >
{ e.stopPropagation(); if ((isProjectManager || isOwnerOrAdmin) && group.name !== t('unmapped')) setIsEditable(true); }} onMouseDown={e => { e.stopPropagation(); }} > {isLoading && (
)} {isEditable ? ( { e.stopPropagation(); }} onClick={e => { e.stopPropagation(); }} /> ) : (
{ e.stopPropagation(); e.preventDefault(); }} onMouseUp={e => { e.stopPropagation(); }} onClick={e => { e.stopPropagation(); }} > {name} ({group.tasks.length})
)}
{(isOwnerOrAdmin || isProjectManager) && name !== t('unmapped') && (
{showDropdown && (
{groupBy === IGroupBy.STATUS && statusCategories && (
{t('changeCategory')}
{statusCategories.map(status => ( ))}
)} {groupBy !== IGroupBy.PRIORITY && (
)}
)}
)}
{/* Simple Delete Confirmation */} {showDeleteConfirm && (

{t('deleteConfirmationTitle')}

)}
{/* Create card at top */} {showNewCardTop && ( )} {/* If group is empty, render a drop zone */} {group.tasks.length === 0 && !showNewCardTop && !showNewCardBottom && (
{ e.preventDefault(); onTaskDragOver(e, group.id, 0); }} onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, 0); }} > {/* Drop indicator at the end of the group */} {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
)} {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && ( )}
) } {/* Drop indicator at the top of the group */} {hoveredGroupId === group.id && hoveredTaskIdx === 0 && (
)} {group.tasks.map((task, idx) => ( ))} {/* Create card at bottom */} {showNewCardBottom && ( )} {/* Footer Add Task Button */} {!showNewCardTop && !showNewCardBottom && group.tasks.length > 0 && ( )} {/* Drop indicator at the end of the group */} {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
)}
); }); KanbanGroup.displayName = 'KanbanGroup'; export default KanbanGroup;