1→import { useEffect, useState, useMemo } from 'react' 2→import { useSearchParams } from 'react-router-dom' 3→import { Plus, X, GripVertical, Clock, User, RefreshCw, Flag, Pencil, Check, Trash2, Filter, ArrowUpDown, LayoutGrid, List, ChevronLeft, ChevronRight } from 'lucide-react' 4→import * as Dialog from '@radix-ui/react-dialog' 5→import * as AlertDialog from '@radix-ui/react-alert-dialog' 6→import { useProjectStore } from '../stores/projectStore' 7→import { useTaskStore, Task, TaskCreate, TaskUpdate } from '../stores/taskStore' 8→ 9→const ITEMS_PER_PAGE = 10 10→ 11→const COLUMNS = [ 12→ { id: 'backlog', title: 'Backlog', color: 'border-slate-500' }, 13→ { id: 'in_progress', title: 'In Progress', color: 'border-accent' }, 14→ { id: 'qa', title: 'QA', color: 'border-primary-400' }, 15→ { id: 'done', title: 'Done', color: 'border-green-400' }, 16→ { id: 'blocked', title: 'Blocked', color: 'border-destructive' }, 17→] 18→ 19→const PRIORITY_LABELS: Record = { 20→ 0: 'Low', 21→ 1: 'Medium', 22→ 2: 'High', 23→ 3: 'Critical', 24→} 25→ 26→const STATUS_LABELS: Record = { 27→ backlog: 'Backlog', 28→ in_progress: 'In Progress', 29→ qa: 'QA', 30→ done: 'Done', 31→ blocked: 'Blocked', 32→} 33→ 34→export default function KanbanBoard() { 35→ const [searchParams, setSearchParams] = useSearchParams() 36→ const { currentProject } = useProjectStore() 37→ const { tasks, fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask, isTaskDeleting, isLoading } = useTaskStore() 38→ const [showAddDialog, setShowAddDialog] = useState(false) 39→ const [newTaskName, setNewTaskName] = useState('') 40→ const [newTaskDescription, setNewTaskDescription] = useState('') 41→ const [newTaskPriority, setNewTaskPriority] = useState(0) 42→ const [isCreating, setIsCreating] = useState(false) 43→ const [draggedTask, setDraggedTask] = useState(null) 44→ const [selectedTask, setSelectedTask] = useState(null) 45→ const [isEditing, setIsEditing] = useState(false) 46→ const [editName, setEditName] = useState('') 47→ const [editDescription, setEditDescription] = useState('') 48→ const [editPriority, setEditPriority] = useState(0) 49→ const [isSaving, setIsSaving] = useState(false) 50→ const [showDeleteDialog, setShowDeleteDialog] = useState(false) 51→ const [isDeleting, setIsDeleting] = useState(false) 52→ const [showUnsavedDialog, setShowUnsavedDialog] = useState(false) 53→ const [pendingCloseAction, setPendingCloseAction] = useState<'close' | 'cancel' | null>(null) 54→ // Filter and Sort state - initialize from URL params 55→ const getInitialPriorityFilter = (): number | 'all' => { 56→ const param = searchParams.get('priority') 57→ if (param === 'all' || param === null) return 'all' 58→ const num = parseInt(param, 10) 59→ return [0, 1, 2, 3].includes(num) ? num : 'all' 60→ } 61→ const getInitialSortOrder = (): 'newest' | 'oldest' | 'priority' => { 62→ const param = searchParams.get('sort') 63→ if (param === 'newest' || param === 'oldest' || param === 'priority') return param 64→ return 'newest' 65→ } 66→ const getInitialViewMode = (): 'kanban' | 'list' => { 67→ const param = searchParams.get('view') 68→ if (param === 'list') return 'list' 69→ return 'kanban' 70→ } 71→ 72→ const [priorityFilter, setPriorityFilter] = useState(getInitialPriorityFilter) 73→ const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority'>(getInitialSortOrder) 74→ // View mode and pagination state 75→ const [viewMode, setViewMode] = useState<'kanban' | 'list'>(getInitialViewMode) 76→ const [currentPage, setCurrentPage] = useState(1) 77→ 78→ useEffect(() => { 79→ if (currentProject?.id) { 80→ fetchTasks(currentProject.id) 81→ } 82→ }, [currentProject?.id, fetchTasks]) 83→ 84→ // Handle deep linking - select task from URL param after tasks are loaded 85→ useEffect(() => { 86→ const taskIdParam = searchParams.get('task') 87→ if (taskIdParam && tasks.length > 0) { 88→ const taskId = parseInt(taskIdParam, 10) 89→ const task = tasks.find(t => t.id === taskId) 90→ if (task) { 91→ setSelectedTask(task) 92→ } 93→ } 94→ }, [searchParams, tasks]) 95→ 96→ // Update URL when task is selected 97→ const handleSelectTask = (task: Task) => { 98→ setSelectedTask(task) 99→ const newParams = new URLSearchParams(searchParams) 100→ newParams.set('task', task.id.toString()) 101→ setSearchParams(newParams) 102→ } 103→ 104→ // Clear task from URL when panel is closed 105→ const clearTaskFromUrl = () => { 106→ const newParams = new URLSearchParams(searchParams) 107→ newParams.delete('task') 108→ setSearchParams(newParams) 109→ } 110→ 111→ // Update URL when filter changes 112→ const handlePriorityFilterChange = (value: number | 'all') => { 113→ setPriorityFilter(value) 114→ const newParams = new URLSearchParams(searchParams) 115→ if (value === 'all') { 116→ newParams.delete('priority') 117→ } else { 118→ newParams.set('priority', value.toString()) 119→ } 120→ setSearchParams(newParams) 121→ } 122→ 123→ // Update URL when sort changes 124→ const handleSortOrderChange = (value: 'newest' | 'oldest' | 'priority') => { 125→ setSortOrder(value) 126→ const newParams = new URLSearchParams(searchParams) 127→ if (value === 'newest') { 128→ newParams.delete('sort') // default value, no need to store 129→ } else { 130→ newParams.set('sort', value) 131→ } 132→ setSearchParams(newParams) 133→ } 134→ 135→ // Update URL when view mode changes 136→ const handleViewModeChange = (value: 'kanban' | 'list') => { 137→ setViewMode(value) 138→ const newParams = new URLSearchParams(searchParams) 139→ if (value === 'kanban') { 140→ newParams.delete('view') // default value, no need to store 141→ } else { 142→ newParams.set('view', value) 143→ } 144→ setSearchParams(newParams) 145→ } 146→ 147→ // Filtered and sorted tasks 148→ const filteredAndSortedTasks = useMemo(() => { 149→ let result = [...tasks] 150→ 151→ // Apply priority filter 152→ if (priorityFilter !== 'all') { 153→ result = result.filter(task => task.priority === priorityFilter) 154→ } 155→ 156→ // Apply sort 157→ result.sort((a, b) => { 158→ if (sortOrder === 'newest') { 159→ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() 160→ } else if (sortOrder === 'oldest') { 161→ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() 162→ } else { 163→ // priority - highest first 164→ return b.priority - a.priority 165→ } 166→ }) 167→ 168→ return result 169→ }, [tasks, priorityFilter, sortOrder]) 170→ 171→ // Pagination calculations 172→ const totalPages = Math.ceil(filteredAndSortedTasks.length / ITEMS_PER_PAGE) 173→ const paginatedTasks = useMemo(() => { 174→ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE 175→ return filteredAndSortedTasks.slice(startIndex, startIndex + ITEMS_PER_PAGE) 176→ }, [filteredAndSortedTasks, currentPage]) 177→ 178→ // Reset page when filter changes 179→ useEffect(() => { 180→ setCurrentPage(1) 181→ }, [priorityFilter, sortOrder]) 182→ 183→ const getTasksByStatus = (status: string) => { 184→ return filteredAndSortedTasks.filter((task) => task.status === status) 185→ } 186→ 187→ const handleCreateTask = async () => { 188→ if (!currentProject || !newTaskName.trim()) return 189→ setIsCreating(true) 190→ try { 191→ const taskData: TaskCreate = { 192→ name: newTaskName.trim(), 193→ description: newTaskDescription.trim() || undefined, 194→ priority: newTaskPriority, 195→ } 196→ await createTask(currentProject.id, taskData) 197→ setNewTaskName('') 198→ setNewTaskDescription('') 199→ setNewTaskPriority(0) 200→ setShowAddDialog(false) 201→ } catch (error) { 202→ console.error('Failed to create task:', error) 203→ } finally { 204→ setIsCreating(false) 205→ } 206→ } 207→ 208→ const handleDragStart = (taskId: number) => { 209→ setDraggedTask(taskId) 210→ } 211→ 212→ const handleDragOver = (e: React.DragEvent) => { 213→ e.preventDefault() 214→ } 215→ 216→ const handleDrop = async (targetStatus: string) => { 217→ if (draggedTask === null) return 218→ try { 219→ await updateTaskStatus(draggedTask, targetStatus) 220→ } catch (error) { 221→ console.error('Failed to update task status:', error) 222→ } 223→ setDraggedTask(null) 224→ } 225→ 226→ const handleStartEdit = () => { 227→ if (!selectedTask) return 228→ setEditName(selectedTask.name) 229→ setEditDescription(selectedTask.description || '') 230→ setEditPriority(selectedTask.priority) 231→ setIsEditing(true) 232→ } 233→ 234→ // Check if edit form has unsaved changes 235→ const hasUnsavedChanges = () => { 236→ if (!selectedTask || !isEditing) return false 237→ return ( 238→ editName.trim() !== selectedTask.name || 239→ (editDescription.trim() || '') !== (selectedTask.description || '') || 240→ editPriority !== selectedTask.priority 241→ ) 242→ } 243→ 244→ const handleCancelEdit = () => { 245→ if (hasUnsavedChanges()) { 246→ setPendingCloseAction('cancel') 247→ setShowUnsavedDialog(true) 248→ } else { 249→ setIsEditing(false) 250→ } 251→ } 252→ 253→ const handleClosePanel = () => { 254→ if (hasUnsavedChanges()) { 255→ setPendingCloseAction('close') 256→ setShowUnsavedDialog(true) 257→ } else { 258→ setSelectedTask(null) 259→ setIsEditing(false) 260→ clearTaskFromUrl() 261→ } 262→ } 263→ 264→ const handleDiscardChanges = () => { 265→ setShowUnsavedDialog(false) 266→ if (pendingCloseAction === 'close') { 267→ setSelectedTask(null) 268→ clearTaskFromUrl() 269→ } 270→ setIsEditing(false) 271→ setPendingCloseAction(null) 272→ } 273→ 274→ const handleKeepEditing = () => { 275→ setShowUnsavedDialog(false) 276→ setPendingCloseAction(null) 277→ } 278→ 279→ const handleSaveEdit = async () => { 280→ if (!selectedTask) return 281→ setIsSaving(true) 282→ try { 283→ const updates: TaskUpdate = { 284→ name: editName.trim(), 285→ description: editDescription.trim() || undefined, 286→ priority: editPriority, 287→ } 288→ const updatedTask = await updateTask(selectedTask.id, updates) 289→ setSelectedTask(updatedTask) 290→ setIsEditing(false) 291→ } catch (error) { 292→ console.error('Failed to update task:', error) 293→ } finally { 294→ setIsSaving(false) 295→ } 296→ } 297→ 298→ const handleDeleteTask = async () => { 299→ if (!selectedTask) return 300→ // Check if already being deleted (prevents rapid clicks) 301→ if (isTaskDeleting(selectedTask.id)) return 302→ 303→ const taskIdToDelete = selectedTask.id 304→ setIsDeleting(true) 305→ // Close dialog immediately to prevent additional clicks 306→ setShowDeleteDialog(false) 307→ 308→ try { 309→ await deleteTask(taskIdToDelete) 310→ setSelectedTask(null) 311→ clearTaskFromUrl() 312→ } catch (error) { 313→ console.error('Failed to delete task:', error) 314→ // Re-open dialog if delete failed so user can retry 315→ setShowDeleteDialog(true) 316→ } finally { 317→ setIsDeleting(false) 318→ } 319→ } 320→ 321→ if (!currentProject) { 322→ return ( 323→
324→

Select a project to view its Kanban board

325→
326→ ) 327→ } 328→ 329→ return ( 330→
331→ {/* Header */} 332→
333→

Kanban Board

334→
335→ {/* Priority Filter */} 336→
337→ 338→ 349→
350→ 351→ {/* Sort Order */} 352→
353→ 354→ 363→
364→ 365→ {/* View Mode Toggle */} 366→
367→ 374→ 381→
382→ 383→ 384→ 385→ 389→ 390→ 391→ 392→ 393→
394→ 395→ Add New Task 396→ 397→ 398→ 401→ 402→
403→ 404→ Create a new task for this project. 405→ 406→
407→
408→ 411→ setNewTaskName(e.target.value)} 415→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent" 416→ placeholder="Enter task name" 417→ /> 418→
419→
420→ 423→