"use client"; import { useState, useCallback } from "react"; import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, type DragStartEvent, type DragEndEvent, } from "@dnd-kit/core"; import { useDroppable } from "@dnd-kit/core"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/ui/use-toast"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { KanbanCard } from "./KanbanCard"; import { TASK_STATUS_CONFIG } from "./TaskCard"; import { updateTaskStatus } from "@/actions/tasks"; import { TaskStatus, TaskType } from "@prisma/client"; import { cn } from "@/lib/utils"; const COLUMN_ORDER: TaskStatus[] = [ "TODO", "IN_PROGRESS", "INTERNAL_REVIEW", "CLIENT_REVIEW", "CHANGES", "DONE", ]; interface KanbanTask { id: string; title: string; type: TaskType; status: TaskStatus; priority: string; dueDate: Date | null; shot?: { shotCode: string } | null; asset?: { assetCode: string; name: string } | null; assignedArtist?: { id: string; name: string | null; email: string; image: string | null; } | null; _count?: { versions: number }; versions?: { id: string; versionNumber: number; approvalStatus: string; createdAt: Date; }[]; } function KanbanColumn({ status, tasks, }: { status: TaskStatus; tasks: KanbanTask[]; }) { const { setNodeRef, isOver } = useDroppable({ id: status }); const cfg = TASK_STATUS_CONFIG[status]; const Icon = cfg.icon; return (
{/* Column header */}
{cfg.label} {tasks.length}
{/* Drop zone */}
{tasks.map((task) => ( ))} {tasks.length === 0 && (

Drop here

)}
); } interface KanbanBoardProps { tasks: KanbanTask[]; projectId: string; artists?: { id: string; name: string | null; email: string }[]; } export function KanbanBoard({ tasks: initialTasks, projectId, artists = [] }: KanbanBoardProps) { const [tasks, setTasks] = useState(initialTasks); const [draggingTask, setDraggingTask] = useState(null); const [filterArtist, setFilterArtist] = useState("__all__"); const { toast } = useToast(); const router = useRouter(); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }) ); const filteredTasks = filterArtist === "__all__" ? tasks : tasks.filter((t) => t.assignedArtist?.id === filterArtist); const columns = COLUMN_ORDER.map((status) => ({ status, tasks: filteredTasks.filter((t) => t.status === status), })); const handleDragStart = useCallback( (event: DragStartEvent) => { const task = tasks.find((t) => t.id === event.active.id); if (task) setDraggingTask(task); }, [tasks] ); const handleDragEnd = useCallback( async (event: DragEndEvent) => { setDraggingTask(null); const { active, over } = event; if (!over || active.id === over.id) return; const taskId = active.id as string; const newStatus = over.id as TaskStatus; // Validate target is a column if (!COLUMN_ORDER.includes(newStatus)) return; const task = tasks.find((t) => t.id === taskId); if (!task || task.status === newStatus) return; // Optimistic update setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t)) ); try { await updateTaskStatus(taskId, newStatus); router.refresh(); } catch { // Revert on failure setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: task.status } : t)) ); toast({ title: "Failed to update task status", variant: "destructive" }); } }, [tasks, router, toast] ); return ( {/* Artist filter */} {artists.length > 0 && (
Filter by artist: {filterArtist !== "__all__" && ( )}
)}
{columns.map(({ status, tasks: colTasks }) => ( ))}
{draggingTask && }
); }