'use client' import { useState, useTransition, useEffect } 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 { 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 { useSprintStore } from '@/stores/sprint-store' 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', } export interface Task { id: string code: string title: string description: string | null priority: number status: string story_id: string sprint_id: string | null } interface TaskListProps { storyId: string sprintId: string productId: string tasks: Task[] isDemo: boolean } function SortableTaskRow({ task, code, isDemo, onStatusToggle, onEdit, }: { task: Task 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({ storyId, sprintId: _sprintId, productId: _productId, tasks, isDemo }: TaskListProps) { const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore() const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() const router = useRouter() const pathname = usePathname() const idKey = tasks.map(t => t.id).join(',') useEffect(() => { initTasks(storyId, idKey ? idKey.split(',') : []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [storyId, idKey]) const taskMap = Object.fromEntries(tasks.map(t => [t.id, t])) const order = taskOrder[storyId] ?? tasks.map(t => t.id) const orderedTasks = order.map(id => taskMap[id]).filter(Boolean) 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 (!over || active.id === over.id) return const prevOrder = [...order] const newOrder = arrayMove([...order], order.indexOf(active.id as string), order.indexOf(over.id as string)) reorderTasks(storyId, newOrder) setActiveDragId(null) startTransition(async () => { const result = await reorderTasksAction(storyId, newOrder) if (!result.success) { rollbackTasks(storyId, prevOrder); toast.error('Volgorde opslaan mislukt') } }) } function handleStatusToggle(task: Task) { startTransition(async () => { await updateTaskStatusAction(task.id, STATUS_CYCLE[task.status] ?? 'TO_DO') }) } function openCreateDialog() { 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}
)}
)}
) }