'use client' import { useState, useTransition, useEffect } from 'react' import { useRouter } from 'next/navigation' import { DndContext, DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, sortableKeyboardCoordinates, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { useSprintStore } from '@/stores/sprint-store' import { addStoryToSprintAction, removeStoryFromSprintAction, reorderSprintStoriesAction, } from '@/actions/sprints' import { cn } from '@/lib/utils' const STATUS_COLORS: Record = { OPEN: 'bg-status-todo/15 text-status-todo border-status-todo/30', IN_SPRINT: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30', DONE: 'bg-status-done/15 text-status-done border-status-done/30', } const STATUS_LABELS: Record = { OPEN: 'Open', IN_SPRINT: 'In Sprint', DONE: 'Klaar' } const PRIORITY_COLORS: Record = { 1: 'bg-priority-critical/15 text-priority-critical border-priority-critical/30', 2: 'bg-priority-high/15 text-priority-high border-priority-high/30', 3: 'bg-priority-medium/15 text-priority-medium border-priority-medium/30', 4: 'bg-priority-low/15 text-priority-low border-priority-low/30', } const PRIORITY_LABELS: Record = { 1: 'Kritiek', 2: 'Hoog', 3: 'Gemiddeld', 4: 'Laag' } export interface SprintStory { id: string title: string priority: number status: string taskCount: number doneCount: number } export interface PbiWithStories { id: string title: string stories: SprintStory[] } // --- Left panel: Sprint Backlog --- function SortableSprintRow({ story, isDemo, onRemove, onClick, }: { story: SprintStory; isDemo: boolean; onRemove: () => void; onClick: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: story.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 } return (
{!isDemo && ( e.stopPropagation()} aria-label="Versleep om te sorteren" className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 select-none text-sm"> ⠿ )}

{story.title}

{PRIORITY_LABELS[story.priority]} {story.doneCount}/{story.taskCount} klaar
{!isDemo && ( )}
) } interface SprintBacklogLeftProps { sprintId: string stories: SprintStory[] isDemo: boolean onSelectStory: (id: string) => void selectedStoryId: string | null } export function SprintBacklogLeft({ sprintId, stories, isDemo, onSelectStory }: SprintBacklogLeftProps) { const { sprintStoryOrder, initSprint, reorderSprintStories, rollbackSprint, removeStoryFromSprint } = useSprintStore() const [, startTransition] = useTransition() const idKey = stories.map(s => s.id).join(',') useEffect(() => { initSprint(sprintId, idKey ? idKey.split(',') : []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [sprintId, idKey]) const storyMap = Object.fromEntries(stories.map(s => [s.id, s])) const order = sprintStoryOrder[sprintId] ?? stories.map(s => s.id) const orderedStories = order.map(id => storyMap[id]).filter(Boolean) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id) return const prevOrder = [...order] const newOrder = arrayMove([...order], order.indexOf(active.id as string), order.indexOf(over.id as string)) reorderSprintStories(sprintId, newOrder) startTransition(async () => { const result = await reorderSprintStoriesAction(sprintId, newOrder) if (!result.success) { rollbackSprint(sprintId, prevOrder); toast.error('Volgorde opslaan mislukt') } }) } function handleRemove(storyId: string) { removeStoryFromSprint(sprintId, storyId) startTransition(async () => { const result = await removeStoryFromSprintAction(storyId) if (!result.success) toast.error('Verwijderen mislukt') }) } return (
{orderedStories.length === 0 ? (

Geen stories in de Sprint. Sleep stories vanuit het rechterpaneel.

) : ( s.id)} strategy={verticalListSortingStrategy}> {orderedStories.map(story => ( handleRemove(story.id)} onClick={() => onSelectStory(story.id)} /> ))} )}
) } // --- Right panel: Product Backlog stories grouped by PBI (droppable source) --- interface SprintBacklogRightProps { sprintId: string pbisWithStories: PbiWithStories[] sprintStoryIds: Set isDemo: boolean onStoryAdded: (storyId: string) => void } export function SprintBacklogRight({ sprintId, pbisWithStories, sprintStoryIds, isDemo, onStoryAdded }: SprintBacklogRightProps) { const [collapsed, setCollapsed] = useState>(new Set()) const [, startTransition] = useTransition() const { addStoryToSprint } = useSprintStore() const router = useRouter() function toggle(pbiId: string) { setCollapsed(prev => { const next = new Set(prev) if (next.has(pbiId)) { next.delete(pbiId) } else { next.add(pbiId) } return next }) } function handleAdd(storyId: string) { addStoryToSprint(sprintId, storyId) onStoryAdded(storyId) startTransition(async () => { const result = await addStoryToSprintAction(sprintId, storyId) if (!result.success) { toast.error(result.error ?? 'Toevoegen mislukt') router.refresh() } }) } return (
{pbisWithStories.map(pbi => (
{!collapsed.has(pbi.id) && pbi.stories.map(story => { const inSprint = sprintStoryIds.has(story.id) return (

{story.title}

{STATUS_LABELS[story.status]}
{!inSprint && !isDemo && ( )} {inSprint && In Sprint}
) })}
))}
) }