feat: implement pending link and submit button components for improved UX
- Added PendingLink component to provide loading feedback on navigation links. - Introduced PendingSubmitButton component for forms to indicate submission status. - Updated dashboard, projects, tasks, and clients pages to utilize new components for better user experience during navigation and form submissions. - Enhanced task and project actions with optimistic updates and loading states. - Improved overall performance and user feedback during data fetching and action processing.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
updateClientRecord,
|
||||
updateClientPipelineStage,
|
||||
} from "@/app/(dashboard)/clients/actions";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -325,9 +326,9 @@ function DraggableClientCard({
|
||||
<Card className="hover:border-primary/50 transition-colors">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<Link href={`/clients/${client.id}`} className="font-medium text-foreground hover:underline" onPointerDown={(e) => e.stopPropagation()}>
|
||||
<PendingLink href={`/clients/${client.id}`} className="font-medium text-foreground hover:underline inline-flex items-center gap-1.5" onPointerDown={(e) => e.stopPropagation()} showSpinner>
|
||||
{client.name}
|
||||
</Link>
|
||||
</PendingLink>
|
||||
<div onPointerDown={(e) => e.stopPropagation()}>
|
||||
<ClientDialog mode="edit" client={client} trigger={<Button variant="ghost" className="h-6 w-6 p-0"><Pencil className="h-3 w-3" /></Button>} />
|
||||
</div>
|
||||
@@ -360,7 +361,7 @@ function ClientRow({ client }: { client: ClientListItem }) {
|
||||
{getInitials(client.name)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<Link href={`/clients/${client.id}`} className="truncate font-medium text-foreground hover:underline block">{client.name}</Link>
|
||||
<PendingLink href={`/clients/${client.id}`} className="truncate font-medium text-foreground hover:underline flex items-center gap-1.5" showSpinner>{client.name}</PendingLink>
|
||||
<div className="truncate text-sm text-muted-foreground">
|
||||
{client.company_name || "Firma bilgisi yok"}
|
||||
</div>
|
||||
@@ -409,11 +410,11 @@ function ClientRow({ client }: { client: ClientListItem }) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link href={`/clients/${client.id}`}>
|
||||
<PendingLink href={`/clients/${client.id}`} className="inline-flex" showSpinner>
|
||||
<Button variant="ghost" className="h-9 w-9 p-0">
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</PendingLink>
|
||||
<ClientDialog mode="edit" client={client} trigger={<Button variant="outline" className="h-9 min-w-20 gap-2 px-3"><Pencil className="h-4 w-4" /> Düzenle</Button>} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { Badge, Card, CardContent } from "poyraz-ui/atoms";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, Line, LineChart } from "recharts";
|
||||
@@ -269,7 +269,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{(data.projects || []).slice(0, 5).map((project) => (
|
||||
<Link key={project.id} href={`/projects/${project.id}`} className="flex items-center gap-3 hover:bg-muted/50 p-2 -mx-2 rounded-md transition-colors">
|
||||
<PendingLink key={project.id} href={`/projects/${project.id}`} className="flex items-center gap-3 hover:bg-muted/50 p-2 -mx-2 rounded-md transition-colors" showSpinner>
|
||||
<div className={`h-2 w-2 rounded-full ${project.status === 'completed' ? 'bg-emerald-500' : project.status === 'active' ? 'bg-blue-500' : 'bg-amber-500'}`} />
|
||||
<div className="flex-1 flex justify-between items-center">
|
||||
<div>
|
||||
@@ -282,7 +282,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
||||
{project.status === 'completed' ? 'Tamamlandı' : project.status === 'active' ? 'Aktif' : 'Beklemede'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
</PendingLink>
|
||||
))}
|
||||
{data.projects.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz proje yok.</div>
|
||||
@@ -300,7 +300,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{(data.clients || []).length > 0 ? (data.clients || []).map((client) => (
|
||||
<Link key={client.id} href={`/clients/${client.id}`} className="flex items-center gap-3 hover:bg-muted/50 p-2 -mx-2 rounded-md transition-colors">
|
||||
<PendingLink key={client.id} href={`/clients/${client.id}`} className="flex items-center gap-3 hover:bg-muted/50 p-2 -mx-2 rounded-md transition-colors" showSpinner>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary font-semibold text-xs">
|
||||
{client.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
@@ -311,7 +311,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(client.created_at).toLocaleDateString("tr-TR", { month: "short", day: "numeric" })}
|
||||
</div>
|
||||
</Link>
|
||||
</PendingLink>
|
||||
)) : (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz müşteri yok.</div>
|
||||
)}
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
updateProjectPlanningSectionRecord,
|
||||
} from "@/app/(dashboard)/projects/actions";
|
||||
import {
|
||||
completeTaskRecord,
|
||||
createTaskRecord,
|
||||
updateTaskStatusRecord,
|
||||
} from "@/app/(dashboard)/tasks/actions";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { PendingSubmitButton } from "@/components/ui/pending-submit-button";
|
||||
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
toast,
|
||||
} from "poyraz-ui/molecules";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
FolderKanban,
|
||||
KanbanSquare,
|
||||
LayoutList,
|
||||
Loader2,
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -43,8 +46,7 @@ import {
|
||||
Trash2,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useTransition, type DragEvent } from "react";
|
||||
import { useEffect, useState, useTransition, type DragEvent } from "react";
|
||||
|
||||
export type ProjectDetail = {
|
||||
id: string;
|
||||
@@ -197,10 +199,10 @@ export function ProjectDetailClient({
|
||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="space-y-3">
|
||||
<Button asChild variant="ghost" className="h-8 gap-2 px-0 text-muted-foreground">
|
||||
<Link href="/projects">
|
||||
<PendingLink href="/projects" className="flex items-center gap-2" showSpinner>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Projelere dön
|
||||
</Link>
|
||||
</PendingLink>
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -223,10 +225,14 @@ export function ProjectDetailClient({
|
||||
{project.status !== "completed" ? (
|
||||
<form action={completeProjectRecord}>
|
||||
<input type="hidden" name="id" value={project.id} />
|
||||
<Button type="submit" variant="outline" className="gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<PendingSubmitButton
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
idleIcon={<CheckCircle2 className="h-4 w-4" />}
|
||||
pendingChildren="Tamamlanıyor"
|
||||
>
|
||||
Tamamla
|
||||
</Button>
|
||||
</PendingSubmitButton>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -450,9 +456,12 @@ function PlanningSectionCard({ section }: { section: ProjectPlanningSectionItem
|
||||
<form action={deleteProjectPlanningSectionRecord}>
|
||||
<input type="hidden" name="id" value={section.id} />
|
||||
<input type="hidden" name="project_id" value={section.project_id} />
|
||||
<Button type="submit" variant="outline" className="h-9 px-3 text-rose-600">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<PendingSubmitButton
|
||||
variant="outline"
|
||||
className="h-9 px-3 text-rose-600"
|
||||
idleIcon={<Trash2 className="h-4 w-4" />}
|
||||
aria-label="Sil"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -586,19 +595,48 @@ function TaskPanel({
|
||||
}) {
|
||||
const [view, setView] = useState<"list" | "kanban">("list");
|
||||
const [localTasks, setLocalTasks] = useState(tasks);
|
||||
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
setLocalTasks(tasks);
|
||||
}, [tasks]);
|
||||
|
||||
function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) {
|
||||
const previousTasks = localTasks;
|
||||
|
||||
setPendingTask(taskId, true);
|
||||
setLocalTasks((currentTasks) =>
|
||||
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
|
||||
);
|
||||
|
||||
startTransition(() => {
|
||||
void updateTaskStatusRecord(taskId, status, projectId).catch(() => {
|
||||
setLocalTasks(previousTasks);
|
||||
});
|
||||
void updateTaskStatusRecord(taskId, status, projectId)
|
||||
.catch((error) => {
|
||||
setLocalTasks(previousTasks);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Görev durumu güncellenemedi.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setPendingTask(taskId, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPendingTask(taskId: string, pending: boolean) {
|
||||
setPendingTaskIds((current) => {
|
||||
const next = new Set(current);
|
||||
|
||||
if (pending) {
|
||||
next.add(taskId);
|
||||
} else {
|
||||
next.delete(taskId);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -682,14 +720,21 @@ function TaskPanel({
|
||||
</div>
|
||||
<div className="flex justify-start lg:justify-end">
|
||||
{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-9 gap-2 px-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={pendingTaskIds.has(task.id)}
|
||||
aria-busy={pendingTaskIds.has(task.id)}
|
||||
className="h-9 gap-2 px-3"
|
||||
onClick={() => handleTaskStatusChange(task.id, "done")}
|
||||
>
|
||||
{pendingTaskIds.has(task.id) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Tamamla
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
{pendingTaskIds.has(task.id) ? "Tamamlanıyor" : "Tamamla"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -700,8 +745,8 @@ function TaskPanel({
|
||||
|
||||
{localTasks.length > 0 && view === "kanban" ? (
|
||||
<ProjectTaskKanban
|
||||
projectId={projectId}
|
||||
tasks={localTasks}
|
||||
pendingTaskIds={pendingTaskIds}
|
||||
onTaskStatusChange={handleTaskStatusChange}
|
||||
/>
|
||||
) : null}
|
||||
@@ -715,12 +760,12 @@ function TaskPanel({
|
||||
}
|
||||
|
||||
function ProjectTaskKanban({
|
||||
projectId,
|
||||
tasks,
|
||||
pendingTaskIds,
|
||||
onTaskStatusChange,
|
||||
}: {
|
||||
projectId: string;
|
||||
tasks: ProjectDetailTaskItem[];
|
||||
pendingTaskIds: Set<string>;
|
||||
onTaskStatusChange: (taskId: string, status: ProjectDetailTaskItem["status"]) => void;
|
||||
}) {
|
||||
const columns = ["todo", "in_progress", "done"] as const;
|
||||
@@ -784,19 +829,22 @@ function ProjectTaskKanban({
|
||||
<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"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={pendingTaskIds.has(task.id)}
|
||||
aria-busy={pendingTaskIds.has(task.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Tamamla"
|
||||
aria-label="Tamamla"
|
||||
onClick={() => onTaskStatusChange(task.id, "done")}
|
||||
>
|
||||
{pendingTaskIds.has(task.id) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
createProjectRecord,
|
||||
updateProjectRecord,
|
||||
} from "@/app/(dashboard)/projects/actions";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { PendingSubmitButton } from "@/components/ui/pending-submit-button";
|
||||
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -36,8 +38,7 @@ import {
|
||||
Brain,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState, type ChangeEvent } from "react";
|
||||
|
||||
export type ProjectClientOption = {
|
||||
@@ -222,9 +223,21 @@ function ProjectCard({
|
||||
clients: ProjectClientOption[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
const detailHref = `/projects/${project.id}`;
|
||||
|
||||
useEffect(() => {
|
||||
setIsNavigating(false);
|
||||
}, [pathname]);
|
||||
|
||||
function goToProjectDetail() {
|
||||
router.push(`/projects/${project.id}`);
|
||||
setIsNavigating(true);
|
||||
router.push(detailHref);
|
||||
}
|
||||
|
||||
function prefetchProjectDetail() {
|
||||
router.prefetch(detailHref);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -232,8 +245,15 @@ function ProjectCard({
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
aria-label={`${project.name} detayına git`}
|
||||
className="cursor-pointer transition-colors hover:border-primary/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-busy={isNavigating}
|
||||
className={
|
||||
isNavigating
|
||||
? "relative cursor-progress opacity-80 transition-colors ring-2 ring-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
: "relative cursor-pointer transition-colors hover:border-primary/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
}
|
||||
onClick={goToProjectDetail}
|
||||
onMouseEnter={prefetchProjectDetail}
|
||||
onFocus={prefetchProjectDetail}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
@@ -242,6 +262,11 @@ function ProjectCard({
|
||||
}}
|
||||
>
|
||||
<CardContent className="flex h-full flex-col gap-5 p-5">
|
||||
{isNavigating ? (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-sm bg-background/70 backdrop-blur-[1px]">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||||
</div>
|
||||
) : null}
|
||||
<ProjectCover project={project} />
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
@@ -353,24 +378,23 @@ function ProjectActions({
|
||||
title="Detaya git"
|
||||
aria-label="Detaya git"
|
||||
>
|
||||
<Link href={`/projects/${project.id}`}>
|
||||
<PendingLink href={`/projects/${project.id}`} className="flex h-full w-full items-center justify-center" showSpinner>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
</PendingLink>
|
||||
</Button>
|
||||
) : null}
|
||||
<ProjectDialog mode="edit" project={project} clients={clients} iconOnly />
|
||||
{project.status !== "completed" ? (
|
||||
<form action={completeProjectRecord}>
|
||||
<input type="hidden" name="id" value={project.id} />
|
||||
<Button
|
||||
type="submit"
|
||||
<PendingSubmitButton
|
||||
variant="outline"
|
||||
className="h-9 w-9 p-0"
|
||||
title="Tamamla"
|
||||
aria-label="Tamamla"
|
||||
idleIcon={<CheckCircle2 className="h-4 w-4" />}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</PendingSubmitButton>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
completeTaskRecord,
|
||||
createTaskRecord,
|
||||
deleteTaskRecord,
|
||||
updateTaskStatusRecord,
|
||||
@@ -21,12 +20,13 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
toast,
|
||||
} from "poyraz-ui/molecules";
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
KanbanSquare,
|
||||
LayoutList,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
@@ -86,6 +86,7 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [projectFilter, setProjectFilter] = useState("__all");
|
||||
const [view, setView] = useState<"list" | "kanban">("list");
|
||||
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,14 +96,65 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
|
||||
function handleTaskStatusChange(taskId: string, status: TaskListItem["status"]) {
|
||||
const previousTasks = localTasks;
|
||||
|
||||
setPendingTask(taskId, true);
|
||||
setLocalTasks((currentTasks) =>
|
||||
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
|
||||
);
|
||||
|
||||
startTransition(() => {
|
||||
void updateTaskStatusRecord(taskId, status).catch(() => {
|
||||
setLocalTasks(previousTasks);
|
||||
});
|
||||
void updateTaskStatusRecord(taskId, status)
|
||||
.catch((error) => {
|
||||
setLocalTasks(previousTasks);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Görev durumu güncellenemedi.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setPendingTask(taskId, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleTaskDelete(taskId: string) {
|
||||
const previousTasks = localTasks;
|
||||
const task = localTasks.find((item) => item.id === taskId);
|
||||
const formData = new FormData();
|
||||
formData.set("id", taskId);
|
||||
|
||||
if (task?.project_id) {
|
||||
formData.set("project_id", task.project_id);
|
||||
}
|
||||
|
||||
setPendingTask(taskId, true);
|
||||
setLocalTasks((currentTasks) => currentTasks.filter((item) => item.id !== taskId));
|
||||
|
||||
startTransition(() => {
|
||||
void deleteTaskRecord(formData)
|
||||
.catch((error) => {
|
||||
setLocalTasks(previousTasks);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Görev silinemedi.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setPendingTask(taskId, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPendingTask(taskId: string, pending: boolean) {
|
||||
setPendingTaskIds((current) => {
|
||||
const next = new Set(current);
|
||||
|
||||
if (pending) {
|
||||
next.add(taskId);
|
||||
} else {
|
||||
next.delete(taskId);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,12 +260,21 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
|
||||
|
||||
{filteredTasks.length > 0 ? (
|
||||
view === "list" ? (
|
||||
<TaskList tasks={filteredTasks} clients={clients} projects={projects} />
|
||||
<TaskList
|
||||
tasks={filteredTasks}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
pendingTaskIds={pendingTaskIds}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onTaskStatusChange={handleTaskStatusChange}
|
||||
/>
|
||||
) : (
|
||||
<TaskKanban
|
||||
tasks={filteredTasks}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
pendingTaskIds={pendingTaskIds}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onTaskStatusChange={handleTaskStatusChange}
|
||||
/>
|
||||
)
|
||||
@@ -230,10 +291,16 @@ function TaskList({
|
||||
tasks,
|
||||
clients,
|
||||
projects,
|
||||
pendingTaskIds,
|
||||
onTaskDelete,
|
||||
onTaskStatusChange,
|
||||
}: {
|
||||
tasks: TaskListItem[];
|
||||
clients: TaskRelationOption[];
|
||||
projects: TaskRelationOption[];
|
||||
pendingTaskIds: Set<string>;
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-sm border border-border">
|
||||
@@ -247,7 +314,15 @@ function TaskList({
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{tasks.map((task) => (
|
||||
<TaskRow key={task.id} task={task} clients={clients} projects={projects} />
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
isPending={pendingTaskIds.has(task.id)}
|
||||
onTaskDelete={onTaskDelete}
|
||||
onTaskStatusChange={onTaskStatusChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,10 +334,16 @@ function TaskRow({
|
||||
task,
|
||||
clients,
|
||||
projects,
|
||||
isPending,
|
||||
onTaskDelete,
|
||||
onTaskStatusChange,
|
||||
}: {
|
||||
task: TaskListItem;
|
||||
clients: TaskRelationOption[];
|
||||
projects: TaskRelationOption[];
|
||||
isPending: boolean;
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-4 px-4 py-4 grid-cols-[1.5fr_1fr_1fr_0.8fr_1fr] items-center">
|
||||
@@ -286,7 +367,14 @@ function TaskRow({
|
||||
<div className={isOverdue(task) ? "text-sm font-medium text-rose-600" : "text-sm text-muted-foreground"}>
|
||||
{task.due_at ? formatDateTime(task.due_at) : "Yok"}
|
||||
</div>
|
||||
<TaskActions task={task} clients={clients} projects={projects} />
|
||||
<TaskActions
|
||||
task={task}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
isPending={isPending}
|
||||
onTaskDelete={onTaskDelete}
|
||||
onTaskStatusChange={onTaskStatusChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -295,11 +383,15 @@ function TaskKanban({
|
||||
tasks,
|
||||
clients,
|
||||
projects,
|
||||
pendingTaskIds,
|
||||
onTaskDelete,
|
||||
onTaskStatusChange,
|
||||
}: {
|
||||
tasks: TaskListItem[];
|
||||
clients: TaskRelationOption[];
|
||||
projects: TaskRelationOption[];
|
||||
pendingTaskIds: Set<string>;
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void;
|
||||
}) {
|
||||
const columns = ["todo", "in_progress", "done"] as const;
|
||||
@@ -362,7 +454,15 @@ function TaskKanban({
|
||||
<Badge className={priorityClasses[task.priority]}>
|
||||
{priorityLabels[task.priority]}
|
||||
</Badge>
|
||||
<TaskActions task={task} clients={clients} projects={projects} compact />
|
||||
<TaskActions
|
||||
task={task}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
compact
|
||||
isPending={pendingTaskIds.has(task.id)}
|
||||
onTaskDelete={onTaskDelete}
|
||||
onTaskStatusChange={onTaskStatusChange}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -380,30 +480,52 @@ function TaskActions({
|
||||
clients,
|
||||
projects,
|
||||
compact = false,
|
||||
isPending,
|
||||
onTaskDelete,
|
||||
onTaskStatusChange,
|
||||
}: {
|
||||
task: TaskListItem;
|
||||
clients: TaskRelationOption[];
|
||||
projects: TaskRelationOption[];
|
||||
compact?: boolean;
|
||||
isPending: boolean;
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={compact ? "flex justify-end gap-1" : "flex justify-start gap-2 lg:justify-end"}>
|
||||
<TaskDialog mode="edit" task={task} clients={clients} projects={projects} />
|
||||
{task.status !== "done" ? (
|
||||
<form action={completeTaskRecord}>
|
||||
<input type="hidden" name="id" value={task.id} />
|
||||
<Button type="submit" variant="outline" className="h-9 min-w-24 gap-2 px-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isPending}
|
||||
aria-busy={isPending}
|
||||
className="h-9 min-w-24 gap-2 px-3"
|
||||
onClick={() => onTaskStatusChange(task.id, "done")}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{!compact ? "Tamamla" : null}
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
<form action={deleteTaskRecord}>
|
||||
<input type="hidden" name="id" value={task.id} />
|
||||
<Button type="submit" variant="outline" className="h-9 gap-2 px-3 text-rose-600">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
{!compact ? (isPending ? "Tamamlanıyor" : "Tamamla") : null}
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isPending}
|
||||
aria-busy={isPending}
|
||||
className="h-9 gap-2 px-3 text-rose-600"
|
||||
onClick={() => onTaskDelete(task.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user