feat(tasks): add task status update functionality with project revalidation

This commit is contained in:
poyrazavsever
2026-06-05 09:49:15 +03:00
parent fb3fe1031c
commit ee63321c43
2 changed files with 172 additions and 10 deletions
@@ -6,7 +6,11 @@ import {
deleteProjectPlanningSectionRecord, deleteProjectPlanningSectionRecord,
updateProjectPlanningSectionRecord, updateProjectPlanningSectionRecord,
} from "@/app/(dashboard)/projects/actions"; } 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 { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
import { import {
Dialog, Dialog,
@@ -29,6 +33,8 @@ import {
ClipboardList, ClipboardList,
FileText, FileText,
FolderKanban, FolderKanban,
KanbanSquare,
LayoutList,
Palette, Palette,
Pencil, Pencil,
Plus, Plus,
@@ -37,7 +43,7 @@ import {
Wallet, Wallet,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState, useTransition, type DragEvent } from "react";
export type ProjectDetail = { export type ProjectDetail = {
id: string; id: string;
@@ -507,6 +513,24 @@ function TaskPanel({
clientId: string | null; clientId: string | null;
tasks: ProjectDetailTaskItem[]; 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 ( return (
<Card> <Card>
<CardContent className="space-y-4 p-5"> <CardContent className="space-y-4 p-5">
@@ -517,10 +541,32 @@ function TaskPanel({
Bu proje ile bağlantılı görevler aynı task modülünden beslenir. Bu proje ile bağlantılı görevler aynı task modülünden beslenir.
</p> </p>
</div> </div>
<ProjectTaskDialog projectId={projectId} clientId={clientId} /> <div className="flex flex-col gap-2 sm:flex-row">
<div className="flex rounded-sm border border-border p-1">
<Button
type="button"
variant={view === "list" ? "default" : "ghost"}
className="h-8 gap-2 px-3"
onClick={() => setView("list")}
>
<LayoutList className="h-4 w-4" />
Liste
</Button>
<Button
type="button"
variant={view === "kanban" ? "default" : "ghost"}
className="h-8 gap-2 px-3"
onClick={() => setView("kanban")}
>
<KanbanSquare className="h-4 w-4" />
Kanban
</Button>
</div>
<ProjectTaskDialog projectId={projectId} clientId={clientId} />
</div>
</div> </div>
{tasks.length > 0 ? ( {localTasks.length > 0 && view === "list" ? (
<div className="overflow-hidden rounded-sm border border-border"> <div className="overflow-hidden rounded-sm border border-border">
<div className="hidden grid-cols-[1.5fr_0.8fr_0.8fr_0.8fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid"> <div className="hidden grid-cols-[1.5fr_0.8fr_0.8fr_0.8fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid">
<span>Görev</span> <span>Görev</span>
@@ -529,7 +575,7 @@ function TaskPanel({
<span className="text-right">İşlem</span> <span className="text-right">İşlem</span>
</div> </div>
<div className="divide-y divide-border"> <div className="divide-y divide-border">
{tasks.map((task) => ( {localTasks.map((task) => (
<div <div
key={task.id} key={task.id}
className="grid gap-4 px-4 py-4 lg:grid-cols-[1.5fr_0.8fr_0.8fr_0.8fr] lg:items-center" className="grid gap-4 px-4 py-4 lg:grid-cols-[1.5fr_0.8fr_0.8fr_0.8fr] lg:items-center"
@@ -574,14 +620,120 @@ function TaskPanel({
))} ))}
</div> </div>
</div> </div>
) : ( ) : null}
{localTasks.length > 0 && view === "kanban" ? (
<ProjectTaskKanban
projectId={projectId}
tasks={localTasks}
onTaskStatusChange={handleTaskStatusChange}
/>
) : null}
{localTasks.length === 0 ? (
<EmptyPanel icon={ClipboardList} title="Bu projeye bağlı görev yok" /> <EmptyPanel icon={ClipboardList} title="Bu projeye bağlı görev yok" />
)} ) : null}
</CardContent> </CardContent>
</Card> </Card>
); );
} }
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<string | null>(null);
function handleDragStart(event: DragEvent<HTMLDivElement>, 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 (
<div className="tiny-scrollbar grid gap-4 overflow-x-auto pb-2 lg:grid-cols-3">
{columns.map((status) => {
const columnTasks = tasks.filter((task) => task.status === status);
return (
<div
key={status}
className="min-w-72 rounded-sm border border-border bg-muted/20 p-3 transition-colors"
onDragOver={(event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}}
onDrop={() => handleDrop(status)}
>
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
{getTaskStatusLabel(status)}
</h3>
<Badge>{columnTasks.length}</Badge>
</div>
<div className="space-y-3">
{columnTasks.map((task) => (
<Card
key={task.id}
draggable
className={
draggedTaskId === task.id
? "cursor-grabbing opacity-50 ring-2 ring-primary/20 transition"
: "cursor-grab transition hover:border-primary/30 active:cursor-grabbing"
}
onDragStart={(event) => handleDragStart(event, task.id)}
onDragEnd={() => setDraggedTaskId(null)}
>
<CardContent className="space-y-3 p-3">
<div>
<div className="font-medium text-foreground">{task.title}</div>
<div className="text-sm text-muted-foreground">
{task.due_at ? formatDateTime(task.due_at) : "Son tarih yok"}
</div>
</div>
<div className="flex items-center justify-between gap-2">
<Badge className={priorityClasses[task.priority]}>{task.priority}</Badge>
{task.status !== "done" ? (
<form action={completeTaskRecord}>
<input type="hidden" name="id" value={task.id} />
<input type="hidden" name="project_id" value={projectId} />
<Button
type="submit"
variant="outline"
className="h-8 w-8 p-0"
title="Tamamla"
aria-label="Tamamla"
>
<CheckCircle2 className="h-4 w-4" />
</Button>
</form>
) : null}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
})}
</div>
);
}
function ProjectTaskDialog({ function ProjectTaskDialog({
projectId, projectId,
clientId, clientId,
@@ -611,7 +763,7 @@ function ProjectTaskDialog({
Görev ekle Görev ekle
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-2xl">
<form action={handleSubmit} className="space-y-5"> <form action={handleSubmit} className="space-y-5">
<input type="hidden" name="project_id" value={projectId} /> <input type="hidden" name="project_id" value={projectId} />
{clientId ? <input type="hidden" name="client_id" value={clientId} /> : null} {clientId ? <input type="hidden" name="client_id" value={clientId} /> : null}
@@ -670,7 +822,7 @@ function ProjectTaskDialog({
</Select> </Select>
</div> </div>
</div> </div>
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-5 md:grid-cols-3">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="project-task-due">Son tarih</Label> <Label htmlFor="project-task-due">Son tarih</Label>
<Input id="project-task-due" name="due_at" type="datetime-local" /> <Input id="project-task-due" name="due_at" type="datetime-local" />
@@ -853,6 +1005,12 @@ function formatDateTime(value: string) {
}).format(new Date(value)); }).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) { function formatCurrency(value: number, currency: string) {
return new Intl.NumberFormat("tr-TR", { return new Intl.NumberFormat("tr-TR", {
style: "currency", style: "currency",
+5 -1
View File
@@ -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 { supabase, userId } = await getCurrentUserId();
const nextStatus = readStatus(status); const nextStatus = readStatus(status);
@@ -159,6 +159,10 @@ export async function updateTaskStatusRecord(taskId: string, status: string) {
} }
revalidatePath("/tasks"); revalidatePath("/tasks");
if (projectId) {
revalidatePath(`/projects/${projectId}`);
}
} }
export async function deleteTaskRecord(formData: FormData) { export async function deleteTaskRecord(formData: FormData) {