import React, { useState, useCallback, useRef, useEffect } from 'react'; import { Modal, Form, Input, Button, Tabs, Space, Divider, Typography, Flex, DatePicker, Select } from '@/shared/antd-imports'; import { PlusOutlined, DragOutlined } from '@/shared/antd-imports'; import { useTranslation } from 'react-i18next'; import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import TaskDetailsForm from '@/components/task-drawer/shared/info-tab/task-details-form'; import AssigneeSelector from '@/components/AssigneeSelector'; import LabelsSelector from '@/components/LabelsSelector'; import { createStatus, fetchStatuses, fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types'; import { Modal as AntModal } from '@/shared/antd-imports'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import { useAuthService } from '@/hooks/useAuth'; import { fetchTasksV3 } from '@/features/task-management/task-management.slice'; import './CreateTaskModal.css'; const { Title, Text } = Typography; const { TabPane } = Tabs; interface CreateTaskModalProps { open: boolean; onClose: () => void; projectId?: string; } interface StatusItemProps { status: any; onRename: (id: string, name: string) => void; onDelete: (id: string) => void; isDarkMode: boolean; } // Sortable Status Item Component const SortableStatusItem: React.FC = ({ id, status, onRename, onDelete, isDarkMode, }) => { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(status.name || ''); const inputRef = useRef(null); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; const handleSave = useCallback(() => { if (editName.trim() && editName.trim() !== status.name) { onRename(id, editName.trim()); } setIsEditing(false); }, [editName, id, onRename, status.name]); const handleCancel = useCallback(() => { setEditName(status.name || ''); setIsEditing(false); }, [status.name]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSave(); } else if (e.key === 'Escape') { handleCancel(); } }, [handleSave, handleCancel]); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); return (
{/* Drag Handle */}
{/* Status Color */}
{/* Status Name */}
{isEditing ? ( setEditName(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} size="small" className="font-medium" /> ) : ( setIsEditing(true)} > {status.name} )}
{/* Actions */}
); }; // Status Management Component const StatusManagement: React.FC<{ projectId: string; isDarkMode: boolean; }> = ({ projectId, isDarkMode }) => { const { t } = useTranslation('task-list-filters'); const dispatch = useAppDispatch(); const { status: statuses } = useAppSelector(state => state.taskStatusReducer); const [localStatuses, setLocalStatuses] = useState(statuses); const [newStatusName, setNewStatusName] = useState(''); // DnD sensors const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ); useEffect(() => { setLocalStatuses(statuses); }, [statuses]); const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) { return; } setLocalStatuses((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); if (oldIndex === -1 || newIndex === -1) return items; const newItems = [...items]; const [movedItem] = newItems.splice(oldIndex, 1); newItems.splice(newIndex, 0, movedItem); // Update status order via API (fire and forget) const columnOrder = newItems.map(item => item.id).filter(Boolean) as string[]; const requestBody = { status_order: columnOrder }; statusApiService.updateStatusOrder(requestBody, projectId).catch(error => { console.error('Error updating status order:', error); }); return newItems; }); }, []); const handleCreateStatus = useCallback(async () => { if (!newStatusName.trim()) return; try { const statusCategories = await dispatch(fetchStatusesCategories()).unwrap(); const defaultCategory = statusCategories[0]?.id; if (!defaultCategory) { console.error('No status categories found'); return; } const body = { name: newStatusName.trim(), category_id: defaultCategory, project_id: projectId, }; const res = await dispatch(createStatus({ body, currentProjectId: projectId })).unwrap(); if (res.done) { setNewStatusName(''); dispatch(fetchStatuses(projectId)); } } catch (error) { console.error('Error creating status:', error); } }, [newStatusName, projectId, dispatch]); const handleRenameStatus = useCallback(async (id: string, name: string) => { try { const body: ITaskStatusUpdateModel = { name: name.trim(), project_id: projectId, }; await statusApiService.updateNameOfStatus(id, body, projectId); dispatch(fetchStatuses(projectId)); } catch (error) { console.error('Error renaming status:', error); } }, [projectId, dispatch]); const handleDeleteStatus = useCallback(async (id: string) => { AntModal.confirm({ title: 'Delete Status', content: 'Are you sure you want to delete this status? This action cannot be undone.', onOk: async () => { try { const replacingStatusId = localStatuses.find(s => s.id !== id)?.id || ''; await statusApiService.deleteStatus(id, projectId, replacingStatusId); dispatch(fetchStatuses(projectId)); } catch (error) { console.error('Error deleting status:', error); } }, }); }, [localStatuses, projectId, dispatch]); return (
{t('manageStatuses')} Drag to reorder
{/* Create New Status */}
setNewStatusName(e.target.value)} onPressEnter={handleCreateStatus} className="flex-1" />
{/* Status List with Drag & Drop */} status.id).map(status => status.id as string)} strategy={verticalListSortingStrategy} >
{localStatuses.filter(status => status.id).map((status) => ( ))}
{localStatuses.length === 0 && (
No statuses found. Create your first status above.
)}
); }; const CreateTaskModal: React.FC = ({ open, onClose, projectId, }) => { const { t } = useTranslation('task-drawer/task-drawer'); const [form] = Form.useForm(); const [activeTab, setActiveTab] = useState('task-info'); const dispatch = useAppDispatch(); // Redux state const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark'); const currentProjectId = useAppSelector(state => state.projectReducer.projectId); const user = useAppSelector(state => state.auth?.user); const finalProjectId = projectId || currentProjectId; const handleSubmit = useCallback(async () => { try { const values = await form.validateFields(); const { socket } = useSocket(); if (!socket || !user || !finalProjectId) { console.error('Missing socket, user, or project ID'); return; } const taskData = { name: values.name, description: values.description || null, project_id: finalProjectId, status_id: values.status || null, priority_id: values.priority || null, assignees: values.assignees || [], due_date: values.dueDate ? values.dueDate.format('YYYY-MM-DD') : null, reporter_id: user.id, }; // Create task via socket socket.emit(SocketEvents.QUICK_TASK.toString(), taskData); // Refresh task list dispatch(fetchTasksV3(finalProjectId)); // Reset form and close modal form.resetFields(); setActiveTab('task-info'); onClose(); } catch (error) { console.error('Form validation failed:', error); } }, [form, finalProjectId, dispatch, onClose]); const handleCancel = useCallback(() => { form.resetFields(); setActiveTab('task-info'); onClose(); }, [form, onClose]); return ( {t('createTask')} } open={open} onCancel={handleCancel} width={800} style={{ top: 20 }} bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflowY: 'auto', padding: '24px', }} footer={
} className={isDarkMode ? 'dark-modal' : ''} >
{/* Status Selection */} {/* Priority Selection */} {/* Assignees */} {/* Due Date */}
), }, { key: 'status-management', label: t('manageStatuses'), children: finalProjectId ? (
) : (
Project ID is required for status management.
), }, ]} /> ); }; export default CreateTaskModal;