feat(tasks): add task status update functionality with project revalidation
This commit is contained in:
@@ -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>
|
||||||
|
<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} />
|
<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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user