"use client" import { useEffect, useState, type ReactNode } from "react" import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useDroppable, useSensor, useSensors, closestCorners, } from "@dnd-kit/core" import { SortableContext, verticalListSortingStrategy, useSortable, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { useRouter } from "next/navigation" import { useTaskStore, Task } from "@/stores/useTaskStore" import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react" import { format, isValid, parseISO } from "date-fns" import { generateAvatarDataUrl } from "@/lib/avatar" const priorityColors: Record = { low: "bg-slate-600", medium: "bg-blue-600", high: "bg-orange-600", urgent: "bg-red-600", } const typeLabels: Record = { idea: "💡", task: "📋", bug: "🐛", research: "🔬", plan: "📐", } interface AssignableUser { id: string name: string email?: string avatarUrl?: string } function AssigneeAvatar({ name, avatarUrl, seed }: { name?: string; avatarUrl?: string; seed?: string }) { const displayUrl = avatarUrl || generateAvatarDataUrl(seed || name || "unassigned", name || "Unassigned") return ( {name ) } // Sortable Task Row function SortableTaskRow({ task, assigneeAvatarUrl, onClick, }: { task: Task assigneeAvatarUrl?: string onClick: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: task.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } return (
{typeLabels[task.type]}

{task.title}

{task.priority} {task.comments && task.comments.length > 0 && ( 💬 {task.comments.length} )}
) } // Drag Overlay Item function DragOverlayItem({ task, assigneeAvatarUrl }: { task: Task; assigneeAvatarUrl?: string }) { return (
{typeLabels[task.type]}

{task.title}

{task.priority}
) } function SectionDropZone({ id, children }: { id: string; children: ReactNode }) { const { isOver, setNodeRef } = useDroppable({ id }) return (
{children}
) } // Collapsible Section function TaskSection({ title, tasks, isOpen, onToggle, onTaskClick, resolveAssigneeAvatar, sprintInfo, }: { title: string tasks: Task[] isOpen: boolean onToggle: () => void onTaskClick: (task: Task) => void resolveAssigneeAvatar: (task: Task) => string | undefined sprintInfo?: { name: string; date: string; status: string } }) { return (
{isOpen && (
t.id)} strategy={verticalListSortingStrategy} >
{tasks.length === 0 ? (

No tasks

) : ( tasks.map((task) => ( onTaskClick(task)} /> )) )}
)}
) } export function BacklogView() { const router = useRouter() const [assignableUsers, setAssignableUsers] = useState([]) const { tasks, sprints, selectedProjectId, updateTask, addSprint, } = useTaskStore() useEffect(() => { let active = true const loadUsers = async () => { try { const response = await fetch("/api/auth/users", { cache: "no-store" }) if (!response.ok) return const data = await response.json() if (!active || !Array.isArray(data?.users)) return setAssignableUsers( data.users.map((entry: { id: string; name: string; email?: string; avatarUrl?: string }) => ({ id: entry.id, name: entry.name, email: entry.email, avatarUrl: entry.avatarUrl, })), ) } catch { // Keep backlog usable if users lookup fails. } } void loadUsers() return () => { active = false } }, []) const resolveAssigneeAvatar = (task: Task) => { if (!task.assigneeId) return task.assigneeAvatarUrl return assignableUsers.find((user) => user.id === task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl } const [activeId, setActiveId] = useState(null) const [openSections, setOpenSections] = useState>({ current: true, backlog: true, }) const [isCreatingSprint, setIsCreatingSprint] = useState(false) const [newSprint, setNewSprint] = useState({ name: "", goal: "", startDate: "", endDate: "", }) // Sensors for drag detection const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ) // Get current active sprint const now = new Date() const currentSprint = sprints.find( (s) => s.status === "active" && new Date(s.startDate) <= now && new Date(s.endDate) >= now ) // Get other sprints (not current) const otherSprints = sprints.filter((s) => s.id !== currentSprint?.id) // Get tasks by section const currentSprintTasks = currentSprint ? tasks.filter((t) => t.sprintId === currentSprint.id) : [] const backlogTasks = tasks.filter((t) => !t.sprintId) // Get active task for drag overlay const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string) } const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event setActiveId(null) if (!over) return const taskId = active.id as string const overId = over.id as string const overTask = tasks.find((t) => t.id === overId) const destinationId = overTask ? overTask.sprintId ? currentSprint && overTask.sprintId === currentSprint.id ? "current" : `sprint-${overTask.sprintId}` : "backlog" : overId // If dropped over a section header, move task to that section's sprint if (destinationId === "backlog") { updateTask(taskId, { sprintId: undefined, status: "open" }) } else if (destinationId === "current" && currentSprint) { updateTask(taskId, { sprintId: currentSprint.id, status: "open" }) } else if (destinationId.startsWith("sprint-")) { const sprintId = destinationId.replace("sprint-", "") updateTask(taskId, { sprintId, status: "open" }) } } const toggleSection = (section: string) => { setOpenSections((prev) => ({ ...prev, [section]: !prev[section] })) } const handleCreateSprint = () => { if (!newSprint.name) return addSprint({ name: newSprint.name, goal: newSprint.goal, startDate: newSprint.startDate || new Date().toISOString(), endDate: newSprint.endDate || new Date().toISOString(), status: "planning", projectId: selectedProjectId || "2", }) setIsCreatingSprint(false) setNewSprint({ name: "", goal: "", startDate: "", endDate: "" }) } return (
{/* Current Sprint Section */} toggleSection("current")} onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)} resolveAssigneeAvatar={resolveAssigneeAvatar} sprintInfo={ currentSprint ? { name: currentSprint.name, date: `${(() => { const start = parseISO(currentSprint.startDate) const end = parseISO(currentSprint.endDate) if (!isValid(start) || !isValid(end)) return "Invalid dates" return `${format(start, "MMM d")} - ${format(end, "MMM d")}` })()}`, status: currentSprint.status, } : undefined } /> {/* Other Sprints Sections - ordered by start date */} {otherSprints .sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()) .map((sprint) => { const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id) console.log(`Sprint ${sprint.name}: ${sprintTasks.length} tasks`, sprintTasks.map(t => t.title)) return ( toggleSection(sprint.id)} onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)} resolveAssigneeAvatar={resolveAssigneeAvatar} sprintInfo={{ name: sprint.name, date: (() => { const start = parseISO(sprint.startDate) const end = parseISO(sprint.endDate) if (!isValid(start) || !isValid(end)) return "Invalid dates" return `${format(start, "MMM d")} - ${format(end, "MMM d")}` })(), status: sprint.status, }} /> ) })} {/* Create Sprint Button */} {!isCreatingSprint ? ( ) : (

Create New Sprint

setNewSprint({ ...newSprint, name: e.target.value })} className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white" />