"use client"; import { getDocumentIntlLocale } from "@/lib/i18n/browser"; import { useTranslations } from "@/components/i18n/i18n-provider"; import { createTaskRecord, deleteTaskRecord, updateTaskStatusRecord, updateTaskRecord, } from "@/app/(dashboard)/tasks/actions"; import { LocalizedFields, type LocalizedFieldLocale, type LocalizedFieldValues } from "@/components/i18n/localized-fields"; import { contentTranslationRegistry } from "@/lib/i18n/content"; import { Badge, Button, Card, CardContent, Input, Label } from "poyraz-ui/atoms"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "poyraz-ui/molecules"; import { CheckCircle2, KanbanSquare, LayoutList, Loader2, Pencil, Plus, Trash2, } from "lucide-react"; import { useState, useTransition, type DragEvent } 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; translations?: LocalizedFieldValues; }; 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[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }; export function TasksClient({ tasks, clients, projects, localization }: TasksClientProps) { const t = useTranslations(); const [statusOverrides, setStatusOverrides] = useState< Partial> >({}); const [deletedTaskIds, setDeletedTaskIds] = useState>(new Set()); const [query, setQuery] = useState(""); const [projectFilter, setProjectFilter] = useState("__all"); const [view, setView] = useState<"list" | "kanban">("list"); const [pendingTaskIds, setPendingTaskIds] = useState>(new Set()); const [, startTransition] = useTransition(); const localTasks = tasks .filter((task) => !deletedTaskIds.has(task.id)) .map((task) => ({ ...task, status: statusOverrides[task.id] ?? task.status, })); function handleTaskStatusChange(taskId: string, status: TaskListItem["status"]) { const previousStatus = localTasks.find((task) => task.id === taskId)?.status; setPendingTask(taskId, true); setStatusOverrides((current) => ({ ...current, [taskId]: status })); startTransition(() => { void updateTaskStatusRecord(taskId, status) .catch((error) => { setStatusOverrides((current) => { const next = { ...current }; if (previousStatus) next[taskId] = previousStatus; else delete next[taskId]; return next; }); toast.error( error instanceof Error ? error.message : t("tasks.messages.updateFailed") || "Görev durumu güncellenemedi.", ); }) .finally(() => { setPendingTask(taskId, false); }); }); } function handleTaskDelete(taskId: string) { 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); setDeletedTaskIds((current) => new Set(current).add(taskId)); startTransition(() => { void deleteTaskRecord(formData) .catch((error) => { setDeletedTaskIds((current) => { const next = new Set(current); next.delete(taskId); return next; }); 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; }); } const normalizedQuery = query.trim().toLowerCase(); const filteredByProject = projectFilter === "__all" ? localTasks : projectFilter === "__none" ? localTasks.filter((task) => !task.project_id) : localTasks.filter((task) => task.project_id === projectFilter); const filteredTasks = normalizedQuery ? filteredByProject.filter((task) => [task.title, task.description, task.clientName, task.projectName] .filter(Boolean) .some((value) => value!.toLowerCase().includes(normalizedQuery)), ) : filteredByProject; const doneCount = localTasks.filter((task) => task.status === "done").length; const overdueCount = localTasks.filter((task) => isOverdue(task)).length; const urgentCount = localTasks.filter((task) => task.priority === "urgent").length; return (

{t("tasks.title")}

{t("tasks.list.title")}

{t("tasks.list.showing", { count: filteredTasks.length.toString() })}

setQuery(event.target.value)} placeholder={t("tasks.list.search")} className="sm:w-80" />
{filteredTasks.length > 0 ? ( view === "list" ? ( ) : ( ) ) : ( )}
); } function TaskList({ tasks, clients, projects, localization, pendingTaskIds, onTaskDelete, onTaskStatusChange, }: { tasks: TaskListItem[]; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; pendingTaskIds: Set; onTaskDelete: (taskId: string) => void; onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void; }) { const t = useTranslations(); return (
{t("tasks.col.task")} {t("tasks.col.relation")} {t("tasks.col.priority")} {t("tasks.col.due")} {t("tasks.col.action")}
{tasks.map((task) => ( ))}
); } function TaskRow({ task, clients, projects, localization, isPending, onTaskDelete, onTaskStatusChange, }: { task: TaskListItem; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; isPending: boolean; onTaskDelete: (taskId: string) => void; onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void; }) { const t = useTranslations(); return (
{task.title}
{t(`tasks.status.${task.status}`)}
{task.projectName || t("tasks.row.noProject")}
{task.clientName || t("tasks.row.noClient")}
{t(`tasks.priority.${task.priority}`)}
{task.due_at ? formatDateTime(task.due_at) : t("tasks.row.noDue")}
); } function TaskKanban({ tasks, clients, projects, localization, pendingTaskIds, onTaskDelete, onTaskStatusChange, }: { tasks: TaskListItem[]; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; pendingTaskIds: Set; onTaskDelete: (taskId: string) => void; onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void; }) { const t = useTranslations(); 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: TaskListItem["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)} >

{t(`tasks.status.${status}`)}

{columnTasks.length}
{columnTasks.map((task) => ( handleDragStart(event, task.id)} onDragEnd={() => setDraggedTaskId(null)} >
{task.title}
{task.projectName || task.clientName || t("tasks.col.relation")}
{t(`tasks.priority.${task.priority}`)}
))}
); })}
); } function TaskActions({ task, clients, projects, localization, compact = false, isPending, onTaskDelete, onTaskStatusChange, }: { task: TaskListItem; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; compact?: boolean; isPending: boolean; onTaskDelete: (taskId: string) => void; onTaskStatusChange: (taskId: string, status: TaskListItem["status"]) => void; }) { const t = useTranslations(); return (
{task.status !== "done" ? ( ) : null}
); } function TaskDialog({ mode, task, clients, projects, localization, }: { mode: "create" | "edit"; task?: TaskListItem; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; }) { const t = useTranslations(); 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); toast.success(mode === "create" ? t("tasks.messages.added") : t("tasks.messages.updated")); } catch (error) { toast.error( error instanceof Error ? error.message : t("tasks.messages.saveFailed"), ); } finally { setIsSubmitting(false); } } return (
{task ? : null} {mode === "create" ? t("tasks.form.createTitle") : t("tasks.form.editTitle")} {t("tasks.form.desc")}
); } function TaskFormFields({ task, clients, projects, localization, }: { task?: TaskListItem; clients: TaskRelationOption[]; projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; }) { const t = useTranslations(); const [clientId, setClientId] = useState(task?.client_id || "__none"); const [projectId, setProjectId] = useState(task?.project_id || "__none"); const selectedProject = projectId === "__none" ? null : projects.find((project) => project.id === projectId) || null; const shouldLockClient = Boolean(selectedProject); const filteredProjects = clientId === "__none" || shouldLockClient ? projects : projects.filter((project) => project.client_id === clientId); function handleClientChange(nextClientId: string) { setClientId(nextClientId); if ( projectId !== "__none" && nextClientId !== "__none" && !projects.some((project) => project.id === projectId && project.client_id === nextClientId) ) { setProjectId("__none"); } } function handleProjectChange(nextProjectId: string) { setProjectId(nextProjectId); if (nextProjectId === "__none") { return; } const nextProject = projects.find((project) => project.id === nextProjectId); setClientId(nextProject?.client_id || "__none"); } return (
({ ...f, label: t(`tasks.fields.${f.name}`) || f.label, placeholder: f.placeholder ? t(`tasks.placeholders.${f.name}`) || f.placeholder : undefined, }))} values={task?.translations} fallbackValues={{ title: task?.title, description: task?.description, }} />
{t("tasks.status.todo")} {t("tasks.status.in_progress")} {t("tasks.status.done")} {t("tasks.priority.low")} {t("tasks.priority.medium")} {t("tasks.priority.high")} {t("tasks.priority.urgent")}
{shouldLockClient ? : null}
); } function SelectField({ name, label, defaultValue, children, }: { name: string; label: string; defaultValue: string; children: React.ReactNode; }) { const t = useTranslations(); return (
); } function StatCard({ label, value }: { label: string; value: string }) { return (

{label}

{value}

); } function EmptyState({ hasQuery }: { hasQuery: boolean }) { const t = useTranslations(); return (

{hasQuery ? t("tasks.empty.noMatchTitle") : t("tasks.empty.noTaskTitle")}

{hasQuery ? t("tasks.empty.noMatchDesc") : t("tasks.empty.noTaskDesc")}

); } function isOverdue(task: TaskListItem) { return Boolean(task.due_at && task.status !== "done" && new Date(task.due_at) < new Date()); } function formatDateTime(value: string) { return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }).format(new Date(value)); } function toDateTimeLocal(value: string) { const date = new Date(value); const offsetDate = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); return offsetDate.toISOString().slice(0, 16); }