'use client' import { useState, useEffect, useTransition } from 'react' import { DndContext, DragEndEvent, DragStartEvent, DragOverlay, 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, ProductMember } from './sprint-backlog' import { TaskList } from './task-list' import type { Task } from './task-list' import { useSprintStore } from '@/stores/sprint-store' import { addStoryToSprintAction, removeStoryFromSprintAction, reorderSprintStoriesAction, } from '@/actions/sprints' interface SprintBoardClientProps { productId: string sprintId: string stories: SprintStory[] pbisWithStories: PbiWithStories[] sprintStoryIdList: string[] tasksByStory: Record isDemo: boolean currentUserId: string members: ProductMember[] } export function SprintBoardClient({ productId, sprintId, stories, pbisWithStories, sprintStoryIdList, tasksByStory, isDemo, currentUserId, members, }: SprintBoardClientProps) { const [sprintStories, setSprintStories] = useState(stories) const [sprintStoryIds, setSprintStoryIds] = useState>(() => new Set(sprintStoryIdList)) const [selectedStoryId, setSelectedStoryId] = useState(null) const { sprintStoryOrder, initSprint, addStoryToSprint, removeStoryFromSprint, reorderSprintStories, rollbackSprint, } = useSprintStore() const [activeDragStory, setActiveDragStory] = useState(null) 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 handleDragStart(event: DragStartEvent) { const id = event.active.id.toString() if (id.startsWith('pb:')) { const story = pbisWithStories.flatMap(p => p.stories).find(s => s.id === id.slice(3)) setActiveDragStory(story ?? null) } } function handleDragEnd(event: DragEndEvent) { setActiveDragStory(null) 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) // Drag from left (product backlog) → add to sprint (middle) 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 } // Drag from middle (sprint backlog) → left (product backlog) → remove 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) if (selectedStoryId === activeId) setSelectedStoryId(null) 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 (middle panel) if (activeId !== overId && !activeId.startsWith('pb:')) { const prevOrder = [...order] const newOrder = order.includes(overId) ? arrayMove([...order], order.indexOf(activeId), order.indexOf(overId)) : [...order.filter(id => id !== activeId), activeId] reorderSprintStories(sprintId, newOrder) startTransition(async () => { const result = await reorderSprintStoriesAction(sprintId, newOrder) if (!result.success) { rollbackSprint(sprintId, prevOrder) toast.error('Volgorde opslaan mislukt') } }) } } function handleAdd(storyId: string) { if (sprintStoryIds.has(storyId)) return 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') } }) } function handleAssigneeChange(storyId: string, assigneeId: string | null, assigneeUsername: string | null) { setSprintStories(prev => prev.map(s => s.id === storyId ? { ...s, assignee_id: assigneeId, assignee_username: assigneeUsername } : s) ) } 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) if (selectedStoryId === storyId) setSelectedStoryId(null) 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') } }) } const selectedTasks = selectedStoryId ? (tasksByStory[selectedStoryId] ?? []) : [] return ( , , selectedStoryId ? ( s.id === selectedStoryId)?.code ?? null} sprintId={sprintId} productId={productId} tasks={selectedTasks} isDemo={isDemo} /> ) : (

Selecteer een story om de taken te bekijken.

), ]} /> {activeDragStory && (
{activeDragStory.title}
)}
) }