From f41815329f349f696b8bcbc051a5fc2e2d8f73f3 Mon Sep 17 00:00:00 2001 From: Poyraz Avsever Date: Thu, 4 Jun 2026 15:51:13 +0300 Subject: [PATCH] feat(tasks): implement task management actions and UI components - Added actions for creating, updating, completing, and deleting tasks in `actions.ts`. - Refactored `TasksPage` to fetch tasks from Supabase and display them using the new `TasksClient` component. - Created `tasks-client.tsx` to handle task listing and Kanban view with filtering capabilities. - Introduced a dialog for task creation and editing with form validation. - Enhanced task display with status and priority badges, and added overdue checks. --- app/(dashboard)/tasks/actions.ts | 144 +++++++ app/(dashboard)/tasks/page.tsx | 355 ++++------------ app/(dashboard)/tasks/tasks-client.tsx | 567 +++++++++++++++++++++++++ 3 files changed, 795 insertions(+), 271 deletions(-) create mode 100644 app/(dashboard)/tasks/actions.ts create mode 100644 app/(dashboard)/tasks/tasks-client.tsx diff --git a/app/(dashboard)/tasks/actions.ts b/app/(dashboard)/tasks/actions.ts new file mode 100644 index 0000000..fe40329 --- /dev/null +++ b/app/(dashboard)/tasks/actions.ts @@ -0,0 +1,144 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; +import { revalidatePath } from "next/cache"; + +const TASK_STATUSES = ["todo", "in_progress", "done"] as const; +const TASK_PRIORITIES = ["low", "medium", "high", "urgent"] as const; + +function cleanText(value: FormDataEntryValue | null) { + const text = typeof value === "string" ? value.trim() : ""; + return text.length > 0 ? text : null; +} + +function readStatus(value: FormDataEntryValue | null) { + const status = typeof value === "string" ? value : "todo"; + return TASK_STATUSES.includes(status as (typeof TASK_STATUSES)[number]) + ? status + : "todo"; +} + +function readPriority(value: FormDataEntryValue | null) { + const priority = typeof value === "string" ? value : "medium"; + return TASK_PRIORITIES.includes(priority as (typeof TASK_PRIORITIES)[number]) + ? priority + : "medium"; +} + +function readMinutes(value: FormDataEntryValue | null) { + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? Math.round(number) : null; +} + +async function getCurrentUserId() { + const supabase = await createClient(); + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (error || !user) { + throw new Error("Görev işlemi için giriş yapmış kullanıcı bulunamadı."); + } + + return { supabase, userId: user.id }; +} + +function readPayload(formData: FormData) { + return { + title: cleanText(formData.get("title")), + description: cleanText(formData.get("description")), + status: readStatus(formData.get("status")), + priority: readPriority(formData.get("priority")), + client_id: cleanText(formData.get("client_id")), + project_id: cleanText(formData.get("project_id")), + due_at: cleanText(formData.get("due_at")), + estimated_minutes: readMinutes(formData.get("estimated_minutes")), + actual_minutes: readMinutes(formData.get("actual_minutes")), + }; +} + +export async function createTaskRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const payload = readPayload(formData); + + if (!payload.title) { + throw new Error("Görev başlığı zorunludur."); + } + + const { error } = await supabase.from("tasks").insert({ + user_id: userId, + date: payload.due_at || new Date().toISOString(), + ...payload, + }); + + if (error) { + throw new Error(`Görev eklenemedi: ${error.message}`); + } + + revalidatePath("/tasks"); +} + +export async function updateTaskRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const id = cleanText(formData.get("id")); + const payload = readPayload(formData); + + if (!id || !payload.title) { + throw new Error("Görev güncellemek için başlık ve kayıt kimliği zorunludur."); + } + + const { error } = await supabase + .from("tasks") + .update(payload) + .eq("id", id) + .eq("user_id", userId); + + if (error) { + throw new Error(`Görev güncellenemedi: ${error.message}`); + } + + revalidatePath("/tasks"); +} + +export async function completeTaskRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const id = cleanText(formData.get("id")); + + if (!id) { + throw new Error("Tamamlanacak görev bulunamadı."); + } + + const { error } = await supabase + .from("tasks") + .update({ status: "done" }) + .eq("id", id) + .eq("user_id", userId); + + if (error) { + throw new Error(`Görev tamamlanamadı: ${error.message}`); + } + + revalidatePath("/tasks"); +} + +export async function deleteTaskRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const id = cleanText(formData.get("id")); + + if (!id) { + throw new Error("Silinecek görev bulunamadı."); + } + + const { error } = await supabase + .from("tasks") + .delete() + .eq("id", id) + .eq("user_id", userId); + + if (error) { + throw new Error(`Görev silinemedi: ${error.message}`); + } + + revalidatePath("/tasks"); +} diff --git a/app/(dashboard)/tasks/page.tsx b/app/(dashboard)/tasks/page.tsx index cfbb1a6..fe4c70d 100644 --- a/app/(dashboard)/tasks/page.tsx +++ b/app/(dashboard)/tasks/page.tsx @@ -1,280 +1,93 @@ -"use client"; +import { + TasksClient, + type TaskListItem, + type TaskRelationOption, +} from "@/app/(dashboard)/tasks/tasks-client"; +import { createClient } from "@/lib/supabase/server"; -import { useState } from "react"; -import { - Plus, Search, Filter, Brain, CheckSquare2, Clock, AlertCircle, - MoreHorizontal, MessageSquare, ArrowUpRight, CheckCircle2, - GripVertical, List, Layout, X, Calendar, User, Tag -} from "lucide-react"; -import { motion, AnimatePresence, Reorder } from "framer-motion"; +type TaskRow = { + id: string; + title: string; + description: string | null; + status: "todo" | "in_progress" | "done"; + priority: "low" | "medium" | "high" | "urgent"; + due_at: string | null; + estimated_minutes: number | null; + actual_minutes: number | null; + client_id: string | null; + project_id: string | null; + created_at: string; + clients: { name: string } | { name: string }[] | null; + projects: { name: string } | { name: string }[] | null; +}; -// Mock Data -const initialTasks = [ - { id: "1", title: "Finalize Q3 Marketing Assets", project: "Marketing Site", priority: "High", status: "In Progress", assignee: "Sarah J.", aiPredict: "2 days", dueDate: "May 15" }, - { id: "2", title: "Database Migration Script", project: "Infrastructure", priority: "Critical", status: "Review", assignee: "Alex R.", aiPredict: "4 hours", dueDate: "May 12" }, - { id: "3", title: "Design System Tokens", project: "Cognis Mobile", priority: "Medium", status: "To Do", assignee: "Mike C.", aiPredict: "3 days", dueDate: "May 20" }, - { id: "4", title: "Client Interview Synthesis", project: "Research", priority: "Low", status: "Done", assignee: "Emma W.", aiPredict: "Completed", dueDate: "May 08" }, - { id: "5", title: "Optimize Webpack Config", project: "Infrastructure", priority: "Medium", status: "To Do", assignee: "Alex R.", aiPredict: "1 day", dueDate: "May 18" } -]; +export default async function TasksPage() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); -const columns = ["To Do", "In Progress", "Review", "Done"]; + if (!user) { + return null; + } -export default function TasksPage() { - const [viewMode, setViewMode] = useState<"list" | "kanban">("list"); - const [tasks, setTasks] = useState(initialTasks); - const [isSorting, setIsSorting] = useState(false); - const [selectedTask, setSelectedTask] = useState(null); + const [{ data: taskRows }, { data: clientRows }, { data: projectRows }] = + await Promise.all([ + supabase + .from("tasks") + .select( + "id, title, description, status, priority, due_at, estimated_minutes, actual_minutes, client_id, project_id, created_at, clients(name), projects(name)", + ) + .eq("user_id", user.id) + .order("created_at", { ascending: false }), + supabase + .from("clients") + .select("id, name") + .eq("user_id", user.id) + .neq("status", "archived") + .order("name", { ascending: true }), + supabase + .from("projects") + .select("id, name, client_id") + .eq("user_id", user.id) + .neq("status", "cancelled") + .order("name", { ascending: true }), + ]); - const handleAutoSort = () => { - setIsSorting(true); - setTimeout(() => { - const sorted = [...tasks].sort((a, b) => { - const priorityOrder: any = { Critical: 0, High: 1, Medium: 2, Low: 3 }; - return priorityOrder[a.priority] - priorityOrder[b.priority]; - }); - setTasks(sorted); - setIsSorting(false); - }, 1500); - }; + const clients = (clientRows || []) as TaskRelationOption[]; + const projects = (projectRows || []) as TaskRelationOption[]; + const tasks: TaskListItem[] = ((taskRows || []) as unknown as TaskRow[]).map((task) => ({ + id: task.id, + title: task.title, + description: task.description, + status: normalizeStatus(task.status), + priority: normalizePriority(task.priority), + due_at: task.due_at, + estimated_minutes: task.estimated_minutes, + actual_minutes: task.actual_minutes, + client_id: task.client_id, + clientName: getRelationName(task.clients), + project_id: task.project_id, + projectName: getRelationName(task.projects), + created_at: task.created_at, + })); - return ( -
- - {/* Top Header */} -
-

