From d3089ff2dd90f8c89177821f87ca8d4859715655 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Sun, 7 Jun 2026 15:41:33 +0330 Subject: [PATCH] feat: add feedback --- app/dashboard/app-feedback/page.tsx | 195 ++++++++++++++++++++++++++++ lib/nav.ts | 5 +- 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 app/dashboard/app-feedback/page.tsx diff --git a/app/dashboard/app-feedback/page.tsx b/app/dashboard/app-feedback/page.tsx new file mode 100644 index 0000000..846b671 --- /dev/null +++ b/app/dashboard/app-feedback/page.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { apiFetch, ApiError } from "@/lib/api"; +import { useToast } from "@/components/toast"; +import { DataTable, type Column } from "@/components/DataTable"; +import { Button, Card, ConfirmDialog, PageHeader } from "@/components/ui"; +import { TrashIcon } from "@/components/icons"; +import { toFa } from "@/lib/utils"; + +interface Feedback { + id: number; + stars?: number | null; + content?: string | null; + created_at?: string; + user?: { + name?: string | null; + identifier?: string | null; + mobile?: string | null; + } | null; +} + +interface Summary { + total: number; + rated: number; + with_comment: number; + average_stars: number; +} + +interface AdminResponse { + summary: Summary; + feedback: { data: Feedback[] }; +} + +function Stars({ value }: { value?: number | null }) { + if (!value) return ; + return ( + + {"★".repeat(value)} + {"★".repeat(5 - value)} + + ); +} + +function userName(u: Feedback["user"]): string { + return u?.name || u?.identifier || u?.mobile || "—"; +} + +function faDate(value?: string): string { + if (!value) return "—"; + try { + return toFa(new Date(value).toLocaleDateString("fa-IR")); + } catch { + return "—"; + } +} + +export default function AppFeedbackPage() { + const toast = useToast(); + + const [rows, setRows] = useState([]); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + // Fetch without touching state synchronously (safe to call from an effect). + const fetchFeedback = useCallback(() => { + return apiFetch("/admin/app-feedback") + .then((res) => { + setRows(res.feedback?.data ?? []); + setSummary(res.summary ?? null); + setError(null); + }) + .catch((e: unknown) => + setError(e instanceof ApiError ? e.message : "خطا در دریافت اطلاعات"), + ) + .finally(() => setLoading(false)); + }, []); + + // Reload triggered by user actions (retry / after delete): show the spinner. + const reload = useCallback(() => { + setLoading(true); + setError(null); + fetchFeedback(); + }, [fetchFeedback]); + + useEffect(() => { + fetchFeedback(); + }, [fetchFeedback]); + + async function confirmDelete() { + if (!deleting) return; + setRemoving(true); + try { + await apiFetch(`/admin/app-feedback/${deleting.id}`, { method: "DELETE" }); + toast.success("بازخورد حذف شد."); + setDeleting(null); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در حذف"); + } finally { + setRemoving(false); + } + } + + const columns: Column[] = [ + { key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" }, + { key: "user", header: "کاربر", render: (r) => userName(r.user) }, + { + key: "stars", + header: "امتیاز", + className: "w-28", + render: (r) => , + }, + { + key: "content", + header: "ایده / نظر", + render: (r) => r.content || , + }, + { + key: "created_at", + header: "تاریخ", + className: "w-32", + render: (r) => faDate(r.created_at), + }, + ]; + + return ( +
+ + + {summary && ( +
+ +

میانگین امتیاز

+

+ {toFa(summary.average_stars)} +

+
+ +

کل بازخوردها

+

+ {toFa(summary.total)} +

+
+ +

دارای امتیاز

+

+ {toFa(summary.rated)} +

+
+ +

دارای نظر

+

+ {toFa(summary.with_comment)} +

+
+
+ )} + + ( + + )} + /> + + setDeleting(null)} + /> +
+ ); +} diff --git a/lib/nav.ts b/lib/nav.ts index 17d15bc..f84d1ca 100644 --- a/lib/nav.ts +++ b/lib/nav.ts @@ -118,6 +118,9 @@ export const NAV: NavSection[] = [ { label: "محتوای کاربران", icon: TagIcon, - items: [{ href: "/dashboard/comments", label: "نظرات" }], + items: [ + { href: "/dashboard/comments", label: "نظرات" }, + { href: "/dashboard/app-feedback", label: "نظرات و ایده‌ها برنامه" }, + ], }, ];