task-column.tsx
1 'use client' 2 3 import { useState, useCallback } from 'react' 4 import { TaskCard } from './task-card' 5 import { useCreateTaskMutation } from '@/features/tasks/queries' 6 import type { Agent, BoardTask, BoardTaskStatus, Project } from '@/types' 7 8 const COLUMN_CONFIG: Record<BoardTaskStatus, { label: string; color: string; dot: string }> = { 9 backlog: { label: 'Backlog', color: 'text-text-3', dot: 'bg-white/20' }, 10 queued: { label: 'Queued', color: 'text-amber-400', dot: 'bg-amber-400' }, 11 running: { label: 'Running', color: 'text-blue-400', dot: 'bg-blue-400' }, 12 completed: { label: 'Completed', color: 'text-emerald-400', dot: 'bg-emerald-400' }, 13 failed: { label: 'Failed', color: 'text-red-400', dot: 'bg-red-400' }, 14 cancelled: { label: 'Cancelled', color: 'text-text-3', dot: 'bg-white/20' }, 15 archived: { label: 'Archived', color: 'text-text-3/50', dot: 'bg-white/10' }, 16 deferred: { label: 'Deferred', color: 'text-orange-400', dot: 'bg-orange-400' }, 17 } 18 19 interface Props { 20 status: BoardTaskStatus 21 tasks: BoardTask[] 22 agents: Record<string, Agent> 23 projects: Record<string, Project> 24 tasksById: Record<string, BoardTask> 25 onDrop: (taskId: string, newStatus: BoardTaskStatus) => void 26 selectionMode?: boolean 27 selectedIds?: Set<string> 28 onToggleSelect?: (id: string) => void 29 onSelectAll?: () => void 30 } 31 32 export function TaskColumn({ 33 status, 34 tasks, 35 agents, 36 projects, 37 tasksById, 38 onDrop, 39 selectionMode, 40 selectedIds, 41 onToggleSelect, 42 onSelectAll, 43 }: Props) { 44 const config = COLUMN_CONFIG[status] 45 const [dragOver, setDragOver] = useState(false) 46 const [quickAddValue, setQuickAddValue] = useState('') 47 const [adding, setAdding] = useState(false) 48 const createTaskMutation = useCreateTaskMutation() 49 50 const handleDragOver = useCallback((e: React.DragEvent) => { 51 e.preventDefault() 52 e.dataTransfer.dropEffect = 'move' 53 setDragOver(true) 54 }, []) 55 56 const handleDragLeave = useCallback(() => { 57 setDragOver(false) 58 }, []) 59 60 const handleDrop = useCallback((e: React.DragEvent) => { 61 e.preventDefault() 62 setDragOver(false) 63 const taskId = e.dataTransfer.getData('text/plain') 64 if (taskId) { 65 onDrop(taskId, status) 66 } 67 }, [onDrop, status]) 68 69 const handleQuickAdd = async () => { 70 const title = quickAddValue.trim() 71 if (!title || adding) return 72 setAdding(true) 73 try { 74 await createTaskMutation.mutateAsync({ title, description: '', agentId: '', status }) 75 setQuickAddValue('') 76 } finally { 77 setAdding(false) 78 } 79 } 80 81 const selectedCount = tasks.filter((t) => selectedIds?.has(t.id)).length 82 83 return ( 84 <div 85 className={`flex-1 min-w-[240px] max-w-[320px] min-h-0 flex flex-col rounded-[16px] transition-colors duration-150 ${ 86 dragOver ? 'bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : '' 87 }`} 88 onDragOver={handleDragOver} 89 onDragLeave={handleDragLeave} 90 onDrop={handleDrop} 91 > 92 <div className="flex items-center gap-2.5 px-2 mb-3"> 93 <div className={`w-2 h-2 rounded-full ${config.dot}`} /> 94 <span className={`font-display text-[13px] font-600 ${config.color}`}>{config.label}</span> 95 <span className="text-[12px] text-text-3 ml-auto">{tasks.length}</span> 96 {selectionMode && tasks.length > 0 && ( 97 <button 98 onClick={onSelectAll} 99 className={`text-[10px] font-600 px-1.5 py-0.5 rounded-[5px] cursor-pointer border-none transition-colors 100 ${selectedCount === tasks.length && selectedCount > 0 101 ? 'bg-accent-bright/20 text-accent-bright' 102 : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08]'}`} 103 style={{ fontFamily: 'inherit' }} 104 > 105 {selectedCount === tasks.length && selectedCount > 0 ? 'All' : 'Select all'} 106 </button> 107 )} 108 </div> 109 110 {/* Quick add input */} 111 {(status === 'backlog' || status === 'queued') && ( 112 <div className="px-1 mb-2"> 113 <input 114 type="text" 115 value={quickAddValue} 116 onChange={(e) => setQuickAddValue(e.target.value)} 117 onKeyDown={(e) => { if (e.key === 'Enter') handleQuickAdd() }} 118 placeholder={`+ Add to ${config.label.toLowerCase()}...`} 119 className="w-full px-3 py-2 rounded-[10px] bg-white/[0.02] border border-dashed border-white/[0.08] text-[12px] text-text placeholder:text-text-3/30 outline-none focus:border-white/[0.15] focus:bg-white/[0.03] transition-colors" 120 style={{ fontFamily: 'inherit' }} 121 disabled={adding} 122 /> 123 </div> 124 )} 125 126 <div className="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto overscroll-y-contain touch-pan-y pr-1 px-1 pb-2"> 127 {tasks.map((task, idx) => ( 128 <TaskCard 129 key={task.id} 130 task={task} 131 agents={agents} 132 projects={projects} 133 tasksById={tasksById} 134 index={idx} 135 selectionMode={selectionMode} 136 selected={selectedIds?.has(task.id)} 137 onToggleSelect={onToggleSelect} 138 /> 139 ))} 140 {tasks.length === 0 && ( 141 <div className={`text-[12px] text-text-3/50 text-center py-8 rounded-[12px] border border-dashed transition-colors ${ 142 dragOver ? 'border-accent-bright/30 text-accent-bright/50' : 'border-transparent' 143 }`}> 144 {dragOver ? 'Drop here' : 'No tasks'} 145 </div> 146 )} 147 </div> 148 </div> 149 ) 150 }