715 lines
31 KiB
TypeScript
715 lines
31 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Label } from "@/components/ui/label"
|
|
import { useTaskStore, Task, TaskType, TaskStatus, Priority, Project } from "@/stores/useTaskStore"
|
|
import { BacklogView } from "@/components/BacklogView"
|
|
import { Plus, MessageSquare, Calendar, Tag, Trash2, Edit2, X, Check, MoreHorizontal, LayoutGrid, ListTodo } from "lucide-react"
|
|
|
|
const typeColors: Record<TaskType, string> = {
|
|
idea: "bg-purple-500",
|
|
task: "bg-blue-500",
|
|
bug: "bg-red-500",
|
|
research: "bg-green-500",
|
|
plan: "bg-amber-500",
|
|
}
|
|
|
|
const typeLabels: Record<TaskType, string> = {
|
|
idea: "💡 Idea",
|
|
task: "📋 Task",
|
|
bug: "🐛 Bug",
|
|
research: "🔬 Research",
|
|
plan: "📐 Plan",
|
|
}
|
|
|
|
const priorityColors: Record<Priority, string> = {
|
|
low: "text-slate-400",
|
|
medium: "text-blue-400",
|
|
high: "text-orange-400",
|
|
urgent: "text-red-400",
|
|
}
|
|
|
|
const allStatuses: TaskStatus[] = ["open", "backlog", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"]
|
|
|
|
// Sprint board columns mapped to workflow statuses
|
|
const sprintColumns = [
|
|
{
|
|
key: "todo",
|
|
label: "To Do",
|
|
statuses: ["open", "backlog"] // OPEN, TO DO
|
|
},
|
|
{
|
|
key: "inprogress",
|
|
label: "In Progress",
|
|
statuses: ["blocked", "in-progress", "review", "validate"] // BLOCKED, IN PROGRESS, REVIEW, VALIDATE
|
|
},
|
|
{
|
|
key: "done",
|
|
label: "Done",
|
|
statuses: ["archived", "canceled", "done"] // ARCHIVED, CANCELED, DONE
|
|
},
|
|
] as const
|
|
|
|
export default function Home() {
|
|
const {
|
|
projects,
|
|
tasks,
|
|
sprints,
|
|
selectedProjectId,
|
|
selectedTaskId,
|
|
selectedSprintId,
|
|
selectProject,
|
|
addProject,
|
|
deleteProject,
|
|
addTask,
|
|
updateTask,
|
|
deleteTask,
|
|
selectTask,
|
|
addComment,
|
|
deleteComment,
|
|
getTasksByProject,
|
|
syncFromServer,
|
|
isLoading,
|
|
} = useTaskStore()
|
|
|
|
const [newProjectName, setNewProjectName] = useState("")
|
|
const [showNewProject, setShowNewProject] = useState(false)
|
|
const [newTaskOpen, setNewTaskOpen] = useState(false)
|
|
const [newTask, setNewTask] = useState<Partial<Task>>({
|
|
title: "",
|
|
description: "",
|
|
type: "task",
|
|
priority: "medium",
|
|
status: "backlog",
|
|
tags: [],
|
|
})
|
|
const [newComment, setNewComment] = useState("")
|
|
const [editingTask, setEditingTask] = useState<Task | null>(null)
|
|
const [viewMode, setViewMode] = useState<'kanban' | 'backlog'>('kanban')
|
|
|
|
// Sync from server on mount
|
|
useEffect(() => {
|
|
syncFromServer()
|
|
}, [syncFromServer])
|
|
|
|
const selectedProject = projects.find((p) => p.id === selectedProjectId)
|
|
const selectedTask = tasks.find((t) => t.id === selectedTaskId)
|
|
const projectTasks = selectedProjectId ? getTasksByProject(selectedProjectId) : []
|
|
|
|
// Get current active sprint (across all projects)
|
|
const now = new Date()
|
|
const currentSprint = sprints.find((s) =>
|
|
s.status === 'active' &&
|
|
new Date(s.startDate) <= now &&
|
|
new Date(s.endDate) >= now
|
|
)
|
|
|
|
// Filter tasks to only show current sprint tasks in Kanban (from ALL projects)
|
|
const sprintTasks = currentSprint
|
|
? tasks.filter((t) => t.sprintId === currentSprint.id)
|
|
: []
|
|
|
|
const handleAddProject = () => {
|
|
if (newProjectName.trim()) {
|
|
addProject(newProjectName.trim())
|
|
setNewProjectName("")
|
|
setShowNewProject(false)
|
|
}
|
|
}
|
|
|
|
const handleAddTask = () => {
|
|
if (newTask.title?.trim() && selectedProjectId) {
|
|
addTask({
|
|
...newTask,
|
|
projectId: selectedProjectId,
|
|
status: newTask.status || "backlog",
|
|
sprintId: currentSprint?.id, // Auto-assign to current sprint
|
|
} as any)
|
|
setNewTask({ title: "", description: "", type: "task", priority: "medium", status: "backlog", tags: [] })
|
|
setNewTaskOpen(false)
|
|
}
|
|
}
|
|
|
|
const handleAddComment = () => {
|
|
if (newComment.trim() && selectedTaskId) {
|
|
addComment(selectedTaskId, newComment.trim(), "user")
|
|
setNewComment("")
|
|
}
|
|
}
|
|
|
|
const handleUpdateTaskStatus = (taskId: string, newStatus: TaskStatus) => {
|
|
updateTask(taskId, { status: newStatus })
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100">
|
|
{/* Header */}
|
|
<header className="border-b border-slate-800 bg-slate-900/50 backdrop-blur">
|
|
<div className="max-w-[1800px] mx-auto px-4 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl md:text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
|
|
OpenClaw Project Hub
|
|
</h1>
|
|
<p className="text-xs md:text-sm text-slate-400 mt-1">
|
|
Track ideas, tasks, bugs, and plans — with threaded notes
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{isLoading && (
|
|
<span className="flex items-center gap-1 text-xs text-blue-400">
|
|
<svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Syncing...
|
|
</span>
|
|
)}
|
|
<span className="hidden md:inline text-sm text-slate-400">
|
|
{tasks.length} tasks · {projects.length} projects
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-[1800px] mx-auto px-4 py-4 md:py-6">
|
|
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
|
|
{/* Sidebar - Sprint Info */}
|
|
<aside className="w-full lg:w-64 shrink-0">
|
|
<Card className="bg-slate-900 border-slate-800">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-semibold text-slate-400 uppercase tracking-wider">
|
|
Current Sprint
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{currentSprint ? (
|
|
<>
|
|
<div>
|
|
<h3 className="font-medium text-white">{currentSprint.name}</h3>
|
|
<p className="text-sm text-slate-400">
|
|
{new Date(currentSprint.startDate).toLocaleDateString()} - {new Date(currentSprint.endDate).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<span className="text-slate-400">
|
|
<strong className="text-white">{sprintTasks.length}</strong> tasks
|
|
</span>
|
|
<span className="text-slate-400">
|
|
<strong className="text-white">{sprintTasks.filter(t => t.status === 'done').length}</strong> done
|
|
</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-slate-500">No active sprint</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Projects Quick View */}
|
|
<Card className="bg-slate-900 border-slate-800 mt-4">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-semibold text-slate-400 uppercase tracking-wider">
|
|
Projects
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{projects.map((project) => (
|
|
<div key={project.id} className="flex items-center gap-2 text-sm">
|
|
<div
|
|
className="w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: project.color }}
|
|
/>
|
|
<span className="text-slate-400">{project.name}</span>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 min-w-0">
|
|
{selectedProject ? (
|
|
<>
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 md:mb-6">
|
|
<div>
|
|
<h2 className="text-lg md:text-xl font-semibold text-white">
|
|
{selectedProject.name}
|
|
</h2>
|
|
<p className="text-sm text-slate-400">
|
|
{sprintTasks.length} tasks · {sprintTasks.filter((t) => t.status === "done").length} done
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{/* View Toggle */}
|
|
<div className="flex bg-slate-800 rounded-lg p-1">
|
|
<button
|
|
onClick={() => setViewMode('kanban')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition-colors ${
|
|
viewMode === 'kanban'
|
|
? 'bg-slate-700 text-white'
|
|
: 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<LayoutGrid className="w-4 h-4" />
|
|
Kanban
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('backlog')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition-colors ${
|
|
viewMode === 'backlog'
|
|
? 'bg-slate-700 text-white'
|
|
: 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<ListTodo className="w-4 h-4" />
|
|
Backlog
|
|
</button>
|
|
</div>
|
|
<Button onClick={() => setNewTaskOpen(true)} className="w-full sm:w-auto">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Task
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* View Content */}
|
|
{viewMode === 'backlog' ? (
|
|
<BacklogView />
|
|
) : (
|
|
<>
|
|
{/* Current Sprint Header */}
|
|
<div className="mb-4 p-3 bg-slate-800/50 border border-slate-700 rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-medium text-white">Sprint 1</h3>
|
|
<p className="text-sm text-slate-400">Feb 16 - Feb 22, 2026</p>
|
|
</div>
|
|
<Badge variant="default">Active</Badge>
|
|
</div>
|
|
</div>
|
|
{/* Kanban Columns */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{sprintColumns.map((column) => {
|
|
// Filter tasks by column statuses
|
|
const columnTasks = sprintTasks.filter((t) =>
|
|
column.statuses.includes(t.status)
|
|
)
|
|
return (
|
|
<div key={column.key} className="flex flex-col">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-sm font-medium text-slate-400">
|
|
{column.label}
|
|
</h3>
|
|
<Badge variant="secondary" className="bg-slate-800 text-slate-400">
|
|
{columnTasks.length}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{columnTasks.map((task) => {
|
|
const taskProject = projects.find((p) => p.id === task.projectId)
|
|
return (
|
|
<Card
|
|
key={task.id}
|
|
className="bg-slate-900 border-slate-800 hover:border-slate-700 cursor-pointer transition-colors group"
|
|
onClick={() => selectTask(task.id)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-xs ${typeColors[task.type]} text-white border-0`}
|
|
>
|
|
{typeLabels[task.type]}
|
|
</Badge>
|
|
{taskProject && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs text-white border-0"
|
|
style={{ backgroundColor: taskProject.color }}
|
|
>
|
|
{taskProject.name}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setEditingTask(task)
|
|
}}
|
|
className="p-1 hover:bg-slate-800 rounded"
|
|
>
|
|
<Edit2 className="w-3 h-3 text-slate-400" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
deleteTask(task.id)
|
|
}}
|
|
className="p-1 hover:bg-slate-800 rounded"
|
|
>
|
|
<Trash2 className="w-3 h-3 text-slate-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h4 className="font-medium text-white mb-1">{task.title}</h4>
|
|
{task.description && (
|
|
<p className="text-sm text-slate-400 line-clamp-2 mb-3">
|
|
{task.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<span className={priorityColors[task.priority]}>
|
|
{task.priority}
|
|
</span>
|
|
{task.comments.length > 0 && (
|
|
<span className="flex items-center gap-1 text-slate-500">
|
|
<MessageSquare className="w-3 h-3" />
|
|
{task.comments.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{task.dueDate && (
|
|
<span className="text-slate-500 flex items-center gap-1">
|
|
<Calendar className="w-3 h-3" />
|
|
{new Date(task.dueDate).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{task.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1 mt-3">
|
|
{task.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="text-xs px-2 py-0.5 bg-slate-800 text-slate-400 rounded"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)})}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="flex items-center justify-center h-96 text-slate-500">
|
|
<p>Select a project to view tasks</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
{/* New Task Dialog */}
|
|
<Dialog open={newTaskOpen} onOpenChange={setNewTaskOpen}>
|
|
<DialogContent className="bg-slate-900 border-slate-800 text-white w-[95vw] max-w-lg max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>New Task</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div>
|
|
<Label>Title</Label>
|
|
<input
|
|
type="text"
|
|
value={newTask.title}
|
|
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="What needs to be done?"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Description</Label>
|
|
<Textarea
|
|
value={newTask.description}
|
|
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
|
|
className="mt-1.5 bg-slate-800 border-slate-700 text-white placeholder-slate-500"
|
|
placeholder="Add more details..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Type</Label>
|
|
<select
|
|
value={newTask.type}
|
|
onChange={(e) => setNewTask({ ...newTask, type: e.target.value as TaskType })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
{Object.entries(typeLabels).map(([type, label]) => (
|
|
<option key={type} value={type}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>Priority</Label>
|
|
<select
|
|
value={newTask.priority}
|
|
onChange={(e) => setNewTask({ ...newTask, priority: e.target.value as Priority })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="low">Low</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="high">High</option>
|
|
<option value="urgent">Urgent</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Status</Label>
|
|
<select
|
|
value={newTask.status}
|
|
onChange={(e) => setNewTask({ ...newTask, status: e.target.value as TaskStatus })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
{allStatuses.map((status) => (
|
|
<option key={status} value={status}>{status.replace("-", " ").toUpperCase()}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>Sprint (optional)</Label>
|
|
<select
|
|
value={newTask.sprintId || ""}
|
|
onChange={(e) => setNewTask({ ...newTask, sprintId: e.target.value || undefined })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="">No Sprint</option>
|
|
{sprints.filter(s => s.projectId === selectedProjectId).map((sprint) => (
|
|
<option key={sprint.id} value={sprint.id}>{sprint.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label>Tags (comma separated)</Label>
|
|
<input
|
|
type="text"
|
|
onChange={(e) => setNewTask({ ...newTask, tags: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) })}
|
|
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
placeholder="ios, backend, urgent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={() => setNewTaskOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleAddTask}>Create Task</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Task Detail Dialog with Comments */}
|
|
<Dialog open={!!selectedTaskId} onOpenChange={() => selectTask(null)}>
|
|
<DialogContent className="bg-slate-900 border-slate-800 text-white w-[95vw] max-w-2xl max-h-[90vh] overflow-y-auto p-4 md:p-6">
|
|
{selectedTask && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge className={`${typeColors[selectedTask.type]} text-white border-0`}>
|
|
{typeLabels[selectedTask.type]}
|
|
</Badge>
|
|
<Badge variant="outline" className={priorityColors[selectedTask.priority]}>
|
|
{selectedTask.priority}
|
|
</Badge>
|
|
</div>
|
|
<DialogTitle className="text-xl">{selectedTask.title}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
{/* Description */}
|
|
{selectedTask.description && (
|
|
<div>
|
|
<Label className="text-slate-400">Description</Label>
|
|
<p className="mt-2 text-slate-300 whitespace-pre-wrap">
|
|
{selectedTask.description}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tags */}
|
|
{selectedTask.tags.length > 0 && (
|
|
<div>
|
|
<Label className="text-slate-400">Tags</Label>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{selectedTask.tags.map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="bg-slate-800 text-slate-300">
|
|
<Tag className="w-3 h-3 mr-1" />
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Priority */}
|
|
<div>
|
|
<Label className="text-slate-400">Priority</Label>
|
|
<div className="flex gap-2 mt-2">
|
|
{(["low", "medium", "high", "urgent"] as Priority[]).map((priority) => (
|
|
<button
|
|
key={priority}
|
|
onClick={() => updateTask(selectedTask.id, { priority })}
|
|
className={`px-3 py-1.5 rounded-lg text-sm transition-colors capitalize ${
|
|
selectedTask.priority === priority
|
|
? priority === "urgent"
|
|
? "bg-red-600 text-white"
|
|
: priority === "high"
|
|
? "bg-orange-600 text-white"
|
|
: priority === "medium"
|
|
? "bg-blue-600 text-white"
|
|
: "bg-slate-600 text-white"
|
|
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
|
|
}`}
|
|
>
|
|
{priority}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div>
|
|
<Label className="text-slate-400">Status</Label>
|
|
<select
|
|
value={selectedTask.status}
|
|
onChange={(e) => handleUpdateTaskStatus(selectedTask.id, e.target.value as TaskStatus)}
|
|
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
{allStatuses.map((status) => (
|
|
<option key={status} value={status}>{status.replace("-", " ").toUpperCase()}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Sprint */}
|
|
<div>
|
|
<Label className="text-slate-400">Sprint</Label>
|
|
<select
|
|
value={selectedTask.sprintId || ""}
|
|
onChange={(e) => updateTask(selectedTask.id, { sprintId: e.target.value || undefined })}
|
|
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="">No Sprint</option>
|
|
{sprints.filter(s => s.projectId === selectedProjectId).map((sprint) => (
|
|
<option key={sprint.id} value={sprint.id}>{sprint.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Comments Section */}
|
|
<div className="border-t border-slate-800 pt-6">
|
|
<h4 className="font-medium text-white mb-4 flex items-center gap-2">
|
|
<MessageSquare className="w-4 h-4" />
|
|
Notes & Comments ({selectedTask.comments.length})
|
|
</h4>
|
|
|
|
{/* Comment List */}
|
|
<div className="space-y-4 mb-4">
|
|
{selectedTask.comments.length === 0 ? (
|
|
<p className="text-slate-500 text-sm">No notes yet. Add the first one!</p>
|
|
) : (
|
|
selectedTask.comments.map((comment) => (
|
|
<div
|
|
key={comment.id}
|
|
className={`flex gap-3 p-3 rounded-lg ${
|
|
comment.author === "assistant" ? "bg-blue-900/20" : "bg-slate-800/50"
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium ${
|
|
comment.author === "assistant"
|
|
? "bg-blue-600 text-white"
|
|
: "bg-slate-700 text-slate-300"
|
|
}`}
|
|
>
|
|
{comment.author === "assistant" ? "AI" : "You"}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-sm font-medium text-slate-300">
|
|
{comment.author === "assistant" ? "Assistant" : "You"}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-slate-500">
|
|
{new Date(comment.createdAt).toLocaleString()}
|
|
</span>
|
|
<button
|
|
onClick={() => deleteComment(selectedTask.id, comment.id)}
|
|
className="text-slate-600 hover:text-red-400"
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p className="text-slate-300 text-sm whitespace-pre-wrap">{comment.text}</p>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Add Comment */}
|
|
<div className="flex gap-3">
|
|
<Textarea
|
|
value={newComment}
|
|
onChange={(e) => setNewComment(e.target.value)}
|
|
placeholder="Add a note or comment..."
|
|
className="flex-1 bg-slate-800 border-slate-700 text-white placeholder-slate-500"
|
|
rows={2}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && e.metaKey) {
|
|
handleAddComment()
|
|
}
|
|
}}
|
|
/>
|
|
<Button onClick={handleAddComment} className="self-end">
|
|
Add
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="border-t border-slate-800 pt-4">
|
|
<Button
|
|
variant="ghost"
|
|
className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
|
onClick={() => {
|
|
deleteTask(selectedTask.id)
|
|
selectTask(null)
|
|
}}
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
Delete Task
|
|
</Button>
|
|
</DialogFooter>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|