'use client' import { useState, useTransition, useEffect } from 'react' import { useActionState } from 'react' import { useFormStatus } from 'react-dom' import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' 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 { createPbiAction, deletePbiAction } from '@/actions/pbis' import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories' 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', } interface Pbi { id: string title: string priority: number } interface PbiListProps { productId: string pbis: Pbi[] isDemo: boolean } // --- Sortable PBI row --- function SortablePbiRow({ pbi, isSelected, isDemo, onSelect, onDelete, }: { pbi: Pbi isSelected: boolean isDemo: boolean onSelect: () => void onDelete: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: pbi.id, }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1, } return (
{!isDemo && ( e.stopPropagation()} > ⠿ )} {pbi.title} {!isDemo && ( )}
) } // --- Inline create form --- function CreatePbiForm({ productId, priority, onDone, }: { productId: string priority: number onDone: () => void }) { const [state, formAction] = useActionState( async (_prev: unknown, fd: FormData) => { const result = await createPbiAction(_prev, fd) if (result?.success) onDone() return result }, undefined ) const error = state?.error return (
{typeof error === 'string' && (

{error}

)} ) } function CreateSubmitButton() { const { pending } = useFormStatus() return ( ) } // --- Main component --- export function PbiList({ productId, pbis, isDemo }: PbiListProps) { const { selectedPbiId, selectPbi } = useSelectionStore() const { pbiOrder, pbiPriority, initPbis, reorderPbis, rollbackPbis, updatePbiPriority } = usePlannerStore() const [filterPriority, setFilterPriority] = useState(null) const [creatingForPriority, setCreatingForPriority] = useState(null) const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() // Sync server data into store — use stable string dep to avoid infinite loop const pbiIdKey = pbis.map(p => p.id).join(',') useEffect(() => { initPbis(productId, pbiIdKey ? pbiIdKey.split(',') : []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [productId, pbiIdKey]) // Build ordered PBI list from store (or fall back to server order) const order = pbiOrder[productId] ?? pbis.map(p => p.id) const pbiMap = Object.fromEntries(pbis.map(p => [p.id, p])) // Apply priority overrides from store const orderedPbis = order .map(id => pbiMap[id]) .filter(Boolean) .map(p => ({ ...p, priority: pbiPriority[p.id] ?? p.priority })) const filtered = filterPriority ? orderedPbis.filter(p => p.priority === filterPriority) : orderedPbis const grouped = [1, 2, 3, 4].reduce>((acc, p) => { acc[p] = filtered.filter(pbi => pbi.priority === p) return acc }, {} as Record) const visiblePriorities = [1, 2, 3, 4].filter( p => grouped[p].length > 0 || creatingForPriority === p ) const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })) 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) return const activePbi = pbiMap[active.id as string] const overPbi = pbiMap[over.id as string] if (!activePbi || !overPbi) 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) // Optimistic update reorderPbis(productId, newOrder) const priorityChanged = activePbi.priority !== overPbi.priority startTransition(async () => { if (priorityChanged) { updatePbiPriority(active.id as string, overPbi.priority) const result = await updatePbiPriorityAction(active.id as string, overPbi.priority, productId) if (!result.success) { rollbackPbis(productId, prevOrder) toast.error('Prioriteit opslaan mislukt') } } else { const result = await reorderPbisAction(productId, newOrder) if (!result.success) { rollbackPbis(productId, prevOrder) toast.error('Volgorde opslaan mislukt') } } }) } function handleDelete(id: string) { startTransition(async () => { await deletePbiAction(id) if (selectedPbiId === id) selectPbi(null) }) } const activePbi = activeDragId ? pbiMap[activeDragId] : null return (
{filterPriority !== null && ( )} {!isDemo && ( )} } />
{pbis.length === 0 && creatingForPriority === null ? (

Nog geen PBI's aangemaakt.

{!isDemo && ( )}
) : (
{visiblePriorities.map(priority => (
{PRIORITY_LABELS[priority]}
{!isDemo && ( )}
p.id)} strategy={verticalListSortingStrategy} > {grouped[priority].map(pbi => ( selectPbi(pbi.id)} onDelete={() => handleDelete(pbi.id)} /> ))} {creatingForPriority === priority && ( setCreatingForPriority(null)} /> )}
))} {creatingForPriority !== null && !visiblePriorities.includes(creatingForPriority) && (
{PRIORITY_LABELS[creatingForPriority]}
setCreatingForPriority(null)} />
)}
{activePbi && (
{activePbi.title}
)}
)}
) }