'use client' import { useState, useTransition } from 'react' import { useRouter, usePathname } from 'next/navigation' import { DndContext, DragEndEvent, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, sortableKeyboardCoordinates, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Pencil } from 'lucide-react' import { toast } from 'sonner' import { useShallow } from 'zustand/react/shallow' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { CodeBadge } from '@/components/shared/code-badge' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { PRIORITY_BORDER } from '@/components/backlog/backlog-card' import { useSprintWorkspaceStore } from '@/stores/sprint-workspace/store' import { selectTasksForActiveStory } from '@/stores/sprint-workspace/selectors' import type { SprintWorkspaceTask, SprintWorkspaceTaskDetail, } from '@/stores/sprint-workspace/types' import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { debugProps } from '@/lib/debug' import { cn } from '@/lib/utils' const STATUS_CYCLE: Record = { TO_DO: 'IN_PROGRESS', IN_PROGRESS: 'DONE', DONE: 'TO_DO', EXCLUDED: 'TO_DO', } 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', DONE: 'bg-status-done/15 text-status-done border-status-done/30', EXCLUDED: 'bg-surface-container-low text-muted-foreground border-border', FAILED: 'bg-status-failed/15 text-status-failed border-status-failed/30', REVIEW: 'bg-status-review/15 text-status-review border-status-review/30', } const STATUS_LABELS: Record = { TO_DO: 'To Do', IN_PROGRESS: 'Bezig', REVIEW: 'Review', DONE: 'Klaar', FAILED: 'Mislukt', EXCLUDED: 'Uitgesloten', } // Behouden voor type-compat met SprintBoardClient props (verdwijnt zodra // SprintBoardClient ook geen tasks-prop meer doorgeeft — T-883). export interface Task { id: string code: string | null title: string description: string | null priority: number status: string story_id: string sprint_id: string | null } type WorkspaceTask = SprintWorkspaceTask | SprintWorkspaceTaskDetail interface TaskListProps { sprintId: string productId: string isDemo: boolean } function SortableTaskRow({ task, code, isDemo, onStatusToggle, onEdit, }: { task: WorkspaceTask code: string | null isDemo: boolean onStatusToggle: () => void onEdit: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 } return (
onEdit()} role="button" tabIndex={0} aria-label={`Bewerk taak: ${task.title}`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() onEdit() } }} > {!isDemo && ( e.stopPropagation()} className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none mt-0.5" aria-hidden="true" > ⠿ )}

{task.title}

{code && }
) } export function TaskList({ sprintId: _sprintId, productId: _productId, isDemo }: TaskListProps) { const storyId = useSprintWorkspaceStore((s) => s.context.activeStoryId) const orderedTasks = useSprintWorkspaceStore( useShallow(selectTasksForActiveStory), ) const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() const router = useRouter() const pathname = usePathname() const taskMap: Record = {} for (const t of orderedTasks) taskMap[t.id] = t const doneCount = orderedTasks.filter(t => t.status === 'DONE').length const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ) function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!storyId) return if (!over || active.id === over.id) return const store = useSprintWorkspaceStore.getState() const prevOrder = [...(store.relations.taskIdsByStory[storyId] ?? [])] const newOrder = arrayMove( [...prevOrder], prevOrder.indexOf(active.id as string), prevOrder.indexOf(over.id as string), ) const mutationId = store.applyOptimisticMutation({ kind: 'sprint-task-order', storyId, prevTaskIds: prevOrder, }) useSprintWorkspaceStore.setState((s) => { s.relations.taskIdsByStory[storyId] = newOrder }) setActiveDragId(null) startTransition(async () => { const result = await reorderTasksAction(storyId, newOrder) const st = useSprintWorkspaceStore.getState() if (result.success) { st.settleMutation(mutationId) } else { st.rollbackMutation(mutationId) toast.error('Volgorde opslaan mislukt') } }) } function handleStatusToggle(task: WorkspaceTask) { startTransition(async () => { await updateTaskStatusAction(task.id, STATUS_CYCLE[task.status] ?? 'TO_DO') }) } function openCreateDialog() { if (!storyId) return router.push(`${pathname}?newTask=1&storyId=${storyId}`) } function openEditDialog(taskId: string) { router.push(`${pathname}?editTask=${taskId}`) } return (
{doneCount}/{orderedTasks.length} klaar } />
{orderedTasks.length === 0 ? (

Geen taken voor deze story.

) : ( setActiveDragId(e.active.id as string)} onDragEnd={handleDragEnd} > t.id)} strategy={verticalListSortingStrategy}> {orderedTasks.map((task) => ( handleStatusToggle(task)} onEdit={() => openEditDialog(task.id)} /> ))} {activeDragId && taskMap[activeDragId] && (
{taskMap[activeDragId].title}
)}
)}
) }