# Kanban Board - Implementación Detallada ## Drag & Drop con dnd-kit ### Configuración del DndContext ```typescript // components/kanban/KanbanBoard.tsx import { DndContext, DragEndEvent, DragOverEvent, DragStartEvent, PointerSensor, useSensor, useSensors, DragOverlay, } from '@dnd-kit/core' import { useState } from 'react' export function KanbanBoard({ projectId }: KanbanBoardProps) { const [activeId, setActiveId] = useState(null) const { data: tasks = [] } = useTasks({ projectId }) const updateTask = useUpdateTask() // Configure sensors const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, // Require 8px movement before dragging starts }, }) ) const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string) } const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event setActiveId(null) if (!over || active.id === over.id) return const taskId = active.id as string const newState = over.id as TaskState // Optimistic update updateTask.mutate({ taskId, updates: { state: newState }, }) } const activeTask = tasks.find((t) => t.id === activeId) return (
{COLUMNS.map((column) => ( ))}
{/* Drag overlay for better UX */} {activeTask ? : null}
) } ``` ### Column como Droppable ```typescript // components/kanban/KanbanColumn.tsx import { useDroppable } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' export default function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) { const { setNodeRef, isOver } = useDroppable({ id }) return (
{/* Header */}

{title} ({tasks.length})

{/* Drop zone */}
t.id)} strategy={verticalListSortingStrategy}>
{tasks.map((task) => ( ))}
{tasks.length === 0 && (
{isOver ? 'Suelta aquí' : 'Sin tareas'}
)}
) } ``` ### Task Card como Draggable ```typescript // components/kanban/TaskCard.tsx import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' export default function TaskCard({ task }: TaskCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: task.id, data: { type: 'task', task, }, }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, cursor: 'move', } return (
{/* Task content */}
) } ``` ## Acciones Rápidas ```typescript // components/kanban/TaskCardActions.tsx import { MoreVertical, ExternalLink, MessageSquare, CheckCircle, XCircle } from 'lucide-react' import { Task } from '@/types/task' import { useApproveTask, useRejectTask } from '@/hooks/useTasks' interface TaskCardActionsProps { task: Task } export function TaskCardActions({ task }: TaskCardActionsProps) { const approveTask = useApproveTask() const rejectTask = useRejectTask() const handleApprove = (e: React.MouseEvent) => { e.stopPropagation() if (confirm('¿Aprobar esta tarea?')) { approveTask.mutate(task.id) } } const handleReject = (e: React.MouseEvent) => { e.stopPropagation() const reason = prompt('Razón del rechazo:') if (reason) { rejectTask.mutate({ taskId: task.id, reason }) } } return (
{/* Preview link */} {task.previewUrl && ( e.stopPropagation()} title="Abrir preview" > )} {/* Questions */} {task.state === 'needs_input' && ( )} {/* Approve/Reject for ready_to_test */} {task.state === 'ready_to_test' && ( <> )} {/* More actions */}
) } ``` ## Filtros y Búsqueda ```typescript // components/kanban/KanbanFilters.tsx import { useState } from 'react' import { Search, Filter } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' interface KanbanFiltersProps { onFilterChange: (filters: TaskFilters) => void } export function KanbanFilters({ onFilterChange }: KanbanFiltersProps) { const [search, setSearch] = useState('') const [priority, setPriority] = useState('all') const [assignedAgent, setAssignedAgent] = useState('all') const handleSearchChange = (value: string) => { setSearch(value) onFilterChange({ search: value, priority, assignedAgent }) } return (
handleSearchChange(e.target.value)} placeholder="Buscar tareas..." className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500" />