'use client' import { useState, useEffect, useTransition } from 'react' import { DndContext, DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { sortableKeyboardCoordinates, arrayMove } from '@dnd-kit/sortable' import { toast } from 'sonner' import { SplitPane } from '@/components/split-pane/split-pane' import { SprintBacklogLeft, SprintBacklogRight } from './sprint-backlog' import type { SprintStory, PbiWithStories } from './sprint-backlog' import { useSprintStore } from '@/stores/sprint-store' import { addStoryToSprintAction, removeStoryFromSprintAction, reorderSprintStoriesAction, } from '@/actions/sprints' interface SprintBacklogClientProps { productId: string sprintId: string stories: SprintStory[] pbisWithStories: PbiWithStories[] sprintStoryIdList: string[] isDemo: boolean } export function SprintBacklogClient({ productId, sprintId, stories, pbisWithStories, sprintStoryIdList, isDemo, }: SprintBacklogClientProps) { const [sprintStories, setSprintStories] = useState(stories) const [sprintStoryIds, setSprintStoryIds] = useState>(() => new Set(sprintStoryIdList)) const { sprintStoryOrder, initSprint, addStoryToSprint, removeStoryFromSprint, reorderSprintStories, rollbackSprint, } = useSprintStore() const [, startTransition] = useTransition() useEffect(() => { initSprint(sprintId, stories.map(s => s.id)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [sprintId]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over) return const activeId = active.id.toString() const overId = over.id.toString() const order = sprintStoryOrder[sprintId] ?? sprintStories.map(s => s.id) // Dragged from right panel (pb: prefix) → add to sprint if (activeId.startsWith('pb:')) { const storyId = activeId.slice(3) const droppingOnSprint = overId === 'sprint-zone' || (!overId.startsWith('pb:') && overId !== 'backlog-zone') if (droppingOnSprint && !sprintStoryIds.has(storyId)) { const storyData = pbisWithStories.flatMap(p => p.stories).find(s => s.id === storyId) if (!storyData) return setSprintStoryIds(prev => new Set([...prev, storyId])) setSprintStories(prev => [...prev, storyData]) addStoryToSprint(sprintId, storyId) startTransition(async () => { const result = await addStoryToSprintAction(sprintId, storyId) if (!result.success) { setSprintStoryIds(prev => { const n = new Set(prev); n.delete(storyId); return n }) setSprintStories(prev => prev.filter(s => s.id !== storyId)) removeStoryFromSprint(sprintId, storyId) toast.error(result.error ?? 'Toevoegen mislukt') } }) } return } // Dragged from left panel to right panel → remove from sprint if (overId === 'backlog-zone') { const storyData = sprintStories.find(s => s.id === activeId) setSprintStoryIds(prev => { const n = new Set(prev); n.delete(activeId); return n }) setSprintStories(prev => prev.filter(s => s.id !== activeId)) removeStoryFromSprint(sprintId, activeId) startTransition(async () => { const result = await removeStoryFromSprintAction(activeId) if (!result.success) { if (storyData) { setSprintStoryIds(prev => new Set([...prev, activeId])) setSprintStories(prev => [...prev, storyData]) } addStoryToSprint(sprintId, activeId) toast.error('Verwijderen mislukt') } }) return } // Reorder within sprint if (activeId !== overId && order.includes(overId)) { const prevOrder = [...order] const newOrder = arrayMove([...order], order.indexOf(activeId), order.indexOf(overId)) 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) { const storyData = sprintStories.find(s => s.id === storyId) setSprintStoryIds(prev => { const n = new Set(prev); n.delete(storyId); return n }) setSprintStories(prev => prev.filter(s => s.id !== storyId)) removeStoryFromSprint(sprintId, storyId) startTransition(async () => { const result = await removeStoryFromSprintAction(storyId) if (!result.success) { if (storyData) { setSprintStoryIds(prev => new Set([...prev, storyId])) setSprintStories(prev => [...prev, storyData]) } addStoryToSprint(sprintId, storyId) toast.error('Verwijderen mislukt') } }) } return ( } right={ } /> ) }