"use client";
import { useMemo, useState } from "react";
import {
DndContext,
type DragEndEvent,
DragOverlay,
type DragStartEvent,
PointerSensor,
useDroppable,
useSensor,
useSensors,
closestCorners,
} from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { Loader2, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { deleteTaskAction, moveTaskAction } from "@/lib/appwrite/task-actions";
import { cn } from "@/lib/utils";
import { TaskCard } from "./task-card";
import { TaskFormSheet } from "./task-form-sheet";
import {
COLUMNS,
type Customer,
type TaskRow,
type TaskStatus,
type TeamMember,
} from "./types";
import { useTransition } from "react";
type Props = {
tasks: TaskRow[];
customers: Customer[];
teamMembers: TeamMember[];
};
function Column({
status,
title,
tasks,
onAdd,
onEdit,
onDelete,
}: {
status: TaskStatus;
title: string;
tasks: TaskRow[];
onAdd: (status: TaskStatus) => void;
onEdit: (task: TaskRow) => void;
onDelete: (task: TaskRow) => void;
}) {
const { setNodeRef, isOver } = useDroppableColumn(status);
return (
{title}
{tasks.length}
t.id)}
strategy={verticalListSortingStrategy}
>
{tasks.map((task) => (
))}
{tasks.length === 0 && (
Boş
)}
);
}
function useDroppableColumn(status: TaskStatus) {
return useDroppable({
id: `col-${status}`,
data: { type: "column", status },
});
}
export function TasksBoard({ tasks: initialTasks, customers, teamMembers }: Props) {
const [tasks, setTasks] = useState(initialTasks);
const [activeId, setActiveId] = useState(null);
const [formOpen, setFormOpen] = useState(false);
const [formStatus, setFormStatus] = useState("todo");
const [editing, setEditing] = useState(null);
const [deleting, setDeleting] = useState(null);
const [busy, startTransition] = useTransition();
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 6 },
}),
);
const grouped = useMemo(() => {
const map: Record = {
backlog: [],
todo: [],
in_progress: [],
done: [],
};
for (const t of tasks) {
map[t.status].push(t);
}
return map;
}, [tasks]);
const activeTask = useMemo(
() => tasks.find((t) => t.id === activeId) ?? null,
[tasks, activeId],
);
const onDragStart = (e: DragStartEvent) => {
setActiveId(String(e.active.id));
};
const onDragEnd = (e: DragEndEvent) => {
const { active, over } = e;
setActiveId(null);
if (!over || active.id === over.id) return;
const activeData = active.data.current as { type?: string; status?: TaskStatus } | undefined;
const overData = over.data.current as { type?: string; status?: TaskStatus } | undefined;
if (activeData?.type !== "task") return;
let targetStatus: TaskStatus | undefined;
if (overData?.type === "column" && overData.status) {
targetStatus = overData.status;
} else if (overData?.type === "task" && overData.status) {
targetStatus = overData.status;
}
if (!targetStatus) return;
const sourceTask = tasks.find((t) => t.id === active.id);
if (!sourceTask) return;
// Compute new order: place after the over item, or end of column
const targetTasks = tasks.filter(
(t) => t.status === targetStatus && t.id !== active.id,
);
let newOrder: number;
if (overData?.type === "task") {
const overIndex = targetTasks.findIndex((t) => t.id === over.id);
if (overIndex === -1) {
newOrder = (targetTasks[targetTasks.length - 1]?.order ?? 0) + 1000;
} else {
const before = targetTasks[overIndex - 1]?.order ?? 0;
const at = targetTasks[overIndex].order;
newOrder = (before + at) / 2;
}
} else {
newOrder = (targetTasks[targetTasks.length - 1]?.order ?? 0) + 1000;
}
// Optimistic update
setTasks((prev) =>
prev.map((t) => (t.id === sourceTask.id ? { ...t, status: targetStatus!, order: newOrder } : t)),
);
startTransition(async () => {
const result = await moveTaskAction(sourceTask.id, targetStatus!, newOrder);
if (!result.ok) {
// Rollback
setTasks((prev) =>
prev.map((t) =>
t.id === sourceTask.id ? { ...t, status: sourceTask.status, order: sourceTask.order } : t,
),
);
toast.error(result.error ?? "Görev taşınamadı.");
}
});
};
// Keep local state in sync when server data changes (e.g., after revalidate)
useMemo(() => setTasks(initialTasks), [initialTasks]);
const handleAdd = (status: TaskStatus) => {
setEditing(null);
setFormStatus(status);
setFormOpen(true);
};
const handleDelete = () => {
if (!deleting) return;
startTransition(async () => {
const fd = new FormData();
fd.set("id", deleting.id);
const result = await deleteTaskAction(fd);
if (result.ok) {
toast.success("Görev silindi.");
setDeleting(null);
} else {
toast.error(result.error ?? "Silme başarısız.");
}
});
};
return (
<>
{COLUMNS.map((col) => (
{
setEditing(t);
setFormOpen(true);
}}
onDelete={(t) => setDeleting(t)}
/>
))}
{activeTask && (
{}}
onDelete={() => {}}
isOverlay
/>
)}
{
setFormOpen(v);
if (!v) setEditing(null);
}}
task={editing}
defaultStatus={formStatus}
customers={customers}
teamMembers={teamMembers}
/>
>
);
}