"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 } from "date-fns" import { parseSprintEnd, parseSprintStart, toLocalDateInputValue } from "@/lib/utils" 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: "📐", } function formatSprintDateRange(startDate?: string, endDate?: string): string { if (!startDate || !endDate) return "No dates" const start = parseSprintStart(startDate) const end = parseSprintEnd(endDate) if (!isValid(start) || !isValid(end)) return "Invalid dates" return `${format(start, "MMM d")} - ${format(end, "MMM d")}` } 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)} /> )) )}
)}
) } interface BacklogViewProps { searchQuery?: string } export function BacklogView({ searchQuery = "" }: BacklogViewProps) { 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 } // Filter tasks by search query const matchesSearch = (task: Task): boolean => { if (!searchQuery.trim()) return true const query = searchQuery.toLowerCase() const matchesTitle = task.title.toLowerCase().includes(query) const matchesDescription = task.description?.toLowerCase().includes(query) ?? false return matchesTitle || matchesDescription } 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 // Treat end date as end-of-day (23:59:59) to handle timezone issues const now = new Date() const currentSprint = sprints.find((s) => { if (s.status !== "active") return false const sprintStart = parseSprintStart(s.startDate) const sprintEnd = parseSprintEnd(s.endDate) return sprintStart <= now && sprintEnd >= now }) // Get other sprints (not current) const otherSprints = sprints.filter((s) => s.id !== currentSprint?.id) // Sort tasks by updatedAt (descending) - latest first const sortByUpdated = (a: Task, b: Task) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() // Get tasks by section (sorted by last updated) const currentSprintTasks = currentSprint ? tasks.filter((t) => t.sprintId === currentSprint.id && matchesSearch(t)).sort(sortByUpdated) : [] const backlogTasks = tasks.filter((t) => !t.sprintId && matchesSearch(t)).sort(sortByUpdated) // 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 || toLocalDateInputValue(), endDate: newSprint.endDate || toLocalDateInputValue(), 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: formatSprintDateRange(currentSprint.startDate, currentSprint.endDate), status: currentSprint.status, } : undefined } /> {/* Other Sprints Sections - ordered by start date */} {otherSprints .sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()) .map((sprint) => { const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id && matchesSearch(t)).sort(sortByUpdated) 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: formatSprintDateRange(sprint.startDate, sprint.endDate), 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" />