'use client' import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { SortableContext, useSortable, rectSortingStrategy, arrayMove, sortableKeyboardCoordinates, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { useShallow } from 'zustand/react/shallow' import { useProductWorkspaceStore } from '@/stores/product-workspace/store' import { selectTasksForActiveStory } from '@/stores/product-workspace/selectors' import type { BacklogTask, TaskDetail, } from '@/stores/product-workspace/types' import { reorderTasksAction } from '@/actions/tasks' import { BacklogCard } from './backlog-card' import { debugProps } from '@/lib/debug' import { EmptyPanel } from './empty-panel' import { cn } from '@/lib/utils' const STATUS_COLORS: Record = { TO_DO: 'bg-status-todo/15 text-status-todo border-status-todo/30', IN_PROGRESS: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30', REVIEW: 'bg-status-review/15 text-status-review border-status-review/30', DONE: 'bg-status-done/15 text-status-done border-status-done/30', } const STATUS_LABELS: Record = { TO_DO: 'To Do', IN_PROGRESS: 'Bezig', REVIEW: 'Review', DONE: 'Klaar', } function SortableTaskCard({ task, isDemo, onClick, }: { task: BacklogTask | TaskDetail isDemo: boolean onClick: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id }) const style = { transform: CSS.Transform.toString(transform), transition, } return ( {STATUS_LABELS[task.status] ?? task.status} } /> ) } interface TaskPanelProps { productId: string isDemo: boolean closePath: string } // PBI-74 / T-851: leest tasks voor active story via selectTasksForActiveStory // (useShallow). DnD via applyOptimisticMutation('task-order'). Detail-view // (ensureTaskLoaded + isDetail()) zit in de task-dialog, niet in deze lijst. export function TaskPanel({ isDemo, closePath }: TaskPanelProps) { const router = useRouter() const [, startTransition] = useTransition() const selectedStoryId = useProductWorkspaceStore((s) => s.context.activeStoryId) const rawTasks = useProductWorkspaceStore(useShallow(selectTasksForActiveStory)) as | (BacklogTask | TaskDetail)[] const [activeDragId, setActiveDragId] = useState(null) const tasks: (BacklogTask | TaskDetail)[] | null = selectedStoryId ? rawTasks : null 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) if (!selectedStoryId || !tasks) return const { active, over } = event if (!over || active.id === over.id) return const store = useProductWorkspaceStore.getState() const prevOrder = [...(store.relations.taskIdsByStory[selectedStoryId] ?? [])] const oldIndex = prevOrder.indexOf(active.id as string) const newIndex = prevOrder.indexOf(over.id as string) if (oldIndex === -1 || newIndex === -1) return const newOrder = arrayMove([...prevOrder], oldIndex, newIndex) const orderMutationId = store.applyOptimisticMutation({ kind: 'task-order', storyId: selectedStoryId, prevTaskIds: prevOrder, }) useProductWorkspaceStore.setState((s) => { s.relations.taskIdsByStory[selectedStoryId] = newOrder }) startTransition(async () => { const result = await reorderTasksAction(selectedStoryId, newOrder) const st = useProductWorkspaceStore.getState() if (result?.error) { st.rollbackMutation(orderMutationId) toast.error(result.error) } else { st.settleMutation(orderMutationId) } }) } const navActions = ( ) const dp = debugProps('task-panel', 'TaskPanel', 'components/backlog/task-panel.tsx') if (tasks === null) { return (
) } if (tasks.length === 0) { return (
router.push(`${closePath}?newTask=1&storyId=${selectedStoryId}`), disabled: isDemo, }} />
) } const activeTask = activeDragId ? tasks.find((t) => t.id === activeDragId) : null return (
t.id)} strategy={rectSortingStrategy}>
{tasks.map((task) => ( router.push(`${closePath}?editTask=${task.id}`)} /> ))}
{activeTask && ( )}
) }