- Management / Tasks -

-
-
- - -
- -
-
- - {/* AI Task Prioritization Card */} -
-
- -
-
-
- -
-
-

- AI Priority Engine -

-

- Based on deadlines and team velocity, I've identified 3 tasks that need immediate focus. Auto-sort will reorder your backlog for maximum strategic impact. -

-
-
- -
- - {/* View Toggle */} -
-
- - -
-
- {tasks.length} Active Tasks -
-
- -
- {viewMode === "list" ? ( -
-
-
Task Details
-
Status
-
Assignee
-
AI Time Est.
-
Actions
-
- -
- - {tasks.map((task) => ( - setSelectedTask(task)} - className="grid grid-cols-12 gap-4 p-4 items-center hover:bg-white/[0.02] transition-colors group cursor-pointer" - > -
- -
-
- {task.title} -
-
- {task.project} - - {task.priority} - -
-
-
-
- - {task.status} - -
-
-
- {task.assignee.split(' ').map(n => n[0]).join('')} -
- {task.assignee} -
-
- - {task.aiPredict} -
-
- -
-
- ))} -
-
-
- ) : ( -
- {columns.map((col) => ( -
-
-

{col}

- {tasks.filter(t => t.status === col).length} -
-
- {tasks.filter(t => t.status === col).map(task => ( - setSelectedTask(task)} - className="bg-[#0A0710] border border-white/5 rounded-sm p-4 hover:border-primary/30 transition-all cursor-pointer group shadow-xl" - > -
{task.title}
-
- - {task.priority} - -
- {task.assignee.split(' ').map(n => n[0]).join('')} -
-
-
- ))} - -
-
- ))} -
- )} -
- - {/* Task Detail Sheet */} - - {selectedTask && ( - <> - setSelectedTask(null)} className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]" /> - -
-
-
- {selectedTask.project} - ID: #TSK-{selectedTask.id} -
-

{selectedTask.title}

-
- -
- -
-
- - - - -
- -
-

Description

-

- This task is part of the strategic {selectedTask.project} roadmap. Please ensure all design tokens are verified against the core Cognis design system before final review. -

-
- -
-

Subtasks

-
- {[1, 2, 3].map(i => ( -
-
- -
- Verification step {i} for the migration script. -
- ))} -
-
-
- -
- - -
-
- - )} -
- -
- ); + return ; } -function DetailItem({ label, icon: Icon, value, color = "text-muted-foreground" }: any) { - return ( -
-
{label}
-
- - {value} -
-
- ); +function getRelationName(relation: TaskRow["clients"] | TaskRow["projects"]) { + if (!relation) return null; + return Array.isArray(relation) ? relation[0]?.name || null : relation.name; +} + +function normalizeStatus(status: string): TaskListItem["status"] { + return status === "in_progress" || status === "done" ? status : "todo"; +} + +function normalizePriority(priority: string): TaskListItem["priority"] { + if (priority === "low" || priority === "high" || priority === "urgent") { + return priority; + } + + return "medium"; } diff --git a/app/(dashboard)/tasks/tasks-client.tsx b/app/(dashboard)/tasks/tasks-client.tsx new file mode 100644 index 0000000..2a4801b --- /dev/null +++ b/app/(dashboard)/tasks/tasks-client.tsx @@ -0,0 +1,567 @@ +"use client"; + +import { + completeTaskRecord, + createTaskRecord, + deleteTaskRecord, + updateTaskRecord, +} from "@/app/(dashboard)/tasks/actions"; +import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "poyraz-ui/molecules"; +import { + CalendarDays, + CheckCircle2, + KanbanSquare, + LayoutList, + Pencil, + Plus, + Trash2, +} from "lucide-react"; +import { useState } from "react"; + +export type TaskRelationOption = { + id: string; + name: string; + client_id?: string | null; +}; + +export type TaskListItem = { + id: string; + title: string; + description: string | null; + status: "todo" | "in_progress" | "done"; + priority: "low" | "medium" | "high" | "urgent"; + due_at: string | null; + estimated_minutes: number | null; + actual_minutes: number | null; + client_id: string | null; + clientName: string | null; + project_id: string | null; + projectName: string | null; + created_at: string; +}; + +const statusLabels = { + todo: "Yapılacak", + in_progress: "Devam ediyor", + done: "Tamamlandı", +}; + +const priorityLabels = { + low: "Düşük", + medium: "Orta", + high: "Yüksek", + urgent: "Acil", +}; + +const priorityClasses = { + low: "border-zinc-200 bg-zinc-50 text-zinc-700", + medium: "border-blue-200 bg-blue-50 text-blue-700", + high: "border-amber-200 bg-amber-50 text-amber-700", + urgent: "border-rose-200 bg-rose-50 text-rose-700", +}; + +type TasksClientProps = { + tasks: TaskListItem[]; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}; + +export function TasksClient({ tasks, clients, projects }: TasksClientProps) { + const [query, setQuery] = useState(""); + const [view, setView] = useState<"list" | "kanban">("list"); + const normalizedQuery = query.trim().toLowerCase(); + const filteredTasks = normalizedQuery + ? tasks.filter((task) => + [task.title, task.description, task.clientName, task.projectName] + .filter(Boolean) + .some((value) => value!.toLowerCase().includes(normalizedQuery)), + ) + : tasks; + + const doneCount = tasks.filter((task) => task.status === "done").length; + const overdueCount = tasks.filter((task) => isOverdue(task)).length; + const urgentCount = tasks.filter((task) => task.priority === "urgent").length; + + return ( +
+
+
+
+ + Günlük operasyon +
+
+

+ Görevler +

+

+ Proje ve müşteri bağlantılı işleri liste veya basit kanban ile takip et. +

+
+
+ + +
+ +
+ + + + +
+ + + +
+
+

Görev listesi

+

+ {filteredTasks.length} kayıt görüntüleniyor. +

+
+
+ setQuery(event.target.value)} + placeholder="Görev, proje veya müşteri ara" + className="sm:w-80" + /> +
+ + +
+
+
+ + {filteredTasks.length > 0 ? ( + view === "list" ? ( + + ) : ( + + ) + ) : ( + + )} +
+
+
+ ); +} + +function TaskList({ + tasks, + clients, + projects, +}: { + tasks: TaskListItem[]; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}) { + return ( +
+
+ Görev + Bağlantı + Öncelik + Son tarih + İşlem +
+
+ {tasks.map((task) => ( + + ))} +
+
+ ); +} + +function TaskRow({ + task, + clients, + projects, +}: { + task: TaskListItem; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}) { + return ( +
+
+
+ {task.title} +
+
+ {statusLabels[task.status]} +
+
+
+
{task.projectName || "Proje yok"}
+
{task.clientName || "Müşteri yok"}
+
+
+ + {priorityLabels[task.priority]} + +
+
+ {task.due_at ? formatDateTime(task.due_at) : "Yok"} +
+ +
+ ); +} + +function TaskKanban({ + tasks, + clients, + projects, +}: { + tasks: TaskListItem[]; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}) { + const columns = ["todo", "in_progress", "done"] as const; + + return ( +
+ {columns.map((status) => { + const columnTasks = tasks.filter((task) => task.status === status); + + return ( +
+
+

{statusLabels[status]}

+ {columnTasks.length} +
+
+ {columnTasks.map((task) => ( + + +
+
{task.title}
+
+ {task.projectName || task.clientName || "Bağlantı yok"} +
+
+
+ + {priorityLabels[task.priority]} + + +
+
+
+ ))} +
+
+ ); + })} +
+ ); +} + +function TaskActions({ + task, + clients, + projects, + compact = false, +}: { + task: TaskListItem; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; + compact?: boolean; +}) { + return ( +
+ + {task.status !== "done" ? ( +
+ + +
+ ) : null} +
+ + +
+
+ ); +} + +function TaskDialog({ + mode, + task, + clients, + projects, +}: { + mode: "create" | "edit"; + task?: TaskListItem; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}) { + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const action = mode === "create" ? createTaskRecord : updateTaskRecord; + + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + + try { + await action(formData); + setOpen(false); + } finally { + setIsSubmitting(false); + } + } + + return ( + + + + + +
+ {task ? : null} + + {mode === "create" ? "Yeni görev" : "Görevi düzenle"} + + Görevi proje, müşteri, öncelik ve son tarih bilgileriyle kaydet. + + + +
+ +
+ + + + +
+
+
+ ); +} + +function TaskFormFields({ + task, + clients, + projects, +}: { + task?: TaskListItem; + clients: TaskRelationOption[]; + projects: TaskRelationOption[]; +}) { + return ( +
+
+ + +
+ +
+ +