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

   315→
Select a project to view its Kanban board

   316→

   317→    )
   318→  }
   319→
   320→  return (
   321→

   322→      {/* Header */}
   323→

   324→
Kanban Board

   325→

   326→          {/* Priority Filter */}
   327→

   328→            <Filter size={16} className="text-slate-400" />
   329→            <select
   330→              value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
   331→              onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
   332→              className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
   333→            >
   334→              <option value="all">All Priorities</option>
   335→              <option value="3">Critical</option>
   336→              <option value="2">High</option>
   337→              <option value="1">Medium</option>
   338→              <option value="0">Low</option>
   339→            </select>
   340→

   341→
   342→          {/* Sort Order */}
   343→

   344→            <ArrowUpDown size={16} className="text-slate-400" />
   345→            <select
   346→              value={sortOrder}
   347→              onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
   348→              className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
   349→            >
   350→              <option value="newest">Newest First</option>
   351→              <option value="oldest">Oldest First</option>
   352→              <option value="priority">By Priority</option>
   353→            </select>
   354→

   355→
   356→          {/* View Mode Toggle */}
   357→

   358→            <button
   359→              onClick={() => handleViewModeChange('kanban')}
   360→              className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
   361→              title="Kanban View"
   362→            >
   363→              <LayoutGrid size={18} />
   364→            </button>
   365→            <button
   366→              onClick={() => handleViewModeChange('list')}
   367→              className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
   368→              title="List View"
   369→            >
   370→              <List size={18} />
   371→            </button>
   372→

   373→
   374→        <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
   375→          <Dialog.Trigger asChild>
   376→            <button className="btn-accent flex items-center gap-2">
   377→              <Plus size={18} />
   378→              Add Task
   379→            </button>
   380→          </Dialog.Trigger>
   381→          <Dialog.Portal>
   382→            <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
   383→            <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
   384→

   385→                <Dialog.Title className="text-xl font-semibold text-white">
   386→                  Add New Task
   387→                </Dialog.Title>
   388→                <Dialog.Close asChild>
   389→                  <button className="text-slate-400 hover:text-white">
   390→                    <X size={20} />
   391→                  </button>
   392→                </Dialog.Close>
   393→

   394→              <Dialog.Description className="text-slate-400 mb-4">
   395→                Create a new task for this project.
   396→              </Dialog.Description>
   397→

   398→

   399→
   400→                    Task Name *
   401→
   402→                  <input
   403→                    type="text"
   404→                    value={newTaskName}
   405→                    onChange={(e) => setNewTaskName(e.target.value)}
   406→                    className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   407→                    placeholder="Enter task name"
   408→                  />
   409→

   410→

   411→
   412→                    Description
   413→
   414→                  <textarea
   415→                    value={newTaskDescription}
   416→                    onChange={(e) => setNewTaskDescription(e.target.value)}
   417→                    rows={3}
   418→                    className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
   419→                    placeholder="Enter task description"
   420→                  />
   421→

   422→

   423→
   424→                    Priority
   425→
   426→                  <select
   427→                    value={newTaskPriority}
   428→                    onChange={(e) => setNewTaskPriority(Number(e.target.value))}
   429→                    className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   430→                  >
   431→                    <option value={0}>Low</option>
   432→                    <option value={1}>Medium</option>
   433→                    <option value={2}>High</option>
   434→                    <option value={3}>Critical</option>
   435→                  </select>
   436→

   437→

   438→

   439→                <Dialog.Close asChild>
   440→                  <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
   441→                    Cancel
   442→                  </button>
   443→                </Dialog.Close>
   444→                <button
   445→                  onClick={handleCreateTask}
   446→                  disabled={isCreating || !newTaskName.trim()}
   447→                  className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
   448→                >
   449→                  {isCreating ? 'Creating...' : 'Create Task'}
   450→                </button>
   451→

   452→            </Dialog.Content>
   453→          </Dialog.Portal>
   454→        </Dialog.Root>
   455→

   456→

   457→
   458→      {/* Main Content Area */}
   459→

   460→        {viewMode === 'list' ? (
   461→          /* List View with Pagination */
   462→

   463→            {/* Task List Table */}
   464→

   465→

   466→

   467→

   468→                    Task Name
   469→                    Status
   470→                    Priority
   471→                    Created
   472→

   473→

   474→

   475→                  {isLoading && tasks.length === 0 ? (
   476→

   477→
   478→                        Loading...
   479→
   480→

   481→                  ) : paginatedTasks.length === 0 ? (
   482→

   483→
   484→                        No tasks found
   485→
   486→

   487→                  ) : (
   488→                    paginatedTasks.map((task) => (
   489→
 handleSelectTask(task)}
   492→                        className={`border-t border-slate-700 hover:bg-slate-700/50 cursor-pointer ${
   493→                          selectedTask?.id === task.id ? 'bg-slate-700' : ''
   494→                        }`}
   495→                      >
   496→
   497→
{task.name}

   498→                          {task.description && (
   499→

   500→                              {task.description}
   501→

   502→                          )}
   503→
   504→
   505→
   512→                            {STATUS_LABELS[task.status] || task.status}
   513→
   514→
   515→
   516→
   522→                            {PRIORITY_LABELS[task.priority] || 'Unknown'}
   523→
   524→
   525→
   526→                          {new Date(task.created_at).toLocaleDateString()}
   527→
   528→

   529→                    ))
   530→                  )}
   531→

   532→

   533→

   534→
   535→            {/* Pagination Controls */}
   536→            {totalPages > 1 && (
   537→

   538→

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

   541→

   542→                  <button
   543→                    onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
   544→                    disabled={currentPage === 1}
   545→                    className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
   546→                  >
   547→                    <ChevronLeft size={18} />
   548→                  </button>
   549→

   550→                    {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
   551→                      <button
   552→                        key={page}
   553→                        onClick={() => setCurrentPage(page)}
   554→                        className={`w-8 h-8 rounded-lg text-sm ${
   555→                          page === currentPage
   556→                            ? 'bg-accent text-white'
   557→                            : 'bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700'
   558→                        }`}
   559→                      >
   560→                        {page}
   561→                      </button>
   562→                    ))}
   563→

   564→                  <button
   565→                    onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
   566→                    disabled={currentPage === totalPages}
   567→                    className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
   568→                  >
   569→                    <ChevronRight size={18} />
   570→                  </button>
   571→

   572→

   573→            )}
   574→

   575→        ) : (
   576→        /* Kanban Columns */
   577→        <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
   578→          {COLUMNS.map((column) => (
   579→          <div
   580→            key={column.id}
   581→            className={`flex-shrink-0 w-72 bg-slate-800 rounded-lg border-t-2 ${column.color}`}
   582→            onDragOver={handleDragOver}
   583→            onDrop={() => handleDrop(column.id)}
   584→          >
   585→            {/* Column Header */}
   586→

   587→

   588→
{column.title}

   589→
   590→                  {getTasksByStatus(column.id).length}
   591→
   592→

   593→

   594→
   595→            {/* Column Content */}
   596→

   597→              {isLoading && tasks.length === 0 ? (
   598→

   599→                  Loading...
   600→

   601→              ) : getTasksByStatus(column.id).length === 0 ? (
   602→

   603→                  No tasks
   604→

   605→              ) : (
   606→                getTasksByStatus(column.id).map((task) => (
   607→
 handleDragStart(task.id)}
   611→                    onClick={() => handleSelectTask(task)}
   612→                    className={`kanban-card bg-slate-900 rounded-lg p-4 border border-slate-700 cursor-pointer hover:border-slate-600 ${
   613→                      draggedTask === task.id ? 'opacity-50' : ''
   614→                    } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
   615→                  >
   616→

   617→                      <GripVertical size={16} className="text-slate-600 mt-0.5 flex-shrink-0" />
   618→

   619→
{task.name}

   620→                        {task.description && (
   621→

   622→                            {task.description}
   623→

   624→                        )}
   625→

   626→                          <span
   627→                            className={`badge ${
   628→                              column.id === 'backlog'
   629→                                ? 'badge-backlog'
   630→                                : column.id === 'in_progress'
   631→                                ? 'badge-in-progress'
   632→                                : column.id === 'qa'
   633→                                ? 'badge-qa'
   634→                                : column.id === 'done'
   635→                                ? 'badge-done'
   636→                                : 'badge-blocked'
   637→                            }`}
   638→                          >
   639→                            {column.title}
   640→
   641→
   647→                            {task.priority === 3 ? 'Critical' :
   648→                             task.priority === 2 ? 'High' :
   649→                             task.priority === 1 ? 'Medium' : 'Low'}
   650→
   651→

   652→

   653→

   654→

   655→                ))
   656→              )}
   657→

   658→

   659→          ))}
   660→

   661→        )}
   662→
   663→        {/* Task Details Panel */}
   664→        {selectedTask && (
   665→

   666→

   667→
Task Details

   668→

   669→                {!isEditing && (
   670→                  <>
   671→                    <button
   672→                      onClick={handleStartEdit}
   673→                      className="text-slate-400 hover:text-accent transition-colors"
   674→                      title="Edit Task"
   675→                    >
   676→                      <Pencil size={18} />
   677→                    </button>
   678→                    <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
   679→                      <AlertDialog.Trigger asChild>
   680→                        <button
   681→                          className="text-slate-400 hover:text-destructive transition-colors"
   682→                          title="Delete Task"
   683→                        >
   684→                          <Trash2 size={18} />
   685→                        </button>
   686→                      </AlertDialog.Trigger>
   687→                      <AlertDialog.Portal>
   688→                        <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
   689→                        <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
   690→                          <AlertDialog.Title className="text-xl font-semibold text-white">
   691→                            Delete Task
   692→                          </AlertDialog.Title>
   693→                          <AlertDialog.Description className="mt-3 text-slate-400">
   694→                            Are you sure you want to delete {selectedTask.name}?
   695→                            This action cannot be undone.
   696→                          </AlertDialog.Description>
   697→

   698→                            <AlertDialog.Cancel asChild>
   699→                              <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
   700→                                Cancel
   701→                              </button>
   702→                            </AlertDialog.Cancel>
   703→                            <AlertDialog.Action asChild>
   704→                              <button
   705→                                onClick={handleDeleteTask}
   706→                                disabled={isDeleting}
   707→                                className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
   708→                              >
   709→                                {isDeleting ? 'Deleting...' : 'Delete Task'}
   710→                              </button>
   711→                            </AlertDialog.Action>
   712→

   713→                        </AlertDialog.Content>
   714→                      </AlertDialog.Portal>
   715→                    </AlertDialog.Root>
   716→                  </>
   717→                )}
   718→                <button
   719→                  onClick={handleClosePanel}
   720→                  className="text-slate-400 hover:text-white transition-colors"
   721→                  title="Close"
   722→                >
   723→                  <X size={20} />
   724→                </button>
   725→

   726→

   727→

   728→              {isEditing ? (
   729→                /* Edit Form */
   730→

   731→

   732→                    Name *
   733→                    <input
   734→                      type="text"
   735→                      value={editName}
   736→                      onChange={(e) => setEditName(e.target.value)}
   737→                      className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   738→                      placeholder="Task name"
   739→                    />
   740→

   741→

   742→                    Description
   743→                    <textarea
   744→                      value={editDescription}
   745→                      onChange={(e) => setEditDescription(e.target.value)}
   746→                      rows={3}
   747→                      className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
   748→                      placeholder="Task description"
   749→                    />
   750→

   751→

   752→                    Priority
   753→                    <select
   754→                      value={editPriority}
   755→                      onChange={(e) => setEditPriority(Number(e.target.value))}
   756→                      className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
   757→                    >
   758→                      <option value={0}>Low</option>
   759→                      <option value={1}>Medium</option>
   760→                      <option value={2}>High</option>
   761→                      <option value={3}>Critical</option>
   762→                    </select>
   763→

   764→

   765→                    <button
   766→                      onClick={handleCancelEdit}
   767→                      className="flex-1 px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
   768→                    >
   769→                      Cancel
   770→                    </button>
   771→                    <button
   772→                      onClick={handleSaveEdit}
   773→                      disabled={isSaving || !editName.trim()}
   774→                      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"
   775→                    >
   776→                      {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
   777→                    </button>
   778→

   779→

   780→              ) : (
   781→                /* View Mode */
   782→                <>
   783→                  {/* Task Name */}
   784→

   785→                    Name
   786→
{selectedTask.name}

   787→

   788→
   789→                  {/* Description */}
   790→

   791→                    Description
   792→

   793→                      {selectedTask.description || No description}
   794→

   795→

   796→
   797→                  {/* Status */}
   798→

   799→                    Status
   800→
   807→                      {STATUS_LABELS[selectedTask.status] || selectedTask.status}
   808→
   809→

   810→
   811→                  {/* Priority */}
   812→

   813→                    Priority
   814→
   820→                      {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}
   821→
   822→

   823→
   824→                  {/* Agent Assignment */}
   825→

   826→
   827→                      <User size={14} />
   828→                      Agent Assignment
   829→
   830→

   831→                      {selectedTask.agent_type ? (
   832→                        {selectedTask.agent_type.replace('_', ' ')}
   833→                      ) : (
   834→                        Not assigned
   835→                      )}
   836→

   837→

   838→
   839→                  {/* Iteration Count */}
   840→

   841→
   842→                      <RefreshCw size={14} />
   843→                      Iteration Count
   844→
   845→
{selectedTask.iteration_count}

   846→

   847→
   848→                  {/* Flagged for Review */}
   849→

   850→
   851→                      <Flag size={14} />
   852→                      Flagged for Review
   853→
   854→

   855→                      {selectedTask.flagged_for_review ? 'Yes' : 'No'}
   856→

   857→

   858→
   859→                  {/* Time Estimates */}
   860→                  {(selectedTask.estimated_time || selectedTask.actual_time) && (
   861→

   862→
   863→                        <Clock size={14} />
   864→                        Time
   865→
   866→

   867→                        {selectedTask.estimated_time &&
Estimated: {selectedTask.estimated_time} min
}
   868→                        {selectedTask.actual_time &&
Actual: {selectedTask.actual_time} min
}
   869→

   870→

   871→                  )}
   872→
   873→                  {/* Created/Updated timestamps */}
   874→

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

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

   877→

   878→                </>
   879→              )}
   880→

   881→

   882→        )}
   883→
   884→        {/* Unsaved Changes Warning Dialog */}
   885→        <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
   886→          <AlertDialog.Portal>
   887→            <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
   888→            <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
   889→              <AlertDialog.Title className="text-xl font-semibold text-white">
   890→                Unsaved Changes
   891→              </AlertDialog.Title>
   892→              <AlertDialog.Description className="mt-3 text-slate-400">
   893→                You have unsaved changes. Are you sure you want to discard them?
   894→              </AlertDialog.Description>
   895→

   896→                <AlertDialog.Cancel asChild>
   897→                  <button
   898→                    onClick={handleKeepEditing}
   899→                    className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
   900→                  >
   901→                    Keep Editing
   902→                  </button>
   903→                </AlertDialog.Cancel>
   904→                <AlertDialog.Action asChild>
   905→                  <button
   906→                    onClick={handleDiscardChanges}
   907→                    className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
   908→                  >
   909→                    Discard Changes
   910→                  </button>
   911→                </AlertDialog.Action>
   912→

   913→            </AlertDialog.Content>
   914→          </AlertDialog.Portal>
   915→        </AlertDialog.Root>
   916→

   917→

   918→  )
   919→}
   920→

<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'

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, isLoading } = useTaskStore()
  const [showAddDialog, setShowAddDialog] = useState(false)
  const [newTaskName, setNewTaskName] = useState('')
  const [newTaskDescription, setNewTaskDescription] = useState('')
  const [newTaskPriority, setNewTaskPriority] = useState(0)
  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 [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 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 [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)
    }
  }, [currentProject?.id, fetchTasks])

  // 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 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)
  }

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

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

    // 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, 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, 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,
      }
      await createTask(currentProject.id, taskData)
      setNewTaskName('')
      setNewTaskDescription('')
      setNewTaskPriority(0)
      setShowAddDialog(false)
    } catch (error) {
      console.error('Failed to create task:', error)
    } 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)
    }
    setDraggedTask(null)
  }

  const handleStartEdit = () => {
    if (!selectedTask) return
    setEditName(selectedTask.name)
    setEditDescription(selectedTask.description || '')
    setEditPriority(selectedTask.priority)
    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
    )
  }

  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,
      }
      const updatedTask = await updateTask(selectedTask.id, updates)
      setSelectedTask(updatedTask)
      setIsEditing(false)
    } catch (error) {
      console.error('Failed to update task:', error)
    } finally {
      setIsSaving(false)
    }
  }

  const handleDeleteTask = async () => {
    if (!selectedTask) return
    setIsDeleting(true)
    try {
      await deleteTask(selectedTask.id)
      setSelectedTask(null)
      setShowDeleteDialog(false)
    } catch (error) {
      console.error('Failed to delete task:', error)
    } 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-slate-400" />
            <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-slate-800 border border-slate-700 rounded-lg text-sm text-white 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>

          {/* Sort Order */}

            <ArrowUpDown size={16} className="text-slate-400" />
            <select
              value={sortOrder}
              onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
              className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white 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>

          {/* View Mode Toggle */}

            <button
              onClick={() => handleViewModeChange('kanban')}
              className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
              title="Kanban View"
            >
              <LayoutGrid size={18} />
            </button>
            <button
              onClick={() => handleViewModeChange('list')}
              className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
              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-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">

                <Dialog.Title className="text-xl font-semibold text-white">
                  Add New Task
                </Dialog.Title>
                <Dialog.Close asChild>
                  <button className="text-slate-400 hover:text-white">
                    <X size={20} />
                  </button>
                </Dialog.Close>

              <Dialog.Description className="text-slate-400 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-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                    placeholder="Enter task name"
                  />

                    Description

                  <textarea
                    value={newTaskDescription}
                    onChange={(e) => setNewTaskDescription(e.target.value)}
                    rows={3}
                    className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent 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-slate-900 border border-slate-700 rounded-lg text-white 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>

                <Dialog.Close asChild>
                  <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 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-slate-700 hover:bg-slate-700/50 cursor-pointer ${
                          selectedTask?.id === task.id ? 'bg-slate-700' : ''
                        }`}
                      >

{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-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 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-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700'
                        }`}
                      >
                        {page}
                      </button>
                    ))}

                  <button
                    onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
                    disabled={currentPage === totalPages}
                    className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 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-slate-800 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-slate-900 rounded-lg p-4 border border-slate-700 cursor-pointer hover:border-slate-600 ${
                      draggedTask === task.id ? 'opacity-50' : ''
                    } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
                  >

                      <GripVertical size={16} className="text-slate-600 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-slate-400 hover:text-accent transition-colors"
                      title="Edit Task"
                    >
                      <Pencil size={18} />
                    </button>
                    <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
                      <AlertDialog.Trigger asChild>
                        <button
                          className="text-slate-400 hover:text-destructive transition-colors"
                          title="Delete Task"
                        >
                          <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-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
                          <AlertDialog.Title className="text-xl font-semibold text-white">
                            Delete Task
                          </AlertDialog.Title>
                          <AlertDialog.Description className="mt-3 text-slate-400">
                            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-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
                                Cancel
                              </button>
                            </AlertDialog.Cancel>
                            <AlertDialog.Action asChild>
                              <button
                                onClick={handleDeleteTask}
                                disabled={isDeleting}
                                className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
                              >
                                {isDeleting ? 'Deleting...' : 'Delete Task'}
                              </button>
                            </AlertDialog.Action>

                        </AlertDialog.Content>
                      </AlertDialog.Portal>
                    </AlertDialog.Root>
                  </>
                )}
                <button
                  onClick={handleClosePanel}
                  className="text-slate-400 hover:text-white 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-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
                      placeholder="Task name"
                    />

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

                    Priority
                    <select
                      value={editPriority}
                      onChange={(e) => setEditPriority(Number(e.target.value))}
                      className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white 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>

                    <button
                      onClick={handleCancelEdit}
                      className="flex-1 px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 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'}

                  {/* 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-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
              <AlertDialog.Title className="text-xl font-semibold text-white">
                Unsaved Changes
              </AlertDialog.Title>
              <AlertDialog.Description className="mt-3 text-slate-400">
                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-slate-300 hover:bg-slate-700 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>

  )
}


Now let me check the task store to understand how the delete function works:


     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→  agent_type?: string
    28→}
    29→
    30→export interface TaskUpdate {
    31→  name?: string
    32→  description?: string
    33→  priority?: number
    34→  status?: string
    35→  agent_type?: string
    36→}
    37→
    38→interface TaskStore {
    39→  tasks: Task[]
    40→  isLoading: boolean
    41→  error: string | null
    42→  fetchTasks: (projectId: number) => Promise<void>
    43→  createTask: (projectId: number, task: TaskCreate) => Promise<Task>
    44→  updateTask: (taskId: number, updates: TaskUpdate) => Promise<Task>
    45→  updateTaskStatus: (taskId: number, status: string) => Promise<void>
    46→  deleteTask: (taskId: number) => Promise<void>
    47→  clearTasks: () => void
    48→}
    49→
    50→const API_BASE = 'http://localhost:8000/api'
    51→
    52→export const useTaskStore = create<TaskStore>((set, get) => ({
    53→  tasks: [],
    54→  isLoading: false,
    55→  error: null,
    56→
    57→  fetchTasks: async (projectId: number) => {
    58→    set({ isLoading: true, error: null })
    59→    try {
    60→      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`)
    61→      if (!response.ok) throw new Error('Failed to fetch tasks')
    62→      const tasks = await response.json()
    63→      set({ tasks, isLoading: false })
    64→    } catch (error) {
    65→      const errorMessage = (error as Error).message
    66→      set({ error: errorMessage, isLoading: false })
    67→      useToastStore.getState().addToast('error', 'Failed to load tasks', errorMessage)
    68→    }
    69→  },
    70→
    71→  createTask: async (projectId: number, task: TaskCreate) => {
    72→    set({ isLoading: true, error: null })
    73→    try {
    74→      const response = await fetch(`${API_BASE}/projects/${projectId}/tasks`, {
    75→        method: 'POST',
    76→        headers: { 'Content-Type': 'application/json' },
    77→        body: JSON.stringify(task),
    78→      })
    79→      if (!response.ok) throw new Error('Failed to create task')
    80→      const newTask = await response.json()
    81→      set((state) => ({
    82→        tasks: [...state.tasks, newTask],
    83→        isLoading: false,
    84→      }))
    85→      return newTask
    86→    } catch (error) {
    87→      const errorMessage = (error as Error).message
    88→      set({ error: errorMessage, isLoading: false })
    89→      useToastStore.getState().addToast('error', 'Failed to create task', errorMessage)
    90→      throw error
    91→    }
    92→  },
    93→
    94→  updateTask: async (taskId: number, updates: TaskUpdate) => {
    95→    try {
    96→      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
    97→        method: 'PUT',
    98→        headers: { 'Content-Type': 'application/json' },
    99→        body: JSON.stringify(updates),
   100→      })
   101→      if (!response.ok) throw new Error('Failed to update task')
   102→      const updatedTask = await response.json()
   103→      set((state) => ({
   104→        tasks: state.tasks.map((t) =>
   105→          t.id === taskId ? updatedTask : t
   106→        ),
   107→      }))
   108→      return updatedTask
   109→    } catch (error) {
   110→      const errorMessage = (error as Error).message
   111→      set({ error: errorMessage })
   112→      useToastStore.getState().addToast('error', 'Failed to update task', errorMessage)
   113→      throw error
   114→    }
   115→  },
   116→
   117→  updateTaskStatus: async (taskId: number, status: string) => {
   118→    try {
   119→      const response = await fetch(`${API_BASE}/tasks/${taskId}/status`, {
   120→        method: 'PUT',
   121→        headers: { 'Content-Type': 'application/json' },
   122→        body: JSON.stringify({ status }),
   123→      })
   124→      if (!response.ok) throw new Error('Failed to update task status')
   125→      const updatedTask = await response.json()
   126→      set((state) => ({
   127→        tasks: state.tasks.map((t) =>
   128→          t.id === taskId ? updatedTask : t
   129→        ),
   130→      }))
   131→    } catch (error) {
   132→      const errorMessage = (error as Error).message
   133→      set({ error: errorMessage })
   134→      useToastStore.getState().addToast('error', 'Failed to update task status', errorMessage)
   135→      throw error
   136→    }
   137→  },
   138→
   139→  deleteTask: async (taskId: number) => {
   140→    try {
   141→      const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
   142→        method: 'DELETE',
   143→      })
   144→      if (!response.ok) throw new Error('Failed to delete task')
   145→      set((state) => ({
   146→        tasks: state.tasks.filter((t) => t.id !== taskId),
   147→      }))
   148→    } catch (error) {
   149→      const errorMessage = (error as Error).message
   150→      set({ error: errorMessage })
   151→      useToastStore.getState().addToast('error', 'Failed to delete task', errorMessage)
   152→      throw error
   153→    }
   154→  },
   155→
   156→  clearTasks: () => {
   157→    set({ tasks: [], error: null })
   158→  },
   159→}))
   160→

<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
  agent_type?: string
}

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

interface TaskStore {
  tasks: Task[]
  isLoading: boolean
  error: string | null
  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>
  clearTasks: () => void
}

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

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

  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) => {
    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
    }
  },

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


Now I understand the current implementation. The issue is that when a user clicks the delete button rapidly multiple times, the delete operation could be triggered multiple times because:

1. The `isDeleting` state is set, but the dialog remains open
2. The button could be clicked again before the API call completes
3. The second delete call would try to delete an already-deleted task and fail with a 404 error

Looking at the code:
- `handleDeleteTask` (line 298-310) sets `isDeleting` and then calls `deleteTask`
- The delete button (line 704-710) is disabled when `isDeleting` is true

The issue is that `AlertDialog.Action` uses `asChild` and the button is only disabled via the `disabled` prop. However, the dialog could remain open briefly, allowing rapid clicks.

Let me check the backend to see how it handles deleting an already-deleted task:


Found 2 files
backend\models\task.py
backend\api\tasks.py


     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
    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→
    30→class TaskUpdate(BaseModel):
    31→    """Schema for updating a task."""
    32→    name: Optional[str] = None
    33→    description: Optional[str] = None
    34→    status: Optional[str] = None
    35→    priority: Optional[int] = None
    36→    estimated_time: Optional[int] = None
    37→    actual_time: Optional[int] = None
    38→    agent_type: Optional[str] = None
    39→    iteration_count: Optional[int] = None
    40→    flagged_for_review: Optional[bool] = None
    41→    depends_on: Optional[List[int]] = None
    42→
    43→
    44→class TaskStatusUpdate(BaseModel):
    45→    """Schema for updating task status."""
    46→    status: str
    47→
    48→
    49→class TaskResponse(BaseModel):
    50→    """Schema for task response."""
    51→    id: int
    52→    project_id: int
    53→    spec_id: Optional[int]
    54→    phase_id: Optional[int]
    55→    name: str
    56→    description: Optional[str]
    57→    status: str
    58→    priority: int
    59→    estimated_time: Optional[int]
    60→    actual_time: Optional[int]
    61→    agent_type: Optional[str]
    62→    iteration_count: int
    63→    flagged_for_review: bool
    64→    depends_on: Optional[List[int]]
    65→    created_at: datetime
    66→    updated_at: datetime
    67→
    68→    class Config:
    69→        from_attributes = True
    70→
    71→
    72→@router.get("/projects/{project_id}/tasks", response_model=List[TaskResponse])
    73→async def list_tasks(
    74→    project_id: int,
    75→    status: Optional[str] = Query(None, description="Filter by status"),
    76→    phase_id: Optional[int] = Query(None, description="Filter by phase"),
    77→    agent_type: Optional[str] = Query(None, description="Filter by agent type"),
    78→    flagged: Optional[bool] = Query(None, description="Filter by flagged status"),
    79→    db: AsyncSession = Depends(get_db)
    80→):
    81→    """List tasks for a project with optional filters."""
    82→    tasks = await Task.get_for_project(
    83→        db,
    84→        project_id,
    85→        status=status,
    86→        phase_id=phase_id,
    87→        agent_type=agent_type,
    88→        flagged=flagged,
    89→    )
    90→    return tasks
    91→
    92→
    93→@router.post("/projects/{project_id}/tasks", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
    94→async def create_task(
    95→    project_id: int,
    96→    task: TaskCreate,
    97→    db: AsyncSession = Depends(get_db)
    98→):
    99→    """Create a new task for a project."""
   100→    new_task = await Task.create(
   101→        db,
   102→        project_id=project_id,
   103→        name=task.name,
   104→        description=task.description,
   105→        phase_id=task.phase_id,
   106→        priority=task.priority,
   107→        estimated_time=task.estimated_time,
   108→        agent_type=task.agent_type,
   109→        depends_on=task.depends_on,
   110→    )
   111→    return new_task
   112→
   113→
   114→@router.get("/tasks/{task_id}", response_model=TaskResponse)
   115→async def get_task(task_id: int, db: AsyncSession = Depends(get_db)):
   116→    """Get a specific task by ID."""
   117→    task = await Task.get_by_id(db, task_id)
   118→    if not task:
   119→        raise HTTPException(
   120→            status_code=status.HTTP_404_NOT_FOUND,
   121→            detail=f"Task with ID {task_id} not found"
   122→        )
   123→    return task
   124→
   125→
   126→@router.put("/tasks/{task_id}", response_model=TaskResponse)
   127→async def update_task(
   128→    task_id: int,
   129→    task_update: TaskUpdate,
   130→    db: AsyncSession = Depends(get_db)
   131→):
   132→    """Update a task."""
   133→    task = await Task.get_by_id(db, task_id)
   134→    if not task:
   135→        raise HTTPException(
   136→            status_code=status.HTTP_404_NOT_FOUND,
   137→            detail=f"Task with ID {task_id} not found"
   138→        )
   139→
   140→    updated_task = await Task.update(
   141→        db,
   142→        task_id,
   143→        **task_update.model_dump(exclude_unset=True)
   144→    )
   145→    return updated_task
   146→
   147→
   148→@router.put("/tasks/{task_id}/status", response_model=TaskResponse)
   149→async def update_task_status(
   150→    task_id: int,
   151→    status_update: TaskStatusUpdate,
   152→    db: AsyncSession = Depends(get_db)
   153→):
   154→    """Update task status."""
   155→    task = await Task.get_by_id(db, task_id)
   156→    if not task:
   157→        raise HTTPException(
   158→            status_code=status.HTTP_404_NOT_FOUND,
   159→            detail=f"Task with ID {task_id} not found"
   160→        )
   161→
   162→    # Validate status
   163→    valid_statuses = ["backlog", "in_progress", "qa", "done", "blocked"]
   164→    if status_update.status not in valid_statuses:
   165→        raise HTTPException(
   166→            status_code=status.HTTP_400_BAD_REQUEST,
   167→            detail=f"Invalid status. Must be one of: {valid_statuses}"
   168→        )
   169→
   170→    updated_task = await Task.update(db, task_id, status=status_update.status)
   171→    return updated_task
   172→
   173→
   174→@router.post("/tasks/{task_id}/retry", response_model=TaskResponse)
   175→async def retry_task(task_id: int, db: AsyncSession = Depends(get_db)):
   176→    """Retry a failed/blocked task."""
   177→    task = await Task.get_by_id(db, task_id)
   178→    if not task:
   179→        raise HTTPException(
   180→            status_code=status.HTTP_404_NOT_FOUND,
   181→            detail=f"Task with ID {task_id} not found"
   182→        )
   183→
   184→    # Increment iteration count and reset status
   185→    updated_task = await Task.update(
   186→        db,
   187→        task_id,
   188→        status="in_progress",
   189→        iteration_count=task.iteration_count + 1,
   190→    )
   191→
   192→    # TODO: Trigger agent to retry the task
   193→
   194→    return updated_task
   195→
   196→
   197→@router.post("/tasks/{task_id}/flag", response_model=TaskResponse)
   198→async def flag_task(task_id: int, db: AsyncSession = Depends(get_db)):
   199→    """Flag a task for human review."""
   200→    task = await Task.get_by_id(db, task_id)
   201→    if not task:
   202→        raise HTTPException(
   203→            status_code=status.HTTP_404_NOT_FOUND,
   204→            detail=f"Task with ID {task_id} not found"
   205→        )
   206→
   207→    updated_task = await Task.update(db, task_id, flagged_for_review=True)
   208→
   209→    # TODO: Create notification for flagged task
   210→
   211→    return updated_task
   212→
   213→
   214→@router.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
   215→async def delete_task(task_id: int, db: AsyncSession = Depends(get_db)):
   216→    """Delete a task."""
   217→    task = await Task.get_by_id(db, task_id)
   218→    if not task:
   219→        raise HTTPException(
   220→            status_code=status.HTTP_404_NOT_FOUND,
   221→            detail=f"Task with ID {task_id} not found"
   222→        )
   223→
   224→    await Task.delete(db, task_id)
   225→    return None
   226→

<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
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

class TaskUpdate(BaseModel):
    """Schema for updating a task."""
    name: Optional[str] = None
    description: Optional[str] = None
    status: Optional[str] = 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

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

class TaskResponse(BaseModel):
    """Schema for task response."""
    id: int
    project_id: int
    spec_id: Optional[int]
    phase_id: Optional[int]
    name: str
    description: Optional[str]
    status: str
    priority: int
    estimated_time: Optional[int]
    actual_time: Optional[int]
    agent_type: Optional[str]
    iteration_count: int
    flagged_for_review: bool
    depends_on: Optional[List[int]]
    created_at: datetime
    updated_at: datetime

    class Config:
        from_attributes = True

@router.get("/projects/{project_id}/tasks", response_model=List[TaskResponse])
async def list_tasks(
    project_id: int,
    status: Optional[str] = Query(None, description="Filter by status"),
    phase_id: Optional[int] = Query(None, description="Filter by phase"),
    agent_type: Optional[str] = Query(None, description="Filter by agent type"),
    flagged: Optional[bool] = Query(None, description="Filter by flagged status"),
    db: AsyncSession = Depends(get_db)
):
    """List tasks for a project with optional filters."""
    tasks = await Task.get_for_project(
        db,
        project_id,
        status=status,
        phase_id=phase_id,
        agent_type=agent_type,
        flagged=flagged,
    )
    return tasks

@router.post("/projects/{project_id}/tasks", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
async def create_task(
    project_id: int,
    task: TaskCreate,
    db: AsyncSession = Depends(get_db)
):
    """Create a new task for a project."""
    new_task = await Task.create(
        db,
        project_id=project_id,
        name=task.name,
        description=task.description,
        phase_id=task.phase_id,
        priority=task.priority,
        estimated_time=task.estimated_time,
        agent_type=task.agent_type,
        depends_on=task.depends_on,
    )
    return new_task

@router.get("/tasks/{task_id}", response_model=TaskResponse)
async def get_task(task_id: int, db: AsyncSession = Depends(get_db)):
    """Get a specific task by ID."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )
    return task

@router.put("/tasks/{task_id}", response_model=TaskResponse)
async def update_task(
    task_id: int,
    task_update: TaskUpdate,
    db: AsyncSession = Depends(get_db)
):
    """Update a task."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )

    updated_task = await Task.update(
        db,
        task_id,
        **task_update.model_dump(exclude_unset=True)
    )
    return updated_task

@router.put("/tasks/{task_id}/status", response_model=TaskResponse)
async def update_task_status(
    task_id: int,
    status_update: TaskStatusUpdate,
    db: AsyncSession = Depends(get_db)
):
    """Update task status."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )

    # Validate status
    valid_statuses = ["backlog", "in_progress", "qa", "done", "blocked"]
    if status_update.status not in valid_statuses:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid status. Must be one of: {valid_statuses}"
        )

    updated_task = await Task.update(db, task_id, status=status_update.status)
    return updated_task

@router.post("/tasks/{task_id}/retry", response_model=TaskResponse)
async def retry_task(task_id: int, db: AsyncSession = Depends(get_db)):
    """Retry a failed/blocked task."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )

    # Increment iteration count and reset status
    updated_task = await Task.update(
        db,
        task_id,
        status="in_progress",
        iteration_count=task.iteration_count + 1,
    )

    # TODO: Trigger agent to retry the task

    return updated_task

@router.post("/tasks/{task_id}/flag", response_model=TaskResponse)
async def flag_task(task_id: int, db: AsyncSession = Depends(get_db)):
    """Flag a task for human review."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )

    updated_task = await Task.update(db, task_id, flagged_for_review=True)

    # TODO: Create notification for flagged task

    return updated_task

@router.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(task_id: int, db: AsyncSession = Depends(get_db)):
    """Delete a task."""
    task = await Task.get_by_id(db, task_id)
    if not task:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Task with ID {task_id} not found"
        )

    await Task.delete(db, task_id)
    return None


Now I understand the full picture:

1. The backend returns a 404 if trying to delete an already-deleted task (lines 217-222)
2. The frontend shows an error toast when deletion fails (taskStore.ts line 151)

The issue is that:
1. When a user clicks delete rapidly, multiple API calls could be made
2. The first succeeds, subsequent ones fail with 404
3. Users would see error messages about "missing record"

To fix this, I need to:
1. Make the delete button properly disabled during deletion
2. Close the dialog immediately when deletion starts (or prevent multiple clicks)
3. Track which task is being deleted to prevent duplicate delete operations

Let me create a fix that:
1. Uses a Set to track tasks currently being deleted
2. Closes the dialog immediately when delete is clicked to prevent subsequent clicks
3. Handles the case where a rapid click tries to delete an already-deleted task


TodoWrite

Implement rapid delete click protection for Feature #110

Implementing rapid delete click protection

Test rapid delete clicks work correctly

Mark Feature #110 as passing after verification


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Implement rapid delete click protection for Feature #110

Implementing rapid delete click protection

Test rapid delete clicks work correctly

Mark Feature #110 as passing after verification


Now let me implement the fix. The key changes needed:

1. In `taskStore.ts` - Track tasks being deleted to prevent duplicate operations
2. In `KanbanBoard.tsx` - Close the dialog immediately when delete is clicked


Edit

interface TaskStore {
  tasks: Task[]
  isLoading: boolean
  error: string | null
  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>
  clearTasks: () => void
}

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
}