import { useState, useMemo, useCallback } from 'react'; import { DragDropContext, Droppable, Draggable, type DropResult } from '@hello-pangea/dnd'; import { trpc } from '@/lib/trpc'; import { useFloatingChat } from '@/context/FloatingChatContext'; import { Badge } from '@/components/ui/badge'; import { TaskRow, type TaskItem } from '@/components/tasks/TaskRow'; import { NewTaskDialog } from '@/components/tasks/NewTaskDialog'; import { EditTaskDialog } from '@/components/tasks/EditTaskDialog'; import { TaskDetailDialog } from '@/components/tasks/TaskDetailDialog'; const COLUMNS = [ { id: 'todo', label: 'To Do' }, { id: 'in_progress', label: 'In Progress' }, { id: 'done', label: 'Completed' }, ] as const; type ColumnId = (typeof COLUMNS)[number]['id']; type KanbanBoardProps = { projectId: string; newTaskOpen: boolean; onNewTaskOpenChange: (open: boolean) => void; }; export function KanbanBoard({ projectId, newTaskOpen, onNewTaskOpenChange }: KanbanBoardProps) { const { state: floatingState } = useFloatingChat(); const { data: tasksList } = trpc.tasks.list.useQuery({ projectId }); const utils = trpc.useUtils(); const updateTask = trpc.tasks.update.useMutation({ onSuccess: () => void utils.tasks.list.invalidate(), }); const deleteTask = trpc.tasks.delete.useMutation({ onSuccess: () => void utils.tasks.list.invalidate(), }); // Edit / view task dialog state const [editTask, setEditTask] = useState(null); const [viewTask, setViewTask] = useState(null); // Group tasks by status (exclude unapproved AI suggestions) const columns = useMemo(() => { const tasks = (tasksList ?? []).filter( (t) => !(t.isAiSuggested === 1 && t.isApproved === 0), ); const grouped: Record = { todo: [], in_progress: [], done: [], }; for (const task of tasks) { const status = (task.status ?? 'todo') as ColumnId; if (status in grouped) { grouped[status].push(task); } else { grouped.todo.push(task); } } return grouped; }, [tasksList]); const handleDragEnd = useCallback( (result: DropResult) => { const { destination, source, draggableId } = result; if (!destination) return; if (destination.droppableId === source.droppableId) return; updateTask.mutate({ id: draggableId, status: destination.droppableId, }); }, [updateTask], ); const handleToggle = useCallback( (taskId: string, currentStatus: string | null) => { const nextStatus = currentStatus === 'todo' ? 'in_progress' : currentStatus === 'in_progress' ? 'done' : 'todo'; updateTask.mutate({ id: taskId, status: nextStatus }); }, [updateTask], ); return ( <>
{COLUMNS.map((col) => (
{/* Column header */}
{col.label} {columns[col.id].length}
{/* Droppable column */} {(provided, snapshot) => (
{columns[col.id].map((task, index) => ( {(dragProvided) => (
deleteTask.mutate({ id })} onClick={setViewTask} hideBreadcrumb layoutId={ floatingState.morphTargetId === `task-morph-${task.id}` ? floatingState.morphTargetId : undefined } />
)}
))} {provided.placeholder}
)}
))}
{ if (!open) setEditTask(null); }} /> { if (!open) setViewTask(null); }} onEdit={(task) => { setViewTask(null); setEditTask(task); }} onDelete={(id) => { deleteTask.mutate({ id }); setViewTask(null); }} /> ); }