'use client' import { useState, useTransition, useEffect } from 'react' import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { SortableContext, useSortable, horizontalListSortingStrategy, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { useSelectionStore } from '@/stores/selection-store' import { usePlannerStore } from '@/stores/planner-store' import { reorderStoriesAction } from '@/actions/stories' import { StoryDialog, type StoryDialogState } from './story-dialog' import { cn } from '@/lib/utils' const PRIORITY_LABELS: Record = { 1: 'Kritiek', 2: 'Hoog', 3: 'Gemiddeld', 4: 'Laag' } 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 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', } export interface Story { id: string title: string description: string | null acceptance_criteria: string | null priority: number status: string pbi_id: string } interface StoryPanelProps { productId: string storiesByPbi: Record isDemo: boolean } // --- Sortable story block --- function SortableStoryBlock({ story, onClick, }: { story: Story 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 (

{story.title}

{PRIORITY_LABELS[story.priority]} {STATUS_LABELS[story.status] ?? story.status}
) } // --- Main component --- export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps) { const { selectedPbiId } = useSelectionStore() const { storyOrder, initStories, reorderStories, rollbackStories } = usePlannerStore() const [filterStatus, setFilterStatus] = useState(null) const [filterPriority, setFilterPriority] = useState(null) const [storyDialogState, setStoryDialogState] = useState(null) const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() const rawStories = selectedPbiId ? (storiesByPbi[selectedPbiId] ?? []) : [] // Sync into store — use stable string dep to avoid infinite loop const storyIdKey = rawStories.map(s => s.id).join(',') useEffect(() => { if (selectedPbiId) { initStories(selectedPbiId, storyIdKey ? storyIdKey.split(',') : []) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedPbiId, storyIdKey]) const storyMap = Object.fromEntries(rawStories.map(s => [s.id, s])) const order = (selectedPbiId ? storyOrder[selectedPbiId] : null) ?? rawStories.map(s => s.id) const orderedStories = order.map(id => storyMap[id]).filter(Boolean) const filtered = orderedStories .filter(s => !filterStatus || s.status === filterStatus) .filter(s => !filterPriority || s.priority === filterPriority) const grouped = [1, 2, 3, 4].reduce>((acc, p) => { acc[p] = filtered.filter(s => s.priority === p) return acc }, {} as Record) const visiblePriorities = [1, 2, 3, 4].filter(p => grouped[p].length > 0) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) function handleDragStart(event: DragStartEvent) { setActiveDragId(event.active.id as string) } function handleDragEnd(event: DragEndEvent) { setActiveDragId(null) const { active, over } = event if (!over || active.id === over.id || !selectedPbiId) return const activeStory = storyMap[active.id as string] const overStory = storyMap[over.id as string] if (!activeStory || !overStory) return const prevOrder = [...order] const oldIndex = order.indexOf(active.id as string) const newIndex = order.indexOf(over.id as string) const newOrder = arrayMove([...order], oldIndex, newIndex) reorderStories(selectedPbiId, newOrder) const priorityChanged = activeStory.priority !== overStory.priority startTransition(async () => { const result = await reorderStoriesAction( selectedPbiId, productId, newOrder, priorityChanged ? overStory.priority : undefined ) if (!result.success) { rollbackStories(selectedPbiId, prevOrder) toast.error('Volgorde opslaan mislukt') } }) } const hasActiveFilters = filterStatus !== null || filterPriority !== null return (
{hasActiveFilters && ( )} {selectedPbiId && !isDemo && ( )} } />
{selectedPbiId === null ? (

Selecteer een PBI om de stories te bekijken.

) : rawStories.length === 0 ? (

Nog geen stories voor dit PBI.

{!isDemo && selectedPbiId && ( )}
) : (
{visiblePriorities.map(priority => (
{PRIORITY_LABELS[priority]}
{!isDemo && selectedPbiId && ( )}
s.id)} strategy={horizontalListSortingStrategy} >
{grouped[priority].map(story => ( setStoryDialogState({ mode: 'edit', story, productId })} /> ))}
))}
{activeDragId && storyMap[activeDragId] && (

{storyMap[activeDragId].title}

)}
)}
setStoryDialogState(null)} isDemo={isDemo} />
) }