From 97948767a0e52ed797f1b1d539c17abe7d47ef69 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Sat, 25 Jul 2026 15:05:07 +0330 Subject: [PATCH] admin: Slice 3d moderation UI (comments + chat) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hooks: useAdminComments/ChatMessages, useAdminCommentModerate/Delete, AdminCommentRow/AdminChatMessageRow types. - /admin/moderation: tabbed page — comments (hide/unhide/delete, filter by visibility & top/reply) and read-only chat browse. - sidebar "مدیریت محتوا" nav link. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/requestHandler/use-admin-hooks.ts | 53 ++++++ app/routes/admin.moderation.tsx | 228 ++++++++++++++++++++++++++ app/routes/admin.tsx | 2 + 3 files changed, 283 insertions(+) create mode 100644 app/routes/admin.moderation.tsx diff --git a/app/requestHandler/use-admin-hooks.ts b/app/requestHandler/use-admin-hooks.ts index eb63837..3591e37 100644 --- a/app/requestHandler/use-admin-hooks.ts +++ b/app/requestHandler/use-admin-hooks.ts @@ -365,6 +365,59 @@ export interface AdminCollectionRow { createdAt: string; } +/* --------------------------- moderation (3d) --------------------------- */ + +export interface AdminCommentRow { + id: string; + productId: string; + productTitle: string | null; + userPhone: string | null; + comment: string; + parentId: string | null; + isReply: boolean; + repliesCount: number; + isHidden: boolean; + isVerifiedBuyer: boolean; + createdAt: string; +} + +export interface AdminChatMessageRow { + id: string; + content: string; + senderPhone: string | null; + threadId: string | null; + productId: string | null; + imageUrl: string | null; + isDeleted: boolean; + createdAt: string; +} + +export const useAdminComments = (params: AdminListParams) => + useAdminList("moderation/comments", params); +export const useAdminChatMessages = (params: AdminListParams) => + useAdminList("moderation/chat", params); + +export function useAdminCommentModerate() { + const token = useAuthToken(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, isHidden }: { id: string; isHidden: boolean }) => + adminPatch(`moderation/comments/${id}/`, { isHidden }, token as string), + onSuccess: () => + qc.invalidateQueries({ queryKey: ["admin", "moderation/comments"] }), + }); +} + +export function useAdminCommentDelete() { + const token = useAuthToken(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => adminDelete(`moderation/comments/${id}/`, token as string), + onSuccess: () => + qc.invalidateQueries({ queryKey: ["admin", "moderation/comments"] }), + }); +} + /* ---------------------- AI pipelines health (3c) ---------------------- */ interface FailureRow { diff --git a/app/routes/admin.moderation.tsx b/app/routes/admin.moderation.tsx new file mode 100644 index 0000000..e8f0d9e --- /dev/null +++ b/app/routes/admin.moderation.tsx @@ -0,0 +1,228 @@ +import { useState } from "react"; +import { Eye, EyeOff, Trash2 } from "lucide-react"; +import { + useAdminComments, + useAdminChatMessages, + useAdminCommentModerate, + useAdminCommentDelete, + type AdminCommentRow, + type AdminChatMessageRow, +} from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; +import { cn } from "~/lib/utils"; + +const PAGE_SIZE = 25; + +export default function AdminModeration() { + const [tab, setTab] = useState<"comments" | "chat">("comments"); + return ( +
+ مدیریت محتوا +
+ {(["comments", "chat"] as const).map((t) => ( + + ))} +
+ {tab === "comments" ? : } +
+ ); +} + +function CommentsTab() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [hidden, setHidden] = useState(""); + const [kind, setKind] = useState(""); + const debouncedSearch = useDebouncedValue(search); + + const { data, isLoading } = useAdminComments({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + is_hidden: hidden, + kind, + }); + const moderate = useAdminCommentModerate(); + const del = useAdminCommentDelete(); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const onDelete = (r: AdminCommentRow) => { + if (confirm("حذف این نظر؟ این عمل بازگشت‌پذیر نیست.")) del.mutate(r.id); + }; + + const columns: Column[] = [ + { + header: "نظر", + render: (r) => ( +
+

+ {r.comment} +

+

+ {r.productTitle || "—"} + {r.isReply && · پاسخ} + {!r.isReply && r.repliesCount > 0 && · {r.repliesCount} پاسخ} +

+
+ ), + }, + { + header: "کاربر", + render: (r) => ( + + {r.userPhone || "—"} + + ), + }, + { + header: "وضعیت", + render: (r) => + r.isHidden ? ( + + ) : ( + + ), + }, + { header: "تاریخ", render: (r) => {faDate(r.createdAt)} }, + { + header: "", + render: (r) => ( +
+ + +
+ ), + }, + ]; + + return ( +
+ + reset(() => setSearch(v))} placeholder="جستجوی متن نظر / کاربر…" /> + reset(() => setHidden(v))} + options={[ + { value: "", label: "همه" }, + { value: "false", label: "نمایش‌داده‌شده" }, + { value: "true", label: "پنهان‌شده" }, + ]} + /> + reset(() => setKind(v))} + options={[ + { value: "", label: "همه" }, + { value: "top", label: "نظر اصلی" }, + { value: "reply", label: "پاسخ" }, + ]} + /> + + r.id} emptyText="نظری ثبت نشده است" /> + +
+ ); +} + +function ChatTab() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [deleted, setDeleted] = useState(""); + const debouncedSearch = useDebouncedValue(search); + + const { data, isLoading } = useAdminChatMessages({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + is_deleted: deleted, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const columns: Column[] = [ + { + header: "پیام", + render: (r) => ( +
+

+ {r.content || (r.imageUrl ? "🖼 تصویر" : "—")} +

+
+ ), + }, + { + header: "فرستنده", + render: (r) => ( + + {r.senderPhone || "—"} + + ), + }, + { + header: "وضعیت", + render: (r) => + r.isDeleted ? : , + }, + { header: "تاریخ", render: (r) => {faDate(r.createdAt)} }, + ]; + + return ( +
+ + reset(() => setSearch(v))} placeholder="جستجوی متن پیام / فرستنده…" /> + reset(() => setDeleted(v))} + options={[ + { value: "", label: "همه" }, + { value: "false", label: "فعال" }, + { value: "true", label: "حذف‌شده" }, + ]} + /> + +

مشاهده‌ی فقط‌خواندنی گفتگوها.

+ r.id} emptyText="پیامی یافت نشد" /> + +
+ ); +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index 88be149..ac86015 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -9,6 +9,7 @@ import { LayoutGrid, Landmark, Activity, + MessageSquareWarning, ScrollText, ArrowRight, ShieldCheck, @@ -35,6 +36,7 @@ const NAV: NavItem[] = [ { label: "مجموعه‌ها", to: "/admin/collections", icon: LayoutGrid }, { label: "دفتر مالی", to: "/admin/ledger", icon: Landmark }, { label: "سلامت AI", to: "/admin/ai-health", icon: Activity }, + { label: "مدیریت محتوا", to: "/admin/moderation", icon: MessageSquareWarning }, { label: "گزارش فعالیت", to: "/admin/audit", icon: ScrollText }, ];