"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 && (
)}
);
}
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 && }
);
}