Redesign Backlog view: vertical collapsible sections with drag-and-drop
This commit is contained in:
parent
af0e467cc1
commit
bbde17ec11
@ -18,10 +18,11 @@ import {
|
|||||||
} from "@dnd-kit/sortable"
|
} from "@dnd-kit/sortable"
|
||||||
import { CSS } from "@dnd-kit/utilities"
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
import { useTaskStore, Task, Sprint } from "@/stores/useTaskStore"
|
import { useTaskStore, Task, Sprint } from "@/stores/useTaskStore"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Plus, GripVertical, Search, Filter } from "lucide-react"
|
import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react"
|
||||||
|
import { format } from "date-fns"
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
const priorityColors: Record<string, string> = {
|
||||||
low: "bg-slate-600",
|
low: "bg-slate-600",
|
||||||
@ -31,32 +32,20 @@ const priorityColors: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const typeLabels: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
idea: "💡 Idea",
|
idea: "💡",
|
||||||
task: "📋 Task",
|
task: "📋",
|
||||||
bug: "🐛 Bug",
|
bug: "🐛",
|
||||||
research: "🔬 Research",
|
research: "🔬",
|
||||||
plan: "📐 Plan",
|
plan: "📐",
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeColors: Record<string, string> = {
|
// Sortable Task Row
|
||||||
idea: "bg-purple-500",
|
function SortableTaskRow({
|
||||||
task: "bg-blue-500",
|
|
||||||
bug: "bg-red-500",
|
|
||||||
research: "bg-green-500",
|
|
||||||
plan: "bg-amber-500",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortable Backlog Item
|
|
||||||
function SortableBacklogItem({
|
|
||||||
task,
|
task,
|
||||||
sprints,
|
|
||||||
onClick,
|
onClick,
|
||||||
onAssignToSprint,
|
|
||||||
}: {
|
}: {
|
||||||
task: Task
|
task: Task
|
||||||
sprints: Sprint[]
|
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onAssignToSprint: (sprintId: string) => void
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@ -73,105 +62,117 @@ function SortableBacklogItem({
|
|||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSprint = sprints.find((s) => s.id === task.sprintId)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className="bg-slate-800 border-slate-700 hover:border-slate-600 transition-colors group"
|
className="flex items-center gap-3 p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-slate-600 cursor-pointer group"
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<CardContent className="p-3">
|
<div
|
||||||
<div className="flex items-center gap-3">
|
{...attributes}
|
||||||
<div
|
{...listeners}
|
||||||
{...attributes}
|
className="opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
|
||||||
{...listeners}
|
>
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
|
<GripVertical className="w-4 h-4 text-slate-500" />
|
||||||
>
|
</div>
|
||||||
<GripVertical className="w-4 h-4 text-slate-500" />
|
<span className="text-lg">{typeLabels[task.type]}</span>
|
||||||
</div>
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-slate-200 truncate">{task.title}</p>
|
||||||
<div className="flex-1 min-w-0" onClick={onClick}>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<Badge className={`${priorityColors[task.priority]} text-white text-xs`}>
|
||||||
<Badge
|
{task.priority}
|
||||||
className={`${typeColors[task.type]} text-white text-xs border-0`}
|
</Badge>
|
||||||
>
|
{task.comments.length > 0 && (
|
||||||
{typeLabels[task.type]}
|
<span className="text-xs text-slate-500">💬 {task.comments.length}</span>
|
||||||
</Badge>
|
)}
|
||||||
<Badge
|
</div>
|
||||||
className={`${priorityColors[task.priority]} text-white text-xs`}
|
|
||||||
>
|
|
||||||
{task.priority}
|
|
||||||
</Badge>
|
|
||||||
{task.comments.length > 0 && (
|
|
||||||
<span className="text-xs text-slate-500">
|
|
||||||
💬 {task.comments.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h4 className="text-sm font-medium text-slate-200 truncate">
|
|
||||||
{task.title}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{currentSprint ? (
|
|
||||||
<Badge variant="outline" className="text-xs border-blue-500 text-blue-400">
|
|
||||||
{currentSprint.name}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
onAssignToSprint(e.target.value)
|
|
||||||
e.target.value = ""
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="bg-slate-900 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300 focus:outline-none focus:border-blue-500"
|
|
||||||
defaultValue=""
|
|
||||||
>
|
|
||||||
<option value="">+ Sprint</option>
|
|
||||||
{sprints.map((sprint) => (
|
|
||||||
<option key={sprint.id} value={sprint.id}>
|
|
||||||
{sprint.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag Overlay Item
|
// Drag Overlay Item
|
||||||
function DragOverlayItem({ task }: { task: Task }) {
|
function DragOverlayItem({ task }: { task: Task }) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-slate-800 border-slate-600 shadow-xl rotate-1">
|
<div className="flex items-center gap-3 p-3 bg-slate-800 border border-slate-600 rounded-lg shadow-xl rotate-1">
|
||||||
<CardContent className="p-3">
|
<GripVertical className="w-4 h-4 text-slate-500" />
|
||||||
|
<span className="text-lg">{typeLabels[task.type]}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-slate-200 truncate">{task.title}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={`${priorityColors[task.priority]} text-white text-xs`}>
|
||||||
|
{task.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapsible Section
|
||||||
|
function TaskSection({
|
||||||
|
title,
|
||||||
|
tasks,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
onTaskClick,
|
||||||
|
sprintInfo,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
tasks: Task[]
|
||||||
|
isOpen: boolean
|
||||||
|
onToggle: () => void
|
||||||
|
onTaskClick: (task: Task) => void
|
||||||
|
sprintInfo?: { name: string; date: string; status: string }
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="border border-slate-800 rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-full flex items-center justify-between p-4 bg-slate-900 hover:bg-slate-800/50 transition-colors"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<GripVertical className="w-4 h-4 text-slate-500" />
|
{isOpen ? (
|
||||||
<div className="flex-1 min-w-0">
|
<ChevronDown className="w-5 h-5 text-slate-400" />
|
||||||
<div className="flex items-center gap-2 mb-1">
|
) : (
|
||||||
<Badge
|
<ChevronRight className="w-5 h-5 text-slate-400" />
|
||||||
className={`${typeColors[task.type]} text-white text-xs border-0`}
|
)}
|
||||||
>
|
<h3 className="font-medium text-slate-200">{title}</h3>
|
||||||
{typeLabels[task.type]}
|
<Badge variant="secondary" className="bg-slate-800 text-slate-400">
|
||||||
</Badge>
|
{tasks.length}
|
||||||
<Badge
|
</Badge>
|
||||||
className={`${priorityColors[task.priority]} text-white text-xs`}
|
|
||||||
>
|
|
||||||
{task.priority}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-sm font-medium text-slate-200 truncate">
|
|
||||||
{task.title}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
{sprintInfo && (
|
||||||
</Card>
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{sprintInfo.date}</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{sprintInfo.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="p-4 bg-slate-950/50">
|
||||||
|
<SortableContext
|
||||||
|
items={tasks.map((t) => t.id)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tasks.length === 0 ? (
|
||||||
|
<p className="text-sm text-slate-600 text-center py-4">No tasks</p>
|
||||||
|
) : (
|
||||||
|
tasks.map((task) => (
|
||||||
|
<SortableTaskRow
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
onClick={() => onTaskClick(task)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,17 +181,24 @@ export function BacklogView() {
|
|||||||
tasks,
|
tasks,
|
||||||
sprints,
|
sprints,
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
selectedTaskId,
|
|
||||||
updateTask,
|
|
||||||
selectTask,
|
selectTask,
|
||||||
addTask,
|
updateTask,
|
||||||
|
addSprint,
|
||||||
} = useTaskStore()
|
} = useTaskStore()
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
|
||||||
const [filterType, setFilterType] = useState<string>("all")
|
|
||||||
const [filterPriority, setFilterPriority] = useState<string>("all")
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const [isCreatingTask, setIsCreatingTask] = useState(false)
|
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
|
||||||
|
current: true,
|
||||||
|
other: false,
|
||||||
|
backlog: true,
|
||||||
|
})
|
||||||
|
const [isCreatingSprint, setIsCreatingSprint] = useState(false)
|
||||||
|
const [newSprint, setNewSprint] = useState({
|
||||||
|
name: "",
|
||||||
|
goal: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
})
|
||||||
|
|
||||||
// Sensors for drag detection
|
// Sensors for drag detection
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@ -201,33 +209,28 @@ export function BacklogView() {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get backlog tasks (backlog status, no sprint assigned)
|
// Get current active sprint
|
||||||
const backlogTasks = tasks.filter(
|
const now = new Date()
|
||||||
(t) =>
|
const currentSprint = sprints.find(
|
||||||
t.projectId === selectedProjectId &&
|
(s) =>
|
||||||
t.status === "backlog" &&
|
s.status === "active" &&
|
||||||
!t.sprintId
|
new Date(s.startDate) <= now &&
|
||||||
|
new Date(s.endDate) >= now
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter tasks
|
// Get other sprints (not current)
|
||||||
const filteredTasks = backlogTasks.filter((task) => {
|
const otherSprints = sprints.filter((s) => s.id !== currentSprint?.id)
|
||||||
const matchesSearch = task.title
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchQuery.toLowerCase())
|
|
||||||
const matchesType = filterType === "all" || task.type === filterType
|
|
||||||
const matchesPriority =
|
|
||||||
filterPriority === "all" || task.priority === filterPriority
|
|
||||||
return matchesSearch && matchesType && matchesPriority
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort by priority (urgent > high > medium > low)
|
// Get tasks by section
|
||||||
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 }
|
const currentSprintTasks = currentSprint
|
||||||
const sortedTasks = [...filteredTasks].sort(
|
? tasks.filter((t) => t.sprintId === currentSprint.id)
|
||||||
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
: []
|
||||||
|
|
||||||
|
const otherSprintsTasks = otherSprints.flatMap((sprint) =>
|
||||||
|
tasks.filter((t) => t.sprintId === sprint.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get project sprints
|
const backlogTasks = tasks.filter((t) => !t.sprintId)
|
||||||
const projectSprints = sprints.filter((s) => s.projectId === selectedProjectId)
|
|
||||||
|
|
||||||
// Get active task for drag overlay
|
// Get active task for drag overlay
|
||||||
const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null
|
const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null
|
||||||
@ -242,27 +245,38 @@ export function BacklogView() {
|
|||||||
|
|
||||||
if (!over) return
|
if (!over) return
|
||||||
|
|
||||||
// Reordering logic would go here
|
const taskId = active.id as string
|
||||||
// For now, just a placeholder for future implementation
|
const overId = over.id as string
|
||||||
|
|
||||||
|
// If dropped over a section header, move task to that section's sprint
|
||||||
|
if (overId === "backlog") {
|
||||||
|
updateTask(taskId, { sprintId: undefined })
|
||||||
|
} else if (overId === "current" && currentSprint) {
|
||||||
|
updateTask(taskId, { sprintId: currentSprint.id })
|
||||||
|
} else if (overId.startsWith("sprint-")) {
|
||||||
|
const sprintId = overId.replace("sprint-", "")
|
||||||
|
updateTask(taskId, { sprintId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAssignToSprint = (taskId: string, sprintId: string) => {
|
const toggleSection = (section: string) => {
|
||||||
updateTask(taskId, { sprintId })
|
setOpenSections((prev) => ({ ...prev, [section]: !prev[section] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateTask = () => {
|
const handleCreateSprint = () => {
|
||||||
if (!selectedProjectId) return
|
if (!newSprint.name) return
|
||||||
|
|
||||||
addTask({
|
addSprint({
|
||||||
title: "New Backlog Item",
|
name: newSprint.name,
|
||||||
description: "",
|
goal: newSprint.goal,
|
||||||
type: "task",
|
startDate: newSprint.startDate || new Date().toISOString(),
|
||||||
priority: "medium",
|
endDate: newSprint.endDate || new Date().toISOString(),
|
||||||
status: "backlog",
|
status: "planning",
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId || "2",
|
||||||
tags: [],
|
|
||||||
})
|
})
|
||||||
setIsCreatingTask(false)
|
|
||||||
|
setIsCreatingSprint(false)
|
||||||
|
setNewSprint({ name: "", goal: "", startDate: "", endDate: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -273,161 +287,106 @@ export function BacklogView() {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header */}
|
{/* Current Sprint Section */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
<div id="current">
|
||||||
<div className="flex items-center gap-2">
|
<TaskSection
|
||||||
<div className="relative">
|
title={currentSprint?.name || "Current Sprint"}
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
tasks={currentSprintTasks}
|
||||||
|
isOpen={openSections.current}
|
||||||
|
onToggle={() => toggleSection("current")}
|
||||||
|
onTaskClick={(task) => selectTask(task.id)}
|
||||||
|
sprintInfo={
|
||||||
|
currentSprint
|
||||||
|
? {
|
||||||
|
name: currentSprint.name,
|
||||||
|
date: `${format(new Date(currentSprint.startDate), "MMM d")} - ${format(
|
||||||
|
new Date(currentSprint.endDate),
|
||||||
|
"MMM d"
|
||||||
|
)}`,
|
||||||
|
status: currentSprint.status,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Sprints Section */}
|
||||||
|
{otherSprints.length > 0 && (
|
||||||
|
<div id="other">
|
||||||
|
<TaskSection
|
||||||
|
title="Other Sprints"
|
||||||
|
tasks={otherSprintsTasks}
|
||||||
|
isOpen={openSections.other}
|
||||||
|
onToggle={() => toggleSection("other")}
|
||||||
|
onTaskClick={(task) => selectTask(task.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Sprint Button */}
|
||||||
|
{!isCreatingSprint ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-dashed border-slate-700 text-slate-400 hover:text-white hover:border-slate-500"
|
||||||
|
onClick={() => setIsCreatingSprint(true)}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Create Sprint
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<h4 className="font-medium text-slate-200">Create New Sprint</h4>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search backlog..."
|
placeholder="Sprint name"
|
||||||
value={searchQuery}
|
value={newSprint.name}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setNewSprint({ ...newSprint, name: e.target.value })}
|
||||||
className="pl-9 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-200 focus:outline-none focus:border-blue-500 w-full sm:w-64"
|
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
<textarea
|
||||||
<select
|
placeholder="Sprint goal (optional)"
|
||||||
value={filterType}
|
value={newSprint.goal}
|
||||||
onChange={(e) => setFilterType(e.target.value)}
|
onChange={(e) => setNewSprint({ ...newSprint, goal: e.target.value })}
|
||||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-200 focus:outline-none focus:border-blue-500"
|
rows={2}
|
||||||
>
|
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
|
||||||
<option value="all">All Types</option>
|
/>
|
||||||
<option value="idea">💡 Idea</option>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<option value="task">📋 Task</option>
|
<input
|
||||||
<option value="bug">🐛 Bug</option>
|
type="date"
|
||||||
<option value="research">🔬 Research</option>
|
value={newSprint.startDate}
|
||||||
<option value="plan">📐 Plan</option>
|
onChange={(e) => setNewSprint({ ...newSprint, startDate: e.target.value })}
|
||||||
</select>
|
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
|
||||||
<select
|
|
||||||
value={filterPriority}
|
|
||||||
onChange={(e) => setFilterPriority(e.target.value)}
|
|
||||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-200 focus:outline-none focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="all">All Priorities</option>
|
|
||||||
<option value="urgent">Urgent</option>
|
|
||||||
<option value="high">High</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="low">Low</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleCreateTask} size="sm">
|
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
|
||||||
Add to Backlog
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="flex items-center gap-4 text-sm text-slate-400">
|
|
||||||
<span>
|
|
||||||
<strong className="text-slate-200">{backlogTasks.length}</strong> items in backlog
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-slate-200">
|
|
||||||
{backlogTasks.filter((t) => t.priority === "urgent").length}
|
|
||||||
</strong>{" "}
|
|
||||||
urgent
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>
|
|
||||||
<strong className="text-slate-200">
|
|
||||||
{backlogTasks.filter((t) => t.priority === "high").length}
|
|
||||||
</strong>{" "}
|
|
||||||
high priority
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Backlog List */}
|
|
||||||
{sortedTasks.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-slate-500">
|
|
||||||
{searchQuery || filterType !== "all" || filterPriority !== "all" ? (
|
|
||||||
<p>No items match your filters</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="mb-4">Backlog is empty</p>
|
|
||||||
<Button onClick={handleCreateTask} variant="outline">
|
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
|
||||||
Add First Item
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<SortableContext
|
|
||||||
items={sortedTasks.map((t) => t.id)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
{sortedTasks.map((task) => (
|
|
||||||
<SortableBacklogItem
|
|
||||||
key={task.id}
|
|
||||||
task={task}
|
|
||||||
sprints={projectSprints}
|
|
||||||
onClick={() => selectTask(task.id)}
|
|
||||||
onAssignToSprint={(sprintId) =>
|
|
||||||
handleAssignToSprint(task.id, sprintId)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
<input
|
||||||
</SortableContext>
|
type="date"
|
||||||
</div>
|
value={newSprint.endDate}
|
||||||
|
onChange={(e) => setNewSprint({ ...newSprint, endDate: e.target.value })}
|
||||||
|
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleCreateSprint} className="flex-1">
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" onClick={() => setIsCreatingSprint(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sprint Planning Section */}
|
{/* Backlog Section */}
|
||||||
{projectSprints.length > 0 && (
|
<div id="backlog">
|
||||||
<div className="mt-8 pt-6 border-t border-slate-800">
|
<TaskSection
|
||||||
<h3 className="text-lg font-medium text-slate-200 mb-4">
|
title="Backlog"
|
||||||
Sprint Planning
|
tasks={backlogTasks}
|
||||||
</h3>
|
isOpen={openSections.backlog}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
onToggle={() => toggleSection("backlog")}
|
||||||
{projectSprints.map((sprint) => {
|
onTaskClick={(task) => selectTask(task.id)}
|
||||||
const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id)
|
/>
|
||||||
const sprintPoints = sprintTasks.length // Simple count for now
|
</div>
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={sprint.id}
|
|
||||||
className="bg-slate-800 border-slate-700 cursor-pointer hover:border-blue-500 transition-colors"
|
|
||||||
onClick={() => {
|
|
||||||
// Switch to sprint view with this sprint selected
|
|
||||||
// This would need to be passed up to parent or use store
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="font-medium text-slate-200">
|
|
||||||
{sprint.name}
|
|
||||||
</h4>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
sprint.status === "active"
|
|
||||||
? "default"
|
|
||||||
: sprint.status === "completed"
|
|
||||||
? "secondary"
|
|
||||||
: "outline"
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{sprint.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-slate-400 line-clamp-2 mb-3">
|
|
||||||
{sprint.goal || "No goal set"}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
|
||||||
<span>{sprintTasks.length} tasks</span>
|
|
||||||
<span>
|
|
||||||
{new Date(sprint.startDate).toLocaleDateString()} -{" "}
|
|
||||||
{new Date(sprint.endDate).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user