import { useState, useEffect } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { ArrowLeft, Plus, MoreHorizontal, Trash2, Edit } from 'lucide-react' import { makeAuthenticatedRequest } from '@/lib/auth' import { KanbanProvider, KanbanBoard, KanbanHeader, KanbanCards, KanbanCard, type DragEndEvent } from '@/components/ui/shadcn-io/kanban' import type { TaskStatus } from 'shared/types' interface Task { id: string project_id: string title: string description: string | null status: TaskStatus created_at: string updated_at: string } interface Project { id: string name: string owner_id: string created_at: string updated_at: string } interface ApiResponse { success: boolean data: T | null message: string | null } // All possible task statuses from shared types const allTaskStatuses: TaskStatus[] = ['todo', 'inprogress', 'inreview', 'done', 'cancelled'] const statusLabels: Record = { todo: 'To Do', inprogress: 'In Progress', inreview: 'In Review', done: 'Done', cancelled: 'Cancelled' } const statusBoardColors: Record = { todo: '#64748b', inprogress: '#3b82f6', inreview: '#f59e0b', done: '#22c55e', cancelled: '#ef4444' } export function ProjectTasks() { const { projectId } = useParams<{ projectId: string }>() const navigate = useNavigate() const [tasks, setTasks] = useState([]) const [project, setProject] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [editingTask, setEditingTask] = useState(null) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) // Form states const [newTaskTitle, setNewTaskTitle] = useState('') const [newTaskDescription, setNewTaskDescription] = useState('') const [editTaskTitle, setEditTaskTitle] = useState('') const [editTaskDescription, setEditTaskDescription] = useState('') const [editTaskStatus, setEditTaskStatus] = useState('todo') useEffect(() => { if (projectId) { fetchProject() fetchTasks() } }, [projectId]) const fetchProject = async () => { try { const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`) if (response.ok) { const result: ApiResponse = await response.json() if (result.success && result.data) { setProject(result.data) } } else if (response.status === 404) { setError('Project not found') navigate('/projects') } } catch (err) { setError('Failed to load project') } } const fetchTasks = async () => { try { setLoading(true) const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`) if (response.ok) { const result: ApiResponse = await response.json() if (result.success && result.data) { setTasks(result.data) } } else { setError('Failed to load tasks') } } catch (err) { setError('Failed to load tasks') } finally { setLoading(false) } } const createTask = async () => { if (!newTaskTitle.trim()) return try { const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`, { method: 'POST', body: JSON.stringify({ project_id: projectId, title: newTaskTitle, description: newTaskDescription || null }) }) if (response.ok) { await fetchTasks() setNewTaskTitle('') setNewTaskDescription('') setIsCreateDialogOpen(false) } else { setError('Failed to create task') } } catch (err) { setError('Failed to create task') } } const updateTask = async () => { if (!editingTask || !editTaskTitle.trim()) return try { const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, { method: 'PUT', body: JSON.stringify({ title: editTaskTitle, description: editTaskDescription || null, status: editTaskStatus }) }) if (response.ok) { await fetchTasks() setEditingTask(null) setIsEditDialogOpen(false) } else { setError('Failed to update task') } } catch (err) { setError('Failed to update task') } } const deleteTask = async (taskId: string) => { if (!confirm('Are you sure you want to delete this task?')) return try { const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'DELETE', }) if (response.ok) { await fetchTasks() } else { setError('Failed to delete task') } } catch (err) { setError('Failed to delete task') } } const openEditDialog = (task: Task) => { setEditingTask(task) setEditTaskTitle(task.title) setEditTaskDescription(task.description || '') setEditTaskStatus(task.status) setIsEditDialogOpen(true) } const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event if (!over || !active.data.current) return const taskId = active.id as string const newStatus = over.id as Task['status'] const task = tasks.find(t => t.id === taskId) if (!task || task.status === newStatus) return // Optimistically update the UI immediately const previousStatus = task.status setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus } : t )) try { const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'PUT', body: JSON.stringify({ title: task.title, description: task.description, status: newStatus }) }) if (!response.ok) { // Revert the optimistic update if the API call failed setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: previousStatus } : t )) setError('Failed to update task status') } } catch (err) { // Revert the optimistic update if the API call failed setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: previousStatus } : t )) setError('Failed to update task status') } } const groupTasksByStatus = () => { const groups: Record = {} as Record // Initialize groups for all possible statuses allTaskStatuses.forEach(status => { groups[status] = [] }) tasks.forEach(task => { // Convert old capitalized status to lowercase if needed const normalizedStatus = task.status.toLowerCase() as TaskStatus if (groups[normalizedStatus]) { groups[normalizedStatus].push(task) } else { // Default to todo if status doesn't match any expected value groups['todo'].push(task) } }) return groups } if (loading) { return
Loading tasks...
} if (error) { return
{error}
} return (
{/* Header */}

{project?.name || 'Project'} Tasks

Manage tasks for this project

Create New Task
setNewTaskTitle(e.target.value)} placeholder="Enter task title" />