commit 38f68d5ed489f416ea91630aea3496ab15365e66 Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 16:16:28 2025 -0400 Fix click and drag commit eb5c41cf31fd8032fe88fd47fe5f3e7f517f6d30 Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 15:57:13 2025 -0400 Update tasks commit 979d4b15373df3193eb1bd41c18ece1dbe044eba Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 15:19:20 2025 -0400 Status commit fa26f1fa8fefe1d84b5b2153327c7e8c0132952a Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 14:54:48 2025 -0400 Cleanup project card commit 14d7a1d7d7574dd8745167b280c04603ba22b189 Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 14:49:19 2025 -0400 Improve existing vs new repo commit 277e1f05ef68e5c67d73b246557a6df2ab23d32c Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 13:01:21 2025 -0400 Make repo path unique commit f80ef55f2ba16836276a81844fc33639872bcc53 Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 12:52:20 2025 -0400 Fix styles commit 077869458fcab199a10ef0fe2fe39f9f4216ce5b Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 12:41:48 2025 -0400 First select repo commit 1b0d9c0280e4cb75294348bb53b2a534458a2e37 Author: Louis Knight-Webb <louis@bloop.ai> Date: Mon Jun 16 11:45:19 2025 -0400 Init
304 lines
8.2 KiB
TypeScript
304 lines
8.2 KiB
TypeScript
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 { ArrowLeft, Plus } from 'lucide-react'
|
|
import { makeAuthenticatedRequest } from '@/lib/auth'
|
|
import { TaskCreateDialog } from '@/components/tasks/TaskCreateDialog'
|
|
import { TaskEditDialog } from '@/components/tasks/TaskEditDialog'
|
|
import { TaskDetailsDialog } from '@/components/tasks/TaskDetailsDialog'
|
|
import { TaskKanbanBoard } from '@/components/tasks/TaskKanbanBoard'
|
|
import type { TaskStatus } from 'shared/types'
|
|
import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban'
|
|
|
|
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<T> {
|
|
success: boolean
|
|
data: T | null
|
|
message: string | null
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ProjectTasks() {
|
|
const { projectId } = useParams<{ projectId: string }>()
|
|
const navigate = useNavigate()
|
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
const [project, setProject] = useState<Project | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
|
const [editingTask, setEditingTask] = useState<Task | null>(null)
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
|
const [isTaskDetailsDialogOpen, setIsTaskDetailsDialogOpen] = useState(false)
|
|
|
|
|
|
useEffect(() => {
|
|
if (projectId) {
|
|
fetchProject()
|
|
fetchTasks()
|
|
}
|
|
}, [projectId])
|
|
|
|
const fetchProject = async () => {
|
|
try {
|
|
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`)
|
|
|
|
if (response.ok) {
|
|
const result: ApiResponse<Project> = 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<Task[]> = 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 handleCreateTask = async (title: string, description: string) => {
|
|
try {
|
|
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
project_id: projectId,
|
|
title,
|
|
description: description || null
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
await fetchTasks()
|
|
} else {
|
|
setError('Failed to create task')
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to create task')
|
|
}
|
|
}
|
|
|
|
const handleUpdateTask = async (title: string, description: string, status: TaskStatus) => {
|
|
if (!editingTask) return
|
|
|
|
try {
|
|
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
title,
|
|
description: description || null,
|
|
status
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
await fetchTasks()
|
|
setEditingTask(null)
|
|
} else {
|
|
setError('Failed to update task')
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to update task')
|
|
}
|
|
}
|
|
|
|
const handleDeleteTask = 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 handleEditTask = (task: Task) => {
|
|
setEditingTask(task)
|
|
setIsEditDialogOpen(true)
|
|
}
|
|
|
|
const handleViewTaskDetails = (task: Task) => {
|
|
setSelectedTask(task)
|
|
setIsTaskDetailsDialogOpen(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')
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-8">Loading tasks...</div>
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="text-center py-8 text-red-600">{error}</div>
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => navigate('/projects')}
|
|
className="flex items-center"
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Back to Projects
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold">
|
|
{project?.name || 'Project'} Tasks
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage tasks for this project
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Task
|
|
</Button>
|
|
</div>
|
|
|
|
<TaskCreateDialog
|
|
isOpen={isCreateDialogOpen}
|
|
onOpenChange={setIsCreateDialogOpen}
|
|
onCreateTask={handleCreateTask}
|
|
/>
|
|
|
|
{/* Tasks View */}
|
|
{tasks.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="text-center py-8">
|
|
<p className="text-muted-foreground">No tasks found for this project.</p>
|
|
<Button
|
|
className="mt-4"
|
|
onClick={() => setIsCreateDialogOpen(true)}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create First Task
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<TaskKanbanBoard
|
|
tasks={tasks}
|
|
onDragEnd={handleDragEnd}
|
|
onEditTask={handleEditTask}
|
|
onDeleteTask={handleDeleteTask}
|
|
onViewTaskDetails={handleViewTaskDetails}
|
|
/>
|
|
)}
|
|
|
|
<TaskEditDialog
|
|
isOpen={isEditDialogOpen}
|
|
onOpenChange={setIsEditDialogOpen}
|
|
task={editingTask}
|
|
onUpdateTask={handleUpdateTask}
|
|
/>
|
|
|
|
<TaskDetailsDialog
|
|
isOpen={isTaskDetailsDialogOpen}
|
|
onOpenChange={setIsTaskDetailsDialogOpen}
|
|
task={selectedTask}
|
|
projectId={projectId!}
|
|
onError={setError}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|