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

   381→
Select a project to view its Kanban board

   382→

   383→    )
   384→  }
   385→
   386→  return (
   387→

   388→      {/* Header */}
   389→

   390→
Kanban Board

   391→

   392→          {/* Priority Filter */}
   393→

   394→            <Filter size={16} className="text-muted-foreground" />
   395→            <select
   396→              value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
   397→              onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
   398→              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
   399→            >
   400→              <option value="all">All Priorities</option>
   401→              <option value="3">Critical</option>
   402→              <option value="2">High</option>
   403→              <option value="1">Medium</option>
   404→              <option value="0">Low</option>
   405→            </select>
   406→

   407→
   408→          {/* Status Filter */}
   409→

   410→            <select
   411→              value={statusFilter}
   412→              onChange={(e) => handleStatusFilterChange(e.target.value)}
   413→              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
   414→            >
   415→              <option value="all">All Statuses</option>
   416→              <option value="backlog">Backlog</option>
   417→              <option value="in_progress">In Progress</option>
   418→              <option value="qa">QA</option>
   419→              <option value="done">Done</option>
   420→              <option value="blocked">Blocked</option>
   421→            </select>
   422→

   423→
   424→          {/* Sort Order */}
   425→

   426→            <ArrowUpDown size={16} className="text-muted-foreground" />
   427→            <select
   428→              value={sortOrder}
   429→              onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
   430→              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
   431→            >
   432→              <option value="newest">Newest First</option>
   433→              <option value="oldest">Oldest First</option>
   434→              <option value="priority">By Priority</option>
   435→            </select>
   436→

   437→
   438→          {/* Reset Filters Button */}
   439→          {hasActiveFilters && (
   440→            <button
   441→              onClick={handleResetFilters}
   442→              className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-secondary rounded-lg transition-colors"
   443→              title="Reset Filters"
   444→            >
   445→              <RefreshCw size={14} />
   446→              Reset
   447→            </button>
   448→          )}
   449→
   450→          {/* View Mode Toggle */}
   451→

   452→            <button
   453→              onClick={() => handleViewModeChange('kanban')}
   454→              className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
   455→              title="Kanban View"
   456→            >
   457→              <LayoutGrid size={18} />
   458→            </button>
   459→            <button
   460→              onClick={() => handleViewModeChange('list')}
   461→              className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
   462→              title="List View"
   463→            >
   464→              <List size={18} />
   465→            </button>
   466→

   467→
   468→        <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
   469→          <Dialog.Trigger asChild>
   470→            <button className="btn-accent flex items-center gap-2">
   471→              <Plus size={18} />
   472→              Add Task
   473→            </button>
   474→          </Dialog.Trigger>
   475→          <Dialog.Portal>
   476→            <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
   477→            <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
   478→

   479→                <Dialog.Title className="text-xl font-semibold text-foreground">
   480→                  Add New Task
   481→                </Dialog.Title>
   482→                <Dialog.Close asChild>
   483→                  <button className="text-muted-foreground hover:text-foreground" aria-label="Close dialog">
   484→                    <X size={20} />
   485→                  </button>
   486→                </Dialog.Close>
   487→

   488→              <Dialog.Description className="text-muted-foreground mb-4">
   489→                Create a new task for this project.
   490→              </Dialog.Description>
   491→

   492→

   493→
   494→                    Task Name *
   495→
   496→                  <input
   497→                    type="text"
   498→                    value={newTaskName}
   499→                    onChange={(e) => setNewTaskName(e.target.value)}
   500→                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   501→                    placeholder="Enter task name"
   502→                  />
   503→

   504→

   505→
   506→                    Description
   507→
   508→                  <textarea
   509→                    value={newTaskDescription}
   510→                    onChange={(e) => setNewTaskDescription(e.target.value)}
   511→                    rows={3}
   512→                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
   513→                    placeholder="Enter task description"
   514→                  />
   515→

   516→

   517→
   518→                    Priority
   519→
   520→                  <select
   521→                    value={newTaskPriority}
   522→                    onChange={(e) => setNewTaskPriority(Number(e.target.value))}
   523→                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   524→                  >
   525→                    <option value={0}>Low</option>
   526→                    <option value={1}>Medium</option>
   527→                    <option value={2}>High</option>
   528→                    <option value={3}>Critical</option>
   529→                  </select>
   530→

   531→

   532→
   533→                    Phase
   534→
   535→                  <select
   536→                    value={newTaskPhaseId ?? ''}
   537→                    onChange={(e) => setNewTaskPhaseId(e.target.value ? Number(e.target.value) : null)}
   538→                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   539→                  >
   540→                    <option value="">No Phase</option>
   541→                    {phases.map((phase) => (
   542→                      <option key={phase.id} value={phase.id}>{phase.name}</option>
   543→                    ))}
   544→                  </select>
   545→

   546→

   547→

   548→                <Dialog.Close asChild>
   549→                  <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
   550→                    Cancel
   551→                  </button>
   552→                </Dialog.Close>
   553→                <button
   554→                  onClick={handleCreateTask}
   555→                  disabled={isCreating || !newTaskName.trim()}
   556→                  className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
   557→                >
   558→                  {isCreating ? 'Creating...' : 'Create Task'}
   559→                </button>
   560→

   561→            </Dialog.Content>
   562→          </Dialog.Portal>
   563→        </Dialog.Root>
   564→

   565→

   566→
   567→      {/* Main Content Area */}
   568→

   569→        {viewMode === 'list' ? (
   570→          /* List View with Pagination */
   571→

   572→            {/* Task List Table */}
   573→

   574→

   575→

   576→

   577→                    Task Name
   578→                    Status
   579→                    Priority
   580→                    Created
   581→

   582→

   583→

   584→                  {isLoading && tasks.length === 0 ? (
   585→

   586→
   587→                        Loading...
   588→
   589→

   590→                  ) : paginatedTasks.length === 0 ? (
   591→

   592→
   593→                        No tasks found
   594→
   595→

   596→                  ) : (
   597→                    paginatedTasks.map((task) => (
   598→
 handleSelectTask(task)}
   601→                        className={`border-t border-border hover:bg-secondary/50 cursor-pointer ${
   602→                          selectedTask?.id === task.id ? 'bg-secondary' : ''
   603→                        }`}
   604→                      >
   605→
   606→
{task.name}

   607→                          {task.description && (
   608→

   609→                              {task.description}
   610→

   611→                          )}
   612→
   613→
   614→
   621→                            {STATUS_LABELS[task.status] || task.status}
   622→
   623→
   624→
   625→
   631→                            {PRIORITY_LABELS[task.priority] || 'Unknown'}
   632→
   633→
   634→
   635→                          {new Date(task.created_at).toLocaleDateString()}
   636→
   637→

   638→                    ))
   639→                  )}
   640→

   641→

   642→

   643→
   644→            {/* Pagination Controls */}
   645→            {totalPages > 1 && (
   646→

   647→

   648→                  Showing {((currentPage - 1) * ITEMS_PER_PAGE) + 1} to {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedTasks.length)} of {filteredAndSortedTasks.length} tasks
   649→

   650→

   651→                  <button
   652→                    onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
   653→                    disabled={currentPage === 1}
   654→                    className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
   655→                  >
   656→                    <ChevronLeft size={18} />
   657→                  </button>
   658→

   659→                    {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
   660→                      <button
   661→                        key={page}
   662→                        onClick={() => setCurrentPage(page)}
   663→                        className={`w-8 h-8 rounded-lg text-sm ${
   664→                          page === currentPage
   665→                            ? 'bg-accent text-white'
   666→                            : 'bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary'
   667→                        }`}
   668→                      >
   669→                        {page}
   670→                      </button>
   671→                    ))}
   672→

   673→                  <button
   674→                    onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
   675→                    disabled={currentPage === totalPages}
   676→                    className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
   677→                  >
   678→                    <ChevronRight size={18} />
   679→                  </button>
   680→

   681→

   682→            )}
   683→

   684→        ) : (
   685→        /* Kanban Columns */
   686→        <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
   687→          {COLUMNS.map((column) => (
   688→          <div
   689→            key={column.id}
   690→            className={`flex-shrink-0 w-72 bg-card rounded-lg border-t-2 ${column.color}`}
   691→            onDragOver={handleDragOver}
   692→            onDrop={() => handleDrop(column.id)}
   693→          >
   694→            {/* Column Header */}
   695→

   696→

   697→
{column.title}

   698→
   699→                  {getTasksByStatus(column.id).length}
   700→
   701→

   702→

   703→
   704→            {/* Column Content */}
   705→

   706→              {isLoading && tasks.length === 0 ? (
   707→

   708→                  Loading...
   709→

   710→              ) : getTasksByStatus(column.id).length === 0 ? (
   711→

   712→                  No tasks
   713→

   714→              ) : (
   715→                getTasksByStatus(column.id).map((task) => (
   716→
 handleDragStart(task.id)}
   720→                    onClick={() => handleSelectTask(task)}
   721→                    className={`kanban-card bg-background rounded-lg p-4 border border-border cursor-pointer hover:border-muted-foreground ${
   722→                      draggedTask === task.id ? 'opacity-50' : ''
   723→                    } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
   724→                  >
   725→

   726→                      <GripVertical size={16} className="text-muted-foreground mt-0.5 flex-shrink-0" />
   727→

   728→
{task.name}

   729→                        {task.description && (
   730→

   731→                            {task.description}
   732→

   733→                        )}
   734→

   735→                          <span
   736→                            className={`badge ${
   737→                              column.id === 'backlog'
   738→                                ? 'badge-backlog'
   739→                                : column.id === 'in_progress'
   740→                                ? 'badge-in-progress'
   741→                                : column.id === 'qa'
   742→                                ? 'badge-qa'
   743→                                : column.id === 'done'
   744→                                ? 'badge-done'
   745→                                : 'badge-blocked'
   746→                            }`}
   747→                          >
   748→                            {column.title}
   749→
   750→
   756→                            {task.priority === 3 ? 'Critical' :
   757→                             task.priority === 2 ? 'High' :
   758→                             task.priority === 1 ? 'Medium' : 'Low'}
   759→
   760→

   761→

   762→

   763→

   764→                ))
   765→              )}
   766→

   767→

   768→          ))}
   769→

   770→        )}
   771→
   772→        {/* Task Details Panel */}
   773→        {selectedTask && (
   774→

   775→

   776→
Task Details

   777→

   778→                {!isEditing && (
   779→                  <>
   780→                    <button
   781→                      onClick={handleStartEdit}
   782→                      className="text-muted-foreground hover:text-accent transition-colors"
   783→                      title="Edit Task"
   784→                    >
   785→                      <Pencil size={18} />
   786→                    </button>
   787→                    <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
   788→                      <AlertDialog.Trigger asChild>
   789→                        <button
   790→                          className="text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
   791→                          title="Delete Task"
   792→                          disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
   793→                        >
   794→                          <Trash2 size={18} />
   795→                        </button>
   796→                      </AlertDialog.Trigger>
   797→                      <AlertDialog.Portal>
   798→                        <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
   799→                        <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
   800→                          <AlertDialog.Title className="text-xl font-semibold text-foreground">
   801→                            Delete Task
   802→                          </AlertDialog.Title>
   803→                          <AlertDialog.Description className="mt-3 text-muted-foreground">
   804→                            Are you sure you want to delete {selectedTask.name}?
   805→                            This action cannot be undone.
   806→                          </AlertDialog.Description>
   807→

   808→                            <AlertDialog.Cancel asChild>
   809→                              <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
   810→                                Cancel
   811→                              </button>
   812→                            </AlertDialog.Cancel>
   813→                            <AlertDialog.Action asChild>
   814→                              <button
   815→                                onClick={handleDeleteTask}
   816→                                disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
   817→                                className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
   818→                              >
   819→                                {isDeleting || (selectedTask && isTaskDeleting(selectedTask.id)) ? 'Deleting...' : 'Delete Task'}
   820→                              </button>
   821→                            </AlertDialog.Action>
   822→

   823→                        </AlertDialog.Content>
   824→                      </AlertDialog.Portal>
   825→                    </AlertDialog.Root>
   826→                  </>
   827→                )}
   828→                <button
   829→                  onClick={handleClosePanel}
   830→                  className="text-muted-foreground hover:text-foreground transition-colors"
   831→                  title="Close"
   832→                >
   833→                  <X size={20} />
   834→                </button>
   835→

   836→

   837→

   838→              {isEditing ? (
   839→                /* Edit Form */
   840→

   841→

   842→                    Name *
   843→                    <input
   844→                      type="text"
   845→                      value={editName}
   846→                      onChange={(e) => setEditName(e.target.value)}
   847→                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   848→                      placeholder="Task name"
   849→                    />
   850→

   851→

   852→                    Description
   853→                    <textarea
   854→                      value={editDescription}
   855→                      onChange={(e) => setEditDescription(e.target.value)}
   856→                      rows={3}
   857→                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
   858→                      placeholder="Task description"
   859→                    />
   860→

   861→

   862→                    Priority
   863→                    <select
   864→                      value={editPriority}
   865→                      onChange={(e) => setEditPriority(Number(e.target.value))}
   866→                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   867→                    >
   868→                      <option value={0}>Low</option>
   869→                      <option value={1}>Medium</option>
   870→                      <option value={2}>High</option>
   871→                      <option value={3}>Critical</option>
   872→                    </select>
   873→

   874→

   875→                    Phase
   876→                    <select
   877→                      value={editPhaseId ?? ''}
   878→                      onChange={(e) => setEditPhaseId(e.target.value ? Number(e.target.value) : null)}
   879→                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   880→                    >
   881→                      <option value="">No Phase</option>
   882→                      {phases.map((phase) => (
   883→                        <option key={phase.id} value={phase.id}>{phase.name}</option>
   884→                      ))}
   885→                    </select>
   886→

   887→

   888→                    <button
   889→                      onClick={handleCancelEdit}
   890→                      className="flex-1 px-3 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
   891→                    >
   892→                      Cancel
   893→                    </button>
   894→                    <button
   895→                      onClick={handleSaveEdit}
   896→                      disabled={isSaving || !editName.trim()}
   897→                      className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
   898→                    >
   899→                      {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
   900→                    </button>
   901→

   902→

   903→              ) : (
   904→                /* View Mode */
   905→                <>
   906→                  {/* Task Name */}
   907→

   908→                    Name
   909→
{selectedTask.name}

   910→

   911→
   912→                  {/* Description */}
   913→

   914→                    Description
   915→

   916→                      {selectedTask.description || No description}
   917→

   918→

   919→
   920→                  {/* Status */}
   921→

   922→                    Status
   923→
   930→                      {STATUS_LABELS[selectedTask.status] || selectedTask.status}
   931→
   932→

   933→
   934→                  {/* Priority */}
   935→

   936→                    Priority
   937→
   943→                      {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}
   944→
   945→

   946→
   947→                  {/* Phase */}
   948→

   949→                    Phase
   950→

   951→                      {selectedTask.phase_id ? (
   952→                        phases.find(p => p.id === selectedTask.phase_id)?.name || 'Unknown Phase'
   953→                      ) : (
   954→                        No phase
   955→                      )}
   956→

   957→

   958→
   959→                  {/* Agent Assignment */}
   960→

   961→
   962→                      <User size={14} />
   963→                      Agent Assignment
   964→
   965→

   966→                      {selectedTask.agent_type ? (
   967→                        {selectedTask.agent_type.replace('_', ' ')}
   968→                      ) : (
   969→                        Not assigned
   970→                      )}
   971→

   972→

   973→
   974→                  {/* Iteration Count */}
   975→

   976→
   977→                      <RefreshCw size={14} />
   978→                      Iteration Count
   979→
   980→
{selectedTask.iteration_count}

   981→

   982→
   983→                  {/* Flagged for Review */}
   984→

   985→
   986→                      <Flag size={14} />
   987→                      Flagged for Review
   988→
   989→

   990→                      {selectedTask.flagged_for_review ? 'Yes' : 'No'}
   991→

   992→

   993→
   994→                  {/* Time Estimates */}
   995→                  {(selectedTask.estimated_time || selectedTask.actual_time) && (
   996→

   997→
   998→                        <Clock size={14} />
   999→                        Time
  1000→
  1001→

  1002→                        {selectedTask.estimated_time &&
Estimated: {selectedTask.estimated_time} min
}
  1003→                        {selectedTask.actual_time &&
Actual: {selectedTask.actual_time} min
}
  1004→

  1005→

  1006→                  )}
  1007→
  1008→                  {/* Created/Updated timestamps */}
  1009→

  1010→
Created: {new Date(selectedTask.created_at).toLocaleString()}

  1011→
Updated: {new Date(selectedTask.updated_at).toLocaleString()}

  1012→

  1013→                </>
  1014→              )}
  1015→

  1016→

  1017→        )}
  1018→
  1019→        {/* Unsaved Changes Warning Dialog */}
  1020→        <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
  1021→          <AlertDialog.Portal>
  1022→            <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
  1023→            <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
  1024→              <AlertDialog.Title className="text-xl font-semibold text-foreground">
  1025→                Unsaved Changes
  1026→              </AlertDialog.Title>
  1027→              <AlertDialog.Description className="mt-3 text-muted-foreground">
  1028→                You have unsaved changes. Are you sure you want to discard them?
  1029→              </AlertDialog.Description>
  1030→

  1031→                <AlertDialog.Cancel asChild>
  1032→                  <button
  1033→                    onClick={handleKeepEditing}
  1034→                    className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
  1035→                  >
  1036→                    Keep Editing
  1037→                  </button>
  1038→                </AlertDialog.Cancel>
  1039→                <AlertDialog.Action asChild>
  1040→                  <button
  1041→                    onClick={handleDiscardChanges}
  1042→                    className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
  1043→                  >
  1044→                    Discard Changes
  1045→                  </button>
  1046→                </AlertDialog.Action>
  1047→

  1048→            </AlertDialog.Content>
  1049→          </AlertDialog.Portal>
  1050→        </AlertDialog.Root>
  1051→

  1052→

  1053→  )
  1054→}
  1055→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import { useEffect, useState, useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Plus, X, GripVertical, Clock, User, RefreshCw, Flag, Pencil, Check, Trash2, Filter, ArrowUpDown, LayoutGrid, List, ChevronLeft, ChevronRight } from 'lucide-react'
import * as Dialog from '@radix-ui/react-dialog'
import * as AlertDialog from '@radix-ui/react-alert-dialog'
import { useProjectStore } from '../stores/projectStore'
import { useTaskStore, Task, TaskCreate, TaskUpdate } from '../stores/taskStore'
import { usePhaseStore } from '../stores/phaseStore'
import { useToastStore } from '../components/Toast'

const ITEMS_PER_PAGE = 10

const COLUMNS = [
  { id: 'backlog', title: 'Backlog', color: 'border-slate-500' },
  { id: 'in_progress', title: 'In Progress', color: 'border-accent' },
  { id: 'qa', title: 'QA', color: 'border-primary-400' },
  { id: 'done', title: 'Done', color: 'border-green-400' },
  { id: 'blocked', title: 'Blocked', color: 'border-destructive' },
]

const PRIORITY_LABELS: Record<number, string> = {
  0: 'Low',
  1: 'Medium',
  2: 'High',
  3: 'Critical',
}

const STATUS_LABELS: Record<string, string> = {
  backlog: 'Backlog',
  in_progress: 'In Progress',
  qa: 'QA',
  done: 'Done',
  blocked: 'Blocked',
}

export default function KanbanBoard() {
  const [searchParams, setSearchParams] = useSearchParams()
  const { currentProject } = useProjectStore()
  const { tasks, fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask, isTaskDeleting, isLoading } = useTaskStore()
  const { phases, fetchPhases } = usePhaseStore()
  const { addToast } = useToastStore()
  const [showAddDialog, setShowAddDialog] = useState(false)
  const [newTaskName, setNewTaskName] = useState('')
  const [newTaskDescription, setNewTaskDescription] = useState('')
  const [newTaskPriority, setNewTaskPriority] = useState(0)
  const [newTaskPhaseId, setNewTaskPhaseId] = useState<number | null>(null)
  const [isCreating, setIsCreating] = useState(false)
  const [draggedTask, setDraggedTask] = useState<number | null>(null)
  const [selectedTask, setSelectedTask] = useState<Task | null>(null)
  const [isEditing, setIsEditing] = useState(false)
  const [editName, setEditName] = useState('')
  const [editDescription, setEditDescription] = useState('')
  const [editPriority, setEditPriority] = useState(0)
  const [editPhaseId, setEditPhaseId] = useState<number | null>(null)
  const [isSaving, setIsSaving] = useState(false)
  const [showDeleteDialog, setShowDeleteDialog] = useState(false)
  const [isDeleting, setIsDeleting] = useState(false)
  const [showUnsavedDialog, setShowUnsavedDialog] = useState(false)
  const [pendingCloseAction, setPendingCloseAction] = useState<'close' | 'cancel' | null>(null)
  // Filter and Sort state - initialize from URL params
  const getInitialPriorityFilter = (): number | 'all' => {
    const param = searchParams.get('priority')
    if (param === 'all' || param === null) return 'all'
    const num = parseInt(param, 10)
    return [0, 1, 2, 3].includes(num) ? num : 'all'
  }
  const getInitialStatusFilter = (): string | 'all' => {
    const param = searchParams.get('status')
    if (param === 'all' || param === null) return 'all'
    const validStatuses = ['backlog', 'in_progress', 'qa', 'done', 'blocked']
    return validStatuses.includes(param) ? param : 'all'
  }
  const getInitialSortOrder = (): 'newest' | 'oldest' | 'priority' => {
    const param = searchParams.get('sort')
    if (param === 'newest' || param === 'oldest' || param === 'priority') return param
    return 'newest'
  }
  const getInitialViewMode = (): 'kanban' | 'list' => {
    const param = searchParams.get('view')
    if (param === 'list') return 'list'
    return 'kanban'
  }

  const [priorityFilter, setPriorityFilter] = useState<number | 'all'>(getInitialPriorityFilter)
  const [statusFilter, setStatusFilter] = useState<string | 'all'>(getInitialStatusFilter)
  const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority'>(getInitialSortOrder)
  // View mode and pagination state
  const [viewMode, setViewMode] = useState<'kanban' | 'list'>(getInitialViewMode)
  const [currentPage, setCurrentPage] = useState(1)

  useEffect(() => {
    if (currentProject?.id) {
      fetchTasks(currentProject.id)
      fetchPhases(currentProject.id)
    }
  }, [currentProject?.id, fetchTasks, fetchPhases])

  // Handle deep linking - select task from URL param after tasks are loaded
  useEffect(() => {
    const taskIdParam = searchParams.get('task')
    if (taskIdParam && tasks.length > 0) {
      const taskId = parseInt(taskIdParam, 10)
      const task = tasks.find(t => t.id === taskId)
      if (task) {
        setSelectedTask(task)
      }
    }
  }, [searchParams, tasks])

  // Update URL when task is selected
  const handleSelectTask = (task: Task) => {
    setSelectedTask(task)
    const newParams = new URLSearchParams(searchParams)
    newParams.set('task', task.id.toString())
    setSearchParams(newParams)
  }

  // Clear task from URL when panel is closed
  const clearTaskFromUrl = () => {
    const newParams = new URLSearchParams(searchParams)
    newParams.delete('task')
    setSearchParams(newParams)
  }

  // Update URL when filter changes
  const handlePriorityFilterChange = (value: number | 'all') => {
    setPriorityFilter(value)
    const newParams = new URLSearchParams(searchParams)
    if (value === 'all') {
      newParams.delete('priority')
    } else {
      newParams.set('priority', value.toString())
    }
    setSearchParams(newParams)
  }

  // Update URL when status filter changes
  const handleStatusFilterChange = (value: string | 'all') => {
    setStatusFilter(value)
    const newParams = new URLSearchParams(searchParams)
    if (value === 'all') {
      newParams.delete('status')
    } else {
      newParams.set('status', value)
    }
    setSearchParams(newParams)
  }

  // Update URL when sort changes
  const handleSortOrderChange = (value: 'newest' | 'oldest' | 'priority') => {
    setSortOrder(value)
    const newParams = new URLSearchParams(searchParams)
    if (value === 'newest') {
      newParams.delete('sort') // default value, no need to store
    } else {
      newParams.set('sort', value)
    }
    setSearchParams(newParams)
  }

  // Update URL when view mode changes
  const handleViewModeChange = (value: 'kanban' | 'list') => {
    setViewMode(value)
    const newParams = new URLSearchParams(searchParams)
    if (value === 'kanban') {
      newParams.delete('view') // default value, no need to store
    } else {
      newParams.set('view', value)
    }
    setSearchParams(newParams)
  }

  // Reset all filters to defaults
  const handleResetFilters = () => {
    setPriorityFilter('all')
    setStatusFilter('all')
    setSortOrder('newest')
    setCurrentPage(1)
    const newParams = new URLSearchParams(searchParams)
    newParams.delete('priority')
    newParams.delete('status')
    newParams.delete('sort')
    setSearchParams(newParams)
  }

  // Check if filters are active (non-default)
  const hasActiveFilters = priorityFilter !== 'all' || statusFilter !== 'all' || sortOrder !== 'newest'

  // Filtered and sorted tasks
  const filteredAndSortedTasks = useMemo(() => {
    let result = [...tasks]

    // Apply priority filter
    if (priorityFilter !== 'all') {
      result = result.filter(task => task.priority === priorityFilter)
    }

    // Apply status filter
    if (statusFilter !== 'all') {
      result = result.filter(task => task.status === statusFilter)
    }

    // Apply sort
    result.sort((a, b) => {
      if (sortOrder === 'newest') {
        return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
      } else if (sortOrder === 'oldest') {
        return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
      } else {
        // priority - highest first
        return b.priority - a.priority
      }
    })

    return result
  }, [tasks, priorityFilter, statusFilter, sortOrder])

  // Pagination calculations
  const totalPages = Math.ceil(filteredAndSortedTasks.length / ITEMS_PER_PAGE)
  const paginatedTasks = useMemo(() => {
    const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
    return filteredAndSortedTasks.slice(startIndex, startIndex + ITEMS_PER_PAGE)
  }, [filteredAndSortedTasks, currentPage])

  // Reset page when filter changes
  useEffect(() => {
    setCurrentPage(1)
  }, [priorityFilter, statusFilter, sortOrder])

  const getTasksByStatus = (status: string) => {
    return filteredAndSortedTasks.filter((task) => task.status === status)
  }

  const handleCreateTask = async () => {
    if (!currentProject || !newTaskName.trim()) return
    setIsCreating(true)
    try {
      const taskData: TaskCreate = {
        name: newTaskName.trim(),
        description: newTaskDescription.trim() || undefined,
        priority: newTaskPriority,
        phase_id: newTaskPhaseId,
      }
      await createTask(currentProject.id, taskData)
      setNewTaskName('')
      setNewTaskDescription('')
      setNewTaskPriority(0)
      setNewTaskPhaseId(null)
      setShowAddDialog(false)
    } catch (error) {
      console.error('Failed to create task:', error)
      addToast('error', 'Create Failed', 'Failed to create task. Please try again.')
    } finally {
      setIsCreating(false)
    }
  }

  const handleDragStart = (taskId: number) => {
    setDraggedTask(taskId)
  }

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault()
  }

  const handleDrop = async (targetStatus: string) => {
    if (draggedTask === null) return
    try {
      await updateTaskStatus(draggedTask, targetStatus)
    } catch (error) {
      console.error('Failed to update task status:', error)
      addToast('error', 'Update Failed', 'Failed to update task status. Please try again.')
    }
    setDraggedTask(null)
  }

  const handleStartEdit = () => {
    if (!selectedTask) return
    setEditName(selectedTask.name)
    setEditDescription(selectedTask.description || '')
    setEditPriority(selectedTask.priority)
    setEditPhaseId(selectedTask.phase_id)
    setIsEditing(true)
  }

  // Check if edit form has unsaved changes
  const hasUnsavedChanges = () => {
    if (!selectedTask || !isEditing) return false
    return (
      editName.trim() !== selectedTask.name ||
      (editDescription.trim() || '') !== (selectedTask.description || '') ||
      editPriority !== selectedTask.priority ||
      editPhaseId !== selectedTask.phase_id
    )
  }

  const handleCancelEdit = () => {
    if (hasUnsavedChanges()) {
      setPendingCloseAction('cancel')
      setShowUnsavedDialog(true)
    } else {
      setIsEditing(false)
    }
  }

  const handleClosePanel = () => {
    if (hasUnsavedChanges()) {
      setPendingCloseAction('close')
      setShowUnsavedDialog(true)
    } else {
      setSelectedTask(null)
      setIsEditing(false)
      clearTaskFromUrl()
    }
  }

  const handleDiscardChanges = () => {
    setShowUnsavedDialog(false)
    if (pendingCloseAction === 'close') {
      setSelectedTask(null)
      clearTaskFromUrl()
    }
    setIsEditing(false)
    setPendingCloseAction(null)
  }

  const handleKeepEditing = () => {
    setShowUnsavedDialog(false)
    setPendingCloseAction(null)
  }

  const handleSaveEdit = async () => {
    if (!selectedTask) return
    setIsSaving(true)
    try {
      const updates: TaskUpdate = {
        name: editName.trim(),
        description: editDescription.trim() || undefined,
        priority: editPriority,
        phase_id: editPhaseId,
      }
      const updatedTask = await updateTask(selectedTask.id, updates)
      setSelectedTask(updatedTask)
      setIsEditing(false)
    } catch (error) {
      console.error('Failed to update task:', error)
      addToast('error', 'Save Failed', 'Failed to save task changes. Please try again.')
    } finally {
      setIsSaving(false)
    }
  }

  const handleDeleteTask = async () => {
    if (!selectedTask) return
    // Check if already being deleted (prevents rapid clicks)
    if (isTaskDeleting(selectedTask.id)) return

    const taskIdToDelete = selectedTask.id
    const taskName = selectedTask.name
    setIsDeleting(true)
    // Close dialog immediately to prevent additional clicks
    setShowDeleteDialog(false)

    try {
      await deleteTask(taskIdToDelete)
      setSelectedTask(null)
      clearTaskFromUrl()
    } catch (error) {
      console.error('Failed to delete task:', error)
      addToast('error', 'Delete Failed', `Failed to delete task "${taskName}". Please try again.`)
      // Re-open dialog if delete failed so user can retry
      setShowDeleteDialog(true)
    } finally {
      setIsDeleting(false)
    }
  }

  if (!currentProject) {
    return (

Select a project to view its Kanban board

    )
  }

  return (

      {/* Header */}

Kanban Board

          {/* Priority Filter */}

            <Filter size={16} className="text-muted-foreground" />
            <select
              value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
              onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
            >
              <option value="all">All Priorities</option>
              <option value="3">Critical</option>
              <option value="2">High</option>
              <option value="1">Medium</option>
              <option value="0">Low</option>
            </select>

          {/* Status Filter */}

            <select
              value={statusFilter}
              onChange={(e) => handleStatusFilterChange(e.target.value)}
              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
            >
              <option value="all">All Statuses</option>
              <option value="backlog">Backlog</option>
              <option value="in_progress">In Progress</option>
              <option value="qa">QA</option>
              <option value="done">Done</option>
              <option value="blocked">Blocked</option>
            </select>

          {/* Sort Order */}

            <ArrowUpDown size={16} className="text-muted-foreground" />
            <select
              value={sortOrder}
              onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
              className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
            >
              <option value="newest">Newest First</option>
              <option value="oldest">Oldest First</option>
              <option value="priority">By Priority</option>
            </select>

          {/* Reset Filters Button */}
          {hasActiveFilters && (
            <button
              onClick={handleResetFilters}
              className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-secondary rounded-lg transition-colors"
              title="Reset Filters"
            >
              <RefreshCw size={14} />
              Reset
            </button>
          )}

          {/* View Mode Toggle */}

            <button
              onClick={() => handleViewModeChange('kanban')}
              className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
              title="Kanban View"
            >
              <LayoutGrid size={18} />
            </button>
            <button
              onClick={() => handleViewModeChange('list')}
              className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
              title="List View"
            >
              <List size={18} />
            </button>

        <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
          <Dialog.Trigger asChild>
            <button className="btn-accent flex items-center gap-2">
              <Plus size={18} />
              Add Task
            </button>
          </Dialog.Trigger>
          <Dialog.Portal>
            <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
            <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">

                <Dialog.Title className="text-xl font-semibold text-foreground">
                  Add New Task
                </Dialog.Title>
                <Dialog.Close asChild>
                  <button className="text-muted-foreground hover:text-foreground" aria-label="Close dialog">
                    <X size={20} />
                  </button>
                </Dialog.Close>

              <Dialog.Description className="text-muted-foreground mb-4">
                Create a new task for this project.
              </Dialog.Description>

                    Task Name *

                  <input
                    type="text"
                    value={newTaskName}
                    onChange={(e) => setNewTaskName(e.target.value)}
                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                    placeholder="Enter task name"
                  />

                    Description

                  <textarea
                    value={newTaskDescription}
                    onChange={(e) => setNewTaskDescription(e.target.value)}
                    rows={3}
                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
                    placeholder="Enter task description"
                  />

                    Priority

                  <select
                    value={newTaskPriority}
                    onChange={(e) => setNewTaskPriority(Number(e.target.value))}
                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                  >
                    <option value={0}>Low</option>
                    <option value={1}>Medium</option>
                    <option value={2}>High</option>
                    <option value={3}>Critical</option>
                  </select>

                    Phase

                  <select
                    value={newTaskPhaseId ?? ''}
                    onChange={(e) => setNewTaskPhaseId(e.target.value ? Number(e.target.value) : null)}
                    className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                  >
                    <option value="">No Phase</option>
                    {phases.map((phase) => (
                      <option key={phase.id} value={phase.id}>{phase.name}</option>
                    ))}
                  </select>

                <Dialog.Close asChild>
                  <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
                    Cancel
                  </button>
                </Dialog.Close>
                <button
                  onClick={handleCreateTask}
                  disabled={isCreating || !newTaskName.trim()}
                  className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
                >
                  {isCreating ? 'Creating...' : 'Create Task'}
                </button>

            </Dialog.Content>
          </Dialog.Portal>
        </Dialog.Root>

      {/* Main Content Area */}

        {viewMode === 'list' ? (
          /* List View with Pagination */

            {/* Task List Table */}

                    Task Name
                    Status
                    Priority
                    Created

                  {isLoading && tasks.length === 0 ? (

                        Loading...

                  ) : paginatedTasks.length === 0 ? (

                        No tasks found

                  ) : (
                    paginatedTasks.map((task) => (

 handleSelectTask(task)}
                        className={`border-t border-border hover:bg-secondary/50 cursor-pointer ${
                          selectedTask?.id === task.id ? 'bg-secondary' : ''
                        }`}
                      >

{task.name}

                          {task.description && (

                              {task.description}

                          )}

                            {STATUS_LABELS[task.status] || task.status}

                            {PRIORITY_LABELS[task.priority] || 'Unknown'}

                          {new Date(task.created_at).toLocaleDateString()}

                    ))
                  )}

            {/* Pagination Controls */}
            {totalPages > 1 && (

                  Showing {((currentPage - 1) * ITEMS_PER_PAGE) + 1} to {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedTasks.length)} of {filteredAndSortedTasks.length} tasks

                  <button
                    onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
                    disabled={currentPage === 1}
                    className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
                  >
                    <ChevronLeft size={18} />
                  </button>

                    {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
                      <button
                        key={page}
                        onClick={() => setCurrentPage(page)}
                        className={`w-8 h-8 rounded-lg text-sm ${
                          page === currentPage
                            ? 'bg-accent text-white'
                            : 'bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary'
                        }`}
                      >
                        {page}
                      </button>
                    ))}

                  <button
                    onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
                    disabled={currentPage === totalPages}
                    className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
                  >
                    <ChevronRight size={18} />
                  </button>

            )}

        ) : (
        /* Kanban Columns */
        <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
          {COLUMNS.map((column) => (
          <div
            key={column.id}
            className={`flex-shrink-0 w-72 bg-card rounded-lg border-t-2 ${column.color}`}
            onDragOver={handleDragOver}
            onDrop={() => handleDrop(column.id)}
          >
            {/* Column Header */}

{column.title}

                  {getTasksByStatus(column.id).length}

            {/* Column Content */}

              {isLoading && tasks.length === 0 ? (

                  Loading...

              ) : getTasksByStatus(column.id).length === 0 ? (

                  No tasks

              ) : (
                getTasksByStatus(column.id).map((task) => (

 handleDragStart(task.id)}
                    onClick={() => handleSelectTask(task)}
                    className={`kanban-card bg-background rounded-lg p-4 border border-border cursor-pointer hover:border-muted-foreground ${
                      draggedTask === task.id ? 'opacity-50' : ''
                    } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
                  >

                      <GripVertical size={16} className="text-muted-foreground mt-0.5 flex-shrink-0" />

{task.name}

                        {task.description && (

                            {task.description}

                        )}

                          <span
                            className={`badge ${
                              column.id === 'backlog'
                                ? 'badge-backlog'
                                : column.id === 'in_progress'
                                ? 'badge-in-progress'
                                : column.id === 'qa'
                                ? 'badge-qa'
                                : column.id === 'done'
                                ? 'badge-done'
                                : 'badge-blocked'
                            }`}
                          >
                            {column.title}

                            {task.priority === 3 ? 'Critical' :
                             task.priority === 2 ? 'High' :
                             task.priority === 1 ? 'Medium' : 'Low'}

                ))
              )}

          ))}

        )}

        {/* Task Details Panel */}
        {selectedTask && (

Task Details

                {!isEditing && (
                  <>
                    <button
                      onClick={handleStartEdit}
                      className="text-muted-foreground hover:text-accent transition-colors"
                      title="Edit Task"
                    >
                      <Pencil size={18} />
                    </button>
                    <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
                      <AlertDialog.Trigger asChild>
                        <button
                          className="text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                          title="Delete Task"
                          disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
                        >
                          <Trash2 size={18} />
                        </button>
                      </AlertDialog.Trigger>
                      <AlertDialog.Portal>
                        <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
                        <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
                          <AlertDialog.Title className="text-xl font-semibold text-foreground">
                            Delete Task
                          </AlertDialog.Title>
                          <AlertDialog.Description className="mt-3 text-muted-foreground">
                            Are you sure you want to delete {selectedTask.name}?
                            This action cannot be undone.
                          </AlertDialog.Description>

                            <AlertDialog.Cancel asChild>
                              <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
                                Cancel
                              </button>
                            </AlertDialog.Cancel>
                            <AlertDialog.Action asChild>
                              <button
                                onClick={handleDeleteTask}
                                disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
                                className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
                              >
                                {isDeleting || (selectedTask && isTaskDeleting(selectedTask.id)) ? 'Deleting...' : 'Delete Task'}
                              </button>
                            </AlertDialog.Action>

                        </AlertDialog.Content>
                      </AlertDialog.Portal>
                    </AlertDialog.Root>
                  </>
                )}
                <button
                  onClick={handleClosePanel}
                  className="text-muted-foreground hover:text-foreground transition-colors"
                  title="Close"
                >
                  <X size={20} />
                </button>

              {isEditing ? (
                /* Edit Form */

                    Name *
                    <input
                      type="text"
                      value={editName}
                      onChange={(e) => setEditName(e.target.value)}
                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                      placeholder="Task name"
                    />

                    Description
                    <textarea
                      value={editDescription}
                      onChange={(e) => setEditDescription(e.target.value)}
                      rows={3}
                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
                      placeholder="Task description"
                    />

                    Priority
                    <select
                      value={editPriority}
                      onChange={(e) => setEditPriority(Number(e.target.value))}
                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                    >
                      <option value={0}>Low</option>
                      <option value={1}>Medium</option>
                      <option value={2}>High</option>
                      <option value={3}>Critical</option>
                    </select>

                    Phase
                    <select
                      value={editPhaseId ?? ''}
                      onChange={(e) => setEditPhaseId(e.target.value ? Number(e.target.value) : null)}
                      className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                    >
                      <option value="">No Phase</option>
                      {phases.map((phase) => (
                        <option key={phase.id} value={phase.id}>{phase.name}</option>
                      ))}
                    </select>

                    <button
                      onClick={handleCancelEdit}
                      className="flex-1 px-3 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
                    >
                      Cancel
                    </button>
                    <button
                      onClick={handleSaveEdit}
                      disabled={isSaving || !editName.trim()}
                      className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
                    >
                      {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
                    </button>

              ) : (
                /* View Mode */
                <>
                  {/* Task Name */}

                    Name

{selectedTask.name}

                  {/* Description */}

                    Description

                      {selectedTask.description || No description}

                  {/* Status */}

                    Status

                      {STATUS_LABELS[selectedTask.status] || selectedTask.status}

                  {/* Priority */}

                    Priority

                      {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}

                  {/* Phase */}

                    Phase

                      {selectedTask.phase_id ? (
                        phases.find(p => p.id === selectedTask.phase_id)?.name || 'Unknown Phase'
                      ) : (
                        No phase
                      )}

                  {/* Agent Assignment */}

                      <User size={14} />
                      Agent Assignment

                      {selectedTask.agent_type ? (
                        {selectedTask.agent_type.replace('_', ' ')}
                      ) : (
                        Not assigned
                      )}

                  {/* Iteration Count */}

                      <RefreshCw size={14} />
                      Iteration Count

{selectedTask.iteration_count}

                  {/* Flagged for Review */}

                      <Flag size={14} />
                      Flagged for Review

                      {selectedTask.flagged_for_review ? 'Yes' : 'No'}

                  {/* Time Estimates */}
                  {(selectedTask.estimated_time || selectedTask.actual_time) && (

                        <Clock size={14} />
                        Time

                        {selectedTask.estimated_time &&
Estimated: {selectedTask.estimated_time} min
}
                        {selectedTask.actual_time &&
Actual: {selectedTask.actual_time} min
}

                  )}

                  {/* Created/Updated timestamps */}

Created: {new Date(selectedTask.created_at).toLocaleString()}

Updated: {new Date(selectedTask.updated_at).toLocaleString()}

                </>
              )}

        )}

        {/* Unsaved Changes Warning Dialog */}
        <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
          <AlertDialog.Portal>
            <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
            <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
              <AlertDialog.Title className="text-xl font-semibold text-foreground">
                Unsaved Changes
              </AlertDialog.Title>
              <AlertDialog.Description className="mt-3 text-muted-foreground">
                You have unsaved changes. Are you sure you want to discard them?
              </AlertDialog.Description>

                <AlertDialog.Cancel asChild>
                  <button
                    onClick={handleKeepEditing}
                    className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
                  >
                    Keep Editing
                  </button>
                </AlertDialog.Cancel>
                <AlertDialog.Action asChild>
                  <button
                    onClick={handleDiscardChanges}
                    className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
                  >
                    Discard Changes
                  </button>
                </AlertDialog.Action>

            </AlertDialog.Content>
          </AlertDialog.Portal>
        </AlertDialog.Root>

  )
}


I can see the KanbanBoard component. Looking at it, I don't see any dependency visualization. The Task model has a `depends_on` field (which is `Optional[List[int]]`), but the UI doesn't currently display dependency indicators. Let me mark this feature as in progress and implement the dependency visualization:


Task Dependency Visualization

Verify that task dependencies are visualized on kanban board

Step 2: Create Task B that depends on Task A

Step 4: Verify dependency indicator on Task B

Step 5: Hover/click to see dependency details

Step 6: Verify Task A is listed as dependency


Now let me first check the taskStore to see how tasks are managed:


     1→import { create } from 'zustand'
     2→import { useToastStore } from '../components/Toast'
     3→
     4→export interface Task {
     5→  id: number
     6→  project_id: number
     7→  spec_id: number | null
     8→  phase_id: number | null
     9→  name: string
    10→  description: string | null
    11→  status: string
    12→  priority: number
    13→  estimated_time: number | null
    14→  actual_time: number | null
    15→  agent_type: string | null
    16→  iteration_count: number
    17→  flagged_for_review: boolean
    18→  depends_on: number[] | null
    19→  created_at: string
    20→  updated_at: string
    21→}
    22→
    23→export interface TaskCreate {
    24→  name: string
    25→  description?: string
    26→  priority?: number
    27→  phase_id?: number | null
    28→  agent_type?: string
    29→}
    30→
    31→export interface TaskUpdate {
    32→  name?: string
    33→  description?: string
    34→  priority?: number
    35→  phase_id?: number | null
    36→  status?: string
    37→  agent_type?: string
    38→}
    39→
    40→interface TaskStore {
    41→  tasks: Task[]
    42→  isLoading: boolean
    43→  error: string | null
    44→  deletingTaskIds: Set<number>
    45→  fetchTasks: (projectId: number) => Promise<void>
    46→  createTask: (projectId: number, task: TaskCreate) => Promise<Task>
    47→  updateTask: (taskId: number, updates: TaskUpdate) => Promise<Task>
    48→  updateTaskStatus: (taskId: number, status: string) => Promise<void>
    49→  deleteTask: (taskId: number) => Promise<void>
    50→  isTaskDeleting: (taskId: number) => boolean
    51→  clearTasks: () => void
    52→}
    53→
    54→const API_BASE = 'http://localhost:8000/api'
    55→
    56→export const useTaskStore = create<TaskStore>((set, get) => ({
    57→  tasks: [],
    58→  isLoading: false,
    59→  error: null,
    60→  deletingTaskIds: new Set<number>(),
    61→
    62→  fetchTasks: async (projectId: number) => {
    63→    set({ isLoading: true, error: null })
    64→    try {
    65→      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`)
    66→      if (!response.ok) throw new Error('Failed to fetch tasks')
    67→      const tasks = await response.json()
    68→      set({ tasks, isLoading: false })
    69→    } catch (error) {
    70→      const errorMessage = (error as Error).message
    71→      set({ error: errorMessage, isLoading: false })
    72→      useToastStore.getState().addToast('error', 'Failed to load tasks', errorMessage)
    73→    }
    74→  },
    75→
    76→  createTask: async (projectId: number, task: TaskCreate) => {
    77→    set({ isLoading: true, error: null })
    78→    try {
    79→      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`, {
    80→        method: 'POST',
    81→        headers: { 'Content-Type': 'application/json' },
    82→        body: JSON.stringify(task),
    83→      })
    84→      if (!response.ok) throw new Error('Failed to create task')
    85→      const newTask = await response.json()
    86→      set((state) => ({
    87→        tasks: [...state.tasks, newTask],
    88→        isLoading: false,
    89→      }))
    90→      return newTask
    91→    } catch (error) {
    92→      const errorMessage = (error as Error).message
    93→      set({ error: errorMessage, isLoading: false })
    94→      useToastStore.getState().addToast('error', 'Failed to create task', errorMessage)
    95→      throw error
    96→    }
    97→  },
    98→
    99→  updateTask: async (taskId: number, updates: TaskUpdate) => {
   100→    try {
   101→      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
   102→        method: 'PUT',
   103→        headers: { 'Content-Type': 'application/json' },
   104→        body: JSON.stringify(updates),
   105→      })
   106→      if (!response.ok) throw new Error('Failed to update task')
   107→      const updatedTask = await response.json()
   108→      set((state) => ({
   109→        tasks: state.tasks.map((t) =>
   110→          t.id === taskId ? updatedTask : t
   111→        ),
   112→      }))
   113→      return updatedTask
   114→    } catch (error) {
   115→      const errorMessage = (error as Error).message
   116→      set({ error: errorMessage })
   117→      useToastStore.getState().addToast('error', 'Failed to update task', errorMessage)
   118→      throw error
   119→    }
   120→  },
   121→
   122→  updateTaskStatus: async (taskId: number, status: string) => {
   123→    try {
   124→      const response = await fetch(`${API_BASE}/tasks/${taskId}/status`, {
   125→        method: 'PUT',
   126→        headers: { 'Content-Type': 'application/json' },
   127→        body: JSON.stringify({ status }),
   128→      })
   129→      if (!response.ok) throw new Error('Failed to update task status')
   130→      const updatedTask = await response.json()
   131→      set((state) => ({
   132→        tasks: state.tasks.map((t) =>
   133→          t.id === taskId ? updatedTask : t
   134→        ),
   135→      }))
   136→    } catch (error) {
   137→      const errorMessage = (error as Error).message
   138→      set({ error: errorMessage })
   139→      useToastStore.getState().addToast('error', 'Failed to update task status', errorMessage)
   140→      throw error
   141→    }
   142→  },
   143→
   144→  deleteTask: async (taskId: number) => {
   145→    // Prevent duplicate delete operations
   146→    const { deletingTaskIds } = get()
   147→    if (deletingTaskIds.has(taskId)) {
   148→      // Already deleting this task, silently return
   149→      return
   150→    }
   151→
   152→    // Mark task as being deleted
   153→    set((state) => ({
   154→      deletingTaskIds: new Set([...state.deletingTaskIds, taskId])
   155→    }))
   156→
   157→    try {
   158→      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
   159→        method: 'DELETE',
   160→      })
   161→      if (!response.ok) throw new Error('Failed to delete task')
   162→      set((state) => ({
   163→        tasks: state.tasks.filter((t) => t.id !== taskId),
   164→      }))
   165→    } catch (error) {
   166→      const errorMessage = (error as Error).message
   167→      set({ error: errorMessage })
   168→      useToastStore.getState().addToast('error', 'Failed to delete task', errorMessage)
   169→      throw error
   170→    } finally {
   171→      // Remove task from deleting set
   172→      set((state) => {
   173→        const newSet = new Set(state.deletingTaskIds)
   174→        newSet.delete(taskId)
   175→        return { deletingTaskIds: newSet }
   176→      })
   177→    }
   178→  },
   179→
   180→  isTaskDeleting: (taskId: number) => {
   181→    return get().deletingTaskIds.has(taskId)
   182→  },
   183→
   184→  clearTasks: () => {
   185→    set({ tasks: [], error: null })
   186→  },
   187→}))
   188→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import { create } from 'zustand'
import { useToastStore } from '../components/Toast'

export interface Task {
  id: number
  project_id: number
  spec_id: number | null
  phase_id: number | null
  name: string
  description: string | null
  status: string
  priority: number
  estimated_time: number | null
  actual_time: number | null
  agent_type: string | null
  iteration_count: number
  flagged_for_review: boolean
  depends_on: number[] | null
  created_at: string
  updated_at: string
}

export interface TaskCreate {
  name: string
  description?: string
  priority?: number
  phase_id?: number | null
  agent_type?: string
}

export interface TaskUpdate {
  name?: string
  description?: string
  priority?: number
  phase_id?: number | null
  status?: string
  agent_type?: string
}

interface TaskStore {
  tasks: Task[]
  isLoading: boolean
  error: string | null
  deletingTaskIds: Set<number>
  fetchTasks: (projectId: number) => Promise<void>
  createTask: (projectId: number, task: TaskCreate) => Promise<Task>
  updateTask: (taskId: number, updates: TaskUpdate) => Promise<Task>
  updateTaskStatus: (taskId: number, status: string) => Promise<void>
  deleteTask: (taskId: number) => Promise<void>
  isTaskDeleting: (taskId: number) => boolean
  clearTasks: () => void
}

const API_BASE = 'http://localhost:8000/api'

export const useTaskStore = create<TaskStore>((set, get) => ({
  tasks: [],
  isLoading: false,
  error: null,
  deletingTaskIds: new Set<number>(),

  fetchTasks: async (projectId: number) => {
    set({ isLoading: true, error: null })
    try {
      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`)
      if (!response.ok) throw new Error('Failed to fetch tasks')
      const tasks = await response.json()
      set({ tasks, isLoading: false })
    } catch (error) {
      const errorMessage = (error as Error).message
      set({ error: errorMessage, isLoading: false })
      useToastStore.getState().addToast('error', 'Failed to load tasks', errorMessage)
    }
  },

  createTask: async (projectId: number, task: TaskCreate) => {
    set({ isLoading: true, error: null })
    try {
      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(task),
      })
      if (!response.ok) throw new Error('Failed to create task')
      const newTask = await response.json()
      set((state) => ({
        tasks: [...state.tasks, newTask],
        isLoading: false,
      }))
      return newTask
    } catch (error) {
      const errorMessage = (error as Error).message
      set({ error: errorMessage, isLoading: false })
      useToastStore.getState().addToast('error', 'Failed to create task', errorMessage)
      throw error
    }
  },

  updateTask: async (taskId: number, updates: TaskUpdate) => {
    try {
      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updates),
      })
      if (!response.ok) throw new Error('Failed to update task')
      const updatedTask = await response.json()
      set((state) => ({
        tasks: state.tasks.map((t) =>
          t.id === taskId ? updatedTask : t
        ),
      }))
      return updatedTask
    } catch (error) {
      const errorMessage = (error as Error).message
      set({ error: errorMessage })
      useToastStore.getState().addToast('error', 'Failed to update task', errorMessage)
      throw error
    }
  },

  updateTaskStatus: async (taskId: number, status: string) => {
    try {
      const response = await fetch(`${API_BASE}/tasks/${taskId}/status`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status }),
      })
      if (!response.ok) throw new Error('Failed to update task status')
      const updatedTask = await response.json()
      set((state) => ({
        tasks: state.tasks.map((t) =>
          t.id === taskId ? updatedTask : t
        ),
      }))
    } catch (error) {
      const errorMessage = (error as Error).message
      set({ error: errorMessage })
      useToastStore.getState().addToast('error', 'Failed to update task status', errorMessage)
      throw error
    }
  },

  deleteTask: async (taskId: number) => {
    // Prevent duplicate delete operations
    const { deletingTaskIds } = get()
    if (deletingTaskIds.has(taskId)) {
      // Already deleting this task, silently return
      return
    }

    // Mark task as being deleted
    set((state) => ({
      deletingTaskIds: new Set([...state.deletingTaskIds, taskId])
    }))

    try {
      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
        method: 'DELETE',
      })
      if (!response.ok) throw new Error('Failed to delete task')
      set((state) => ({
        tasks: state.tasks.filter((t) => t.id !== taskId),
      }))
    } catch (error) {
      const errorMessage = (error as Error).message
      set({ error: errorMessage })
      useToastStore.getState().addToast('error', 'Failed to delete task', errorMessage)
      throw error
    } finally {
      // Remove task from deleting set
      set((state) => {
        const newSet = new Set(state.deletingTaskIds)
        newSet.delete(taskId)
        return { deletingTaskIds: newSet }
      })
    }
  },

  isTaskDeleting: (taskId: number) => {
    return get().deletingTaskIds.has(taskId)
  },

  clearTasks: () => {
    set({ tasks: [], error: null })
  },
}))


I can see that:
1. The Task interface has `depends_on: number[] | null` field (line 18)
2. The TaskCreate interface doesn't include depends_on (I'll need to add it)
3. The TaskUpdate interface doesn't include depends_on

Now I need to implement the dependency visualization. Let me:
1. Add a dependency indicator icon to task cards
2. Show dependency details when hovering/clicking
3. First, let me check if there are any tasks with dependencies already


Bash

curl -s http://localhost:8000/api/projects/5/tasks 2>/dev/null | grep -o '"depends_on":[^,}]*' | head -10

Check if any tasks have dependencies set


"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null
"depends_on":null


No tasks have dependencies currently. Let me first update the TaskCreate and TaskUpdate interfaces to support dependencies, then add the visualization. Let me also check what the API expects for the depends_on field:


     1→"""
     2→Tasks API Router
     3→
     4→Handles task CRUD, status updates, and task management.
     5→"""
     6→
     7→from typing import List, Optional
     8→from fastapi import APIRouter, Depends, HTTPException, Query, status
     9→from sqlalchemy.ext.asyncio import AsyncSession
    10→from pydantic import BaseModel, field_validator
    11→from datetime import datetime
    12→
    13→from services.database import get_db
    14→from models.task import Task
    15→
    16→router = APIRouter()
    17→
    18→
    19→class TaskCreate(BaseModel):
    20→    """Schema for creating a new task."""
    21→    name: str
    22→    description: Optional[str] = None
    23→    phase_id: Optional[int] = None
    24→    priority: int = 0
    25→    estimated_time: Optional[int] = None
    26→    agent_type: Optional[str] = None
    27→    depends_on: Optional[List[int]] = None
    28→
    29→    @field_validator('name')
    30→    @classmethod
    31→    def validate_name(cls, v: str) -> str:
    32→        """Validate task name - reject whitespace-only."""
    33→        stripped = v.strip() if v else ''
    34→        if not stripped:
    35→            raise ValueError('Task name is required')
    36→        return stripped
    37→
    38→
    39→class TaskUpdate(BaseModel):
    40→    """Schema for updating a task."""
    41→    name: Optional[str] = None
    42→    description: Optional[str] = None
    43→    status: Optional[str] = None
    44→    phase_id: Optional[int] = None
    45→    priority: Optional[int] = None
    46→    estimated_time: Optional[int] = None
    47→    actual_time: Optional[int] = None
    48→    agent_type: Optional[str] = None
    49→    iteration_count: Optional[int] = None
    50→    flagged_for_review: Optional[bool] = None
    51→    depends_on: Optional[List[int]] = None
    52→
    53→    @field_validator('name')
    54→    @classmethod
    55→    def validate_name(cls, v: Optional[str]) -> Optional[str]:
    56→        """Validate task name - reject whitespace-only."""
    57→        if v is None:
    58→            return v
    59→        stripped = v.strip() if v else ''
    60→        if not stripped:
    61→            raise ValueError('Task name is required')
    62→        return stripped
    63→
    64→
    65→class TaskStatusUpdate(BaseModel):
    66→    """Schema for updating task status."""
    67→    status: str
    68→
    69→
    70→class TaskResponse(BaseModel):

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
Tasks API Router

Handles task CRUD, status updates, and task management.
"""

from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, field_validator
from datetime import datetime

from services.database import get_db
from models.task import Task

router = APIRouter()

class TaskCreate(BaseModel):
    """Schema for creating a new task."""
    name: str
    description: Optional[str] = None
    phase_id: Optional[int] = None
    priority: int = 0
    estimated_time: Optional[int] = None
    agent_type: Optional[str] = None
    depends_on: Optional[List[int]] = None

    @field_validator('name')
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate task name - reject whitespace-only."""
        stripped = v.strip() if v else ''
        if not stripped:
            raise ValueError('Task name is required')
        return stripped

class TaskUpdate(BaseModel):
    """Schema for updating a task."""
    name: Optional[str] = None
    description: Optional[str] = None
    status: Optional[str] = None
    phase_id: Optional[int] = None
    priority: Optional[int] = None
    estimated_time: Optional[int] = None
    actual_time: Optional[int] = None
    agent_type: Optional[str] = None
    iteration_count: Optional[int] = None
    flagged_for_review: Optional[bool] = None
    depends_on: Optional[List[int]] = None

    @field_validator('name')
    @classmethod
    def validate_name(cls, v: Optional[str]) -> Optional[str]:
        """Validate task name - reject whitespace-only."""
        if v is None:
            return v
        stripped = v.strip() if v else ''
        if not stripped:
            raise ValueError('Task name is required')
        return stripped

class TaskStatusUpdate(BaseModel):
    """Schema for updating task status."""
    status: str

class TaskResponse(BaseModel):


The backend already supports `depends_on` in both TaskCreate and TaskUpdate (lines 27 and 51). Now I need to:

1. Update the frontend TaskCreate and TaskUpdate interfaces to include depends_on
2. Add dependency visualization to the task cards
3. Add dependency display in task details panel

Let me first update the taskStore interfaces:


Edit

export interface TaskCreate {
  name: string
  description?: string
  priority?: number
  phase_id?: number | null
  agent_type?: string
}

export interface TaskCreate {
  name: string
  description?: string
  priority?: number
  phase_id?: number | null
  agent_type?: string
  depends_on?: number[] | null
}