'use client' import { useState, useTransition, useEffect, useActionState } from 'react' import { useFormStatus } from 'react-dom' 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 { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { useSprintStore } from '@/stores/sprint-store' import { createTaskAction, updateTaskStatusAction, updateTaskAction, deleteTaskAction, reorderTasksAction, } from '@/actions/tasks' import { cn } from '@/lib/utils' const STATUS_CYCLE: Record = { TO_DO: 'IN_PROGRESS', IN_PROGRESS: 'DONE', DONE: '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', } const STATUS_LABELS: Record = { TO_DO: 'To Do', IN_PROGRESS: 'Bezig', DONE: 'Klaar' } const PRIORITY_LABELS: Record = { 1: 'Kritiek', 2: 'Hoog', 3: 'Gemiddeld', 4: 'Laag' } export interface Task { id: 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, isDemo, onStatusToggle, onDelete, }: { task: Task; isDemo: boolean; onStatusToggle: () => void; onDelete: () => void }) { const [editing, setEditing] = useState(false) const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 } const [, formAction] = useActionState( async (_prev: unknown, fd: FormData) => { const result = await updateTaskAction(_prev, fd) if (result?.success) setEditing(false) return result }, undefined ) if (editing) { return (
) } return (
{!isDemo && ( )}

{task.title}

{PRIORITY_LABELS[task.priority]}
{!isDemo && (
)}
) } function EditSubmitButton() { const { pending } = useFormStatus() return } function CreateTaskForm({ storyId, sprintId, onDone }: { storyId: string; sprintId: string; onDone: () => void }) { const [state, formAction] = useActionState( async (_prev: unknown, fd: FormData) => { const result = await createTaskAction(_prev, fd) if (result?.success) { onDone(); return result } if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Aanmaken mislukt') return result }, undefined ) return (
{state && 'error' in state && typeof state.error === 'string' && (

{state.error}

)}
) } function CreateSubmitButton() { const { pending } = useFormStatus() return } export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDemo }: TaskListProps) { const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore() const [creating, setCreating] = useState(false) const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() 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 handleDelete(id: string) { startTransition(async () => { const result = await deleteTaskAction(id) if (result && 'error' in result) toast.error(result.error ?? 'Verwijderen mislukt') }) } return (
{doneCount}/{orderedTasks.length} klaar {!isDemo && ( )} } />
{creating && ( setCreating(false)} /> )} {orderedTasks.length === 0 && !creating ? (

Geen taken voor deze story.

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