diff --git a/app/(dashboard)/projects/[id]/project-detail-client.tsx b/app/(dashboard)/projects/[id]/project-detail-client.tsx index 0253def..bb4af79 100644 --- a/app/(dashboard)/projects/[id]/project-detail-client.tsx +++ b/app/(dashboard)/projects/[id]/project-detail-client.tsx @@ -6,7 +6,11 @@ import { deleteProjectPlanningSectionRecord, updateProjectPlanningSectionRecord, } from "@/app/(dashboard)/projects/actions"; -import { completeTaskRecord, createTaskRecord } from "@/app/(dashboard)/tasks/actions"; +import { + completeTaskRecord, + createTaskRecord, + updateTaskStatusRecord, +} from "@/app/(dashboard)/tasks/actions"; import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms"; import { Dialog, @@ -29,6 +33,8 @@ import { ClipboardList, FileText, FolderKanban, + KanbanSquare, + LayoutList, Palette, Pencil, Plus, @@ -37,7 +43,7 @@ import { Wallet, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useState, useTransition, type DragEvent } from "react"; export type ProjectDetail = { id: string; @@ -507,6 +513,24 @@ function TaskPanel({ clientId: string | null; tasks: ProjectDetailTaskItem[]; }) { + const [view, setView] = useState<"list" | "kanban">("list"); + const [localTasks, setLocalTasks] = useState(tasks); + const [, startTransition] = useTransition(); + + function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) { + const previousTasks = localTasks; + + setLocalTasks((currentTasks) => + currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)), + ); + + startTransition(() => { + void updateTaskStatusRecord(taskId, status, projectId).catch(() => { + setLocalTasks(previousTasks); + }); + }); + } + return ( @@ -517,10 +541,32 @@ function TaskPanel({ Bu proje ile bağlantılı görevler aynı task modülünden beslenir.

- +
+
+ + +
+ +
- {tasks.length > 0 ? ( + {localTasks.length > 0 && view === "list" ? (
Görev @@ -529,7 +575,7 @@ function TaskPanel({ İşlem
- {tasks.map((task) => ( + {localTasks.map((task) => (
- ) : ( + ) : null} + + {localTasks.length > 0 && view === "kanban" ? ( + + ) : null} + + {localTasks.length === 0 ? ( - )} + ) : null} ); } +function ProjectTaskKanban({ + projectId, + tasks, + onTaskStatusChange, +}: { + projectId: string; + tasks: ProjectDetailTaskItem[]; + onTaskStatusChange: (taskId: string, status: ProjectDetailTaskItem["status"]) => void; +}) { + const columns = ["todo", "in_progress", "done"] as const; + const [draggedTaskId, setDraggedTaskId] = useState(null); + + function handleDragStart(event: DragEvent, taskId: string) { + setDraggedTaskId(taskId); + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", taskId); + event.dataTransfer.setDragImage(event.currentTarget, 24, 24); + } + + function handleDrop(status: ProjectDetailTaskItem["status"]) { + if (!draggedTaskId) return; + + onTaskStatusChange(draggedTaskId, status); + setDraggedTaskId(null); + } + + return ( +
+ {columns.map((status) => { + const columnTasks = tasks.filter((task) => task.status === status); + + return ( +
{ + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }} + onDrop={() => handleDrop(status)} + > +
+

+ {getTaskStatusLabel(status)} +

+ {columnTasks.length} +
+
+ {columnTasks.map((task) => ( + handleDragStart(event, task.id)} + onDragEnd={() => setDraggedTaskId(null)} + > + +
+
{task.title}
+
+ {task.due_at ? formatDateTime(task.due_at) : "Son tarih yok"} +
+
+
+ {task.priority} + {task.status !== "done" ? ( +
+ + + +
+ ) : null} +
+
+
+ ))} +
+
+ ); + })} +
+ ); +} + function ProjectTaskDialog({ projectId, clientId, @@ -611,7 +763,7 @@ function ProjectTaskDialog({ Görev ekle - +
{clientId ? : null} @@ -670,7 +822,7 @@ function ProjectTaskDialog({
-
+
@@ -853,6 +1005,12 @@ function formatDateTime(value: string) { }).format(new Date(value)); } +function getTaskStatusLabel(status: ProjectDetailTaskItem["status"]) { + if (status === "done") return "Tamamlandı"; + if (status === "in_progress") return "Devam ediyor"; + return "Yapılacak"; +} + function formatCurrency(value: number, currency: string) { return new Intl.NumberFormat("tr-TR", { style: "currency", diff --git a/app/(dashboard)/tasks/actions.ts b/app/(dashboard)/tasks/actions.ts index b355d7d..6053c7f 100644 --- a/app/(dashboard)/tasks/actions.ts +++ b/app/(dashboard)/tasks/actions.ts @@ -140,7 +140,7 @@ export async function completeTaskRecord(formData: FormData) { } } -export async function updateTaskStatusRecord(taskId: string, status: string) { +export async function updateTaskStatusRecord(taskId: string, status: string, projectId?: string) { const { supabase, userId } = await getCurrentUserId(); const nextStatus = readStatus(status); @@ -159,6 +159,10 @@ export async function updateTaskStatusRecord(taskId: string, status: string) { } revalidatePath("/tasks"); + + if (projectId) { + revalidatePath(`/projects/${projectId}`); + } } export async function deleteTaskRecord(formData: FormData) {