From 935c337df0fa6e26d7a4217e673c6617c53b2640 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Sat, 25 Jul 2026 12:17:22 +0330 Subject: [PATCH] =?UTF-8?q?feat(admin):=20monitoring=20boards=20UI=20?= =?UTF-8?q?=E2=80=94=20orders/sellers/users/products=20(slice=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reusable admin primitives: AdminTable, AdminPagination, AdminToolbar, AdminSearch/Select, StatusBadge + fa number/date/toman + useDebouncedValue. - Four board routes with search, filters, paging: orders (read-only), sellers (approve/reject/verify), users (ban/unban), products (active toggle). Wired into the admin sidebar nav + hooks. Co-Authored-By: Claude Opus 4.8 --- app/components/admin/DataTable.tsx | 248 ++++++++++++++++++++++++++ app/requestHandler/use-admin-hooks.ts | 140 ++++++++++++++- app/routes/admin.orders.tsx | 150 ++++++++++++++++ app/routes/admin.products.tsx | 160 +++++++++++++++++ app/routes/admin.sellers.tsx | 193 ++++++++++++++++++++ app/routes/admin.tsx | 8 +- app/routes/admin.users.tsx | 167 +++++++++++++++++ 7 files changed, 1061 insertions(+), 5 deletions(-) create mode 100644 app/components/admin/DataTable.tsx create mode 100644 app/routes/admin.orders.tsx create mode 100644 app/routes/admin.products.tsx create mode 100644 app/routes/admin.sellers.tsx create mode 100644 app/routes/admin.users.tsx diff --git a/app/components/admin/DataTable.tsx b/app/components/admin/DataTable.tsx new file mode 100644 index 0000000..8cc98ec --- /dev/null +++ b/app/components/admin/DataTable.tsx @@ -0,0 +1,248 @@ +import { useEffect, useState } from "react"; +import { ChevronLeft, ChevronRight, Search, Loader2 } from "lucide-react"; +import { cn } from "~/lib/utils"; + +/* ------------------------------ helpers ------------------------------ */ + +export const faNum = (n?: number | string | null) => + n === null || n === undefined || n === "" + ? "—" + : Number(n).toLocaleString("fa-IR"); + +export const faToman = (n?: number | null) => + n === null || n === undefined ? "—" : `${Math.round(n).toLocaleString("fa-IR")} تومان`; + +export const faDate = (iso?: string | null) => { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleDateString("fa-IR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + } catch { + return "—"; + } +}; + +/** Debounce a rapidly-changing value (e.g. a search box). */ +export function useDebouncedValue(value: T, delay = 350): T { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const t = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(t); + }, [value, delay]); + return debounced; +} + +/* ---------------------------- status badge --------------------------- */ + +type Tone = "green" | "red" | "yellow" | "blue" | "gray"; + +const TONE: Record = { + green: "bg-GREEN/10 text-GREEN", + red: "bg-RED/10 text-RED", + yellow: "bg-yellow-500/10 text-yellow-600", + blue: "bg-VITROWN_BLUE/10 text-VITROWN_BLUE", + gray: "bg-WHITE3 text-BLACK2", +}; + +export function StatusBadge({ label, tone }: { label: string; tone: Tone }) { + return ( + + {label} + + ); +} + +/* ------------------------------- table ------------------------------- */ + +export interface Column { + header: string; + render: (row: T) => React.ReactNode; + className?: string; +} + +export function AdminTable({ + columns, + rows, + isLoading, + getRowKey, + emptyText = "موردی یافت نشد", +}: { + columns: Column[]; + rows: T[]; + isLoading?: boolean; + getRowKey: (row: T) => string; + emptyText?: string; +}) { + return ( +
+
+ + + + {columns.map((c, i) => ( + + ))} + + + + {isLoading ? ( + + + + ) : rows.length === 0 ? ( + + + + ) : ( + rows.map((row) => ( + + {columns.map((c, i) => ( + + ))} + + )) + )} + +
+ {c.header} +
+ +
+ {emptyText} +
+ {c.render(row)} +
+
+
+ ); +} + +/* ---------------------------- pagination ----------------------------- */ + +export function AdminPagination({ + page, + pageSize, + count, + onPageChange, +}: { + page: number; + pageSize: number; + count: number; + onPageChange: (page: number) => void; +}) { + const totalPages = Math.max(1, Math.ceil(count / pageSize)); + const from = count === 0 ? 0 : (page - 1) * pageSize + 1; + const to = Math.min(count, page * pageSize); + const btn = + "h-8 w-8 grid place-items-center rounded-lg border border-WHITE3 text-BLACK2 disabled:opacity-40 hover:bg-WHITE2 transition-colors cursor-pointer disabled:cursor-default"; + + return ( +
+ + {faNum(from)}–{faNum(to)} از {faNum(count)} + +
+ + + {faNum(page)} / {faNum(totalPages)} + + +
+
+ ); +} + +/* --------------------------- toolbar bits ---------------------------- */ + +export function AdminSearch({ + value, + onChange, + placeholder = "جستجو…", +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full rounded-xl border border-WHITE3 bg-WHITE pr-9 pl-3 py-2 text-[13px] outline-none focus:border-VITROWN_BLUE" + /> +
+ ); +} + +export function AdminSelect({ + value, + onChange, + options, +}: { + value: string; + onChange: (v: string) => void; + options: { value: string; label: string }[]; +}) { + return ( + + ); +} + +export function AdminToolbar({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +export function AdminPageTitle({ children }: { children: React.ReactNode }) { + return ( +

{children}

+ ); +} diff --git a/app/requestHandler/use-admin-hooks.ts b/app/requestHandler/use-admin-hooks.ts index 7d96e9d..fa5b3b9 100644 --- a/app/requestHandler/use-admin-hooks.ts +++ b/app/requestHandler/use-admin-hooks.ts @@ -1,4 +1,9 @@ -import { useQuery } from "@tanstack/react-query"; +import { + useQuery, + useMutation, + useQueryClient, + keepPreviousData, +} from "@tanstack/react-query"; import { useAuthToken } from "~/hooks/use-root-data"; import { getApiBaseUrl } from "~/utils/api-config"; @@ -57,6 +62,25 @@ async function adminGet(path: string, token: string): Promise { return res.json(); } +async function adminPatch( + path: string, + body: Record, + token: string +): Promise { + const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`Admin update failed (${res.status})`); + } + return res.json(); +} + export function useAdminOverview() { const token = useAuthToken(); return useQuery({ @@ -66,3 +90,117 @@ export function useAdminOverview() { queryFn: () => adminGet("overview/", token as string), }); } + +/* ------------------------------ boards ------------------------------ */ + +export interface Paginated { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} + +export type AdminListParams = Record< + string, + string | number | boolean | undefined +>; + +function toQuery(params: AdminListParams): string { + const usp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { + if (v !== undefined && v !== "" && v !== null) usp.set(k, String(v)); + }); + const s = usp.toString(); + return s ? `?${s}` : ""; +} + +function useAdminList(key: string, params: AdminListParams) { + const token = useAuthToken(); + return useQuery({ + queryKey: ["admin", key, params], + enabled: !!token, + placeholderData: keepPreviousData, + queryFn: () => + adminGet>(`${key}/${toQuery(params)}`, token as string), + }); +} + +export interface AdminOrderRow { + id: string; + createdAt: string; + status: string; + paymentStatus: string; + totalPrice: number; + userPhone: string | null; + userName: string | null; + sellerName: string | null; + sellerUsername: string | null; +} + +export interface AdminSellerRow { + id: string; + username: string; + storeName: string; + storeLogo: string | null; + status: string; + isVerified: boolean; + platformFee: number; + productCount: number; + followerCount: number; + reviewAverage: number; + createdAt: string; + userPhone: string | null; +} + +export interface AdminUserRow { + id: string; + phoneNumber: string; + username: string | null; + email: string | null; + isActive: boolean; + isSeller: boolean; + isStaff: boolean; + isSuperuser: boolean; + otpVerified: boolean; + createdAt: string; +} + +export interface AdminProductRow { + id: string; + title: string; + basePrice: number; + isActive: boolean; + createdAt: string; + sellerName: string | null; + sellerUsername: string | null; + categoryName: string | null; + primaryImage: string | null; +} + +export const useAdminOrders = (params: AdminListParams) => + useAdminList("orders", params); +export const useAdminSellers = (params: AdminListParams) => + useAdminList("sellers", params); +export const useAdminUsers = (params: AdminListParams) => + useAdminList("users", params); +export const useAdminProducts = (params: AdminListParams) => + useAdminList("products", params); + +/* ----------------------------- actions ------------------------------ */ + +function useAdminUpdate(resource: "sellers" | "users" | "products") { + const token = useAuthToken(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => + adminPatch(`${resource}/${id}/`, data, token as string), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["admin", resource] }); + qc.invalidateQueries({ queryKey: ["admin", "overview"] }); + }, + }); +} + +export const useAdminSellerUpdate = () => useAdminUpdate("sellers"); +export const useAdminUserUpdate = () => useAdminUpdate("users"); +export const useAdminProductUpdate = () => useAdminUpdate("products"); diff --git a/app/routes/admin.orders.tsx b/app/routes/admin.orders.tsx new file mode 100644 index 0000000..101c1a1 --- /dev/null +++ b/app/routes/admin.orders.tsx @@ -0,0 +1,150 @@ +import type { MetaFunction } from "@remix-run/node"; +import { useState } from "react"; +import { useAdminOrders } from "~/requestHandler/use-admin-hooks"; +import type { AdminOrderRow } from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faToman, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; + +export const meta: MetaFunction = () => [ + { title: "سفارش‌ها | پنل مدیریت" }, + { name: "robots", content: "noindex, nofollow" }, +]; + +const PAGE_SIZE = 25; + +const STATUS: Record = { + pending: { label: "در انتظار", tone: "yellow" }, + confirmed: { label: "تأییدشده", tone: "blue" }, + shipped: { label: "ارسال‌شده", tone: "blue" }, + delivered: { label: "تحویل‌شده", tone: "green" }, + cancelled: { label: "لغوشده", tone: "red" }, +}; +const PAYMENT: Record = { + paid: { label: "پرداخت‌شده", tone: "green" }, + pending: { label: "در انتظار", tone: "yellow" }, + failed: { label: "ناموفق", tone: "red" }, +}; + +export default function AdminOrders() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState(""); + const [payment, setPayment] = useState(""); + const debouncedSearch = useDebouncedValue(search); + + const { data, isLoading } = useAdminOrders({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + status, + payment_status: payment, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const columns: Column[] = [ + { + header: "کد", + render: (o) => {o.id.slice(0, 8)}, + }, + { + header: "خریدار", + render: (o) => ( +
+

{o.userName || "—"}

+

+ {o.userPhone || ""} +

+
+ ), + }, + { + header: "فروشگاه", + render: (o) => {o.sellerName || o.sellerUsername || "—"}, + }, + { + header: "مبلغ", + render: (o) => {faToman(o.totalPrice)}, + }, + { + header: "پرداخت", + render: (o) => { + const p = PAYMENT[o.paymentStatus] || { label: o.paymentStatus, tone: "gray" as const }; + return ; + }, + }, + { + header: "وضعیت", + render: (o) => { + const s = STATUS[o.status] || { label: o.status, tone: "gray" as const }; + return ; + }, + }, + { + header: "تاریخ", + render: (o) => {faDate(o.createdAt)}, + }, + ]; + + return ( +
+ سفارش‌ها + + reset(() => setSearch(v))} + placeholder="جستجوی خریدار یا فروشگاه…" + /> + reset(() => setStatus(v))} + options={[ + { value: "", label: "همه وضعیت‌ها" }, + { value: "pending", label: "در انتظار" }, + { value: "confirmed", label: "تأییدشده" }, + { value: "shipped", label: "ارسال‌شده" }, + { value: "delivered", label: "تحویل‌شده" }, + { value: "cancelled", label: "لغوشده" }, + ]} + /> + reset(() => setPayment(v))} + options={[ + { value: "", label: "همه پرداخت‌ها" }, + { value: "paid", label: "پرداخت‌شده" }, + { value: "pending", label: "در انتظار" }, + { value: "failed", label: "ناموفق" }, + ]} + /> + + + o.id} + /> + +
+ ); +} diff --git a/app/routes/admin.products.tsx b/app/routes/admin.products.tsx new file mode 100644 index 0000000..5cb421f --- /dev/null +++ b/app/routes/admin.products.tsx @@ -0,0 +1,160 @@ +import type { MetaFunction } from "@remix-run/node"; +import { useState } from "react"; +import { Link } from "@remix-run/react"; +import { Loader2, Eye, EyeOff } from "lucide-react"; +import { + useAdminProducts, + useAdminProductUpdate, +} from "~/requestHandler/use-admin-hooks"; +import type { AdminProductRow } from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faToman, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; + +export const meta: MetaFunction = () => [ + { title: "محصولات | پنل مدیریت" }, + { name: "robots", content: "noindex, nofollow" }, +]; + +const PAGE_SIZE = 25; + +export default function AdminProducts() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [active, setActive] = useState(""); + const debouncedSearch = useDebouncedValue(search); + const update = useAdminProductUpdate(); + + const { data, isLoading } = useAdminProducts({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + is_active: active, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const pendingId = update.isPending ? update.variables?.id : null; + const actBtn = + "inline-flex items-center gap-1 px-2 py-1 rounded-lg text-[12px] font-semibold transition-colors disabled:opacity-40 cursor-pointer"; + + const columns: Column[] = [ + { + header: "محصول", + render: (p) => ( + +
+ {p.primaryImage && ( + + )} +
+

{p.title}

+ + ), + }, + { + header: "فروشگاه", + render: (p) => {p.sellerName || p.sellerUsername || "—"}, + }, + { + header: "دسته", + render: (p) => {p.categoryName || "—"}, + }, + { + header: "قیمت", + render: (p) => {faToman(p.basePrice)}, + }, + { + header: "وضعیت", + render: (p) => + p.isActive ? ( + + ) : ( + + ), + }, + { + header: "تاریخ", + render: (p) => {faDate(p.createdAt)}, + }, + { + header: "عملیات", + className: "text-left", + render: (p) => { + const busy = pendingId === p.id; + if (busy) + return ; + return ( +
+ {p.isActive ? ( + + ) : ( + + )} +
+ ); + }, + }, + ]; + + return ( +
+ محصولات + + reset(() => setSearch(v))} + placeholder="جستجوی عنوان یا فروشگاه…" + /> + reset(() => setActive(v))} + options={[ + { value: "", label: "همه" }, + { value: "true", label: "فعال" }, + { value: "false", label: "غیرفعال" }, + ]} + /> + + + p.id} + /> + +
+ ); +} diff --git a/app/routes/admin.sellers.tsx b/app/routes/admin.sellers.tsx new file mode 100644 index 0000000..ab3be14 --- /dev/null +++ b/app/routes/admin.sellers.tsx @@ -0,0 +1,193 @@ +import type { MetaFunction } from "@remix-run/node"; +import { useState } from "react"; +import { Check, X, BadgeCheck, Loader2 } from "lucide-react"; +import { + useAdminSellers, + useAdminSellerUpdate, +} from "~/requestHandler/use-admin-hooks"; +import type { AdminSellerRow } from "~/requestHandler/use-admin-hooks"; +import SellerLogo from "~/components/SellerLogo"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faNum, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; + +export const meta: MetaFunction = () => [ + { title: "فروشگاه‌ها | پنل مدیریت" }, + { name: "robots", content: "noindex, nofollow" }, +]; + +const PAGE_SIZE = 25; + +const STATUS: Record = { + pending: { label: "در انتظار", tone: "yellow" }, + approved: { label: "تأییدشده", tone: "green" }, + rejected: { label: "ردشده", tone: "red" }, +}; + +export default function AdminSellers() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState(""); + const [verified, setVerified] = useState(""); + const debouncedSearch = useDebouncedValue(search); + const update = useAdminSellerUpdate(); + + const { data, isLoading } = useAdminSellers({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + status, + is_verified: verified, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const pendingId = update.isPending ? update.variables?.id : null; + + const actBtn = + "inline-flex items-center gap-1 px-2 py-1 rounded-lg text-[12px] font-semibold transition-colors disabled:opacity-40 cursor-pointer"; + + const columns: Column[] = [ + { + header: "فروشگاه", + render: (s) => ( +
+ +
+

{s.storeName}

+

@{s.username}

+
+
+ ), + }, + { + header: "تلفن", + render: (s) => ( + + {s.userPhone || "—"} + + ), + }, + { + header: "وضعیت", + render: (s) => { + const st = STATUS[s.status] || { label: s.status, tone: "yellow" as const }; + return ( +
+ + {s.isVerified && } +
+ ); + }, + }, + { + header: "کمیسیون", + render: (s) => {faNum(s.platformFee)}٪, + }, + { + header: "محصولات", + render: (s) => {faNum(s.productCount)}, + }, + { + header: "دنبال‌کننده", + render: (s) => {faNum(s.followerCount)}, + }, + { + header: "تاریخ", + render: (s) => {faDate(s.createdAt)}, + }, + { + header: "عملیات", + className: "text-left", + render: (s) => { + const busy = pendingId === s.id; + if (busy) + return ; + return ( +
+ {s.status !== "approved" && ( + + )} + {s.status !== "rejected" && ( + + )} + +
+ ); + }, + }, + ]; + + return ( +
+ فروشگاه‌ها + + reset(() => setSearch(v))} + placeholder="جستجوی نام یا آیدی فروشگاه…" + /> + reset(() => setStatus(v))} + options={[ + { value: "", label: "همه وضعیت‌ها" }, + { value: "pending", label: "در انتظار" }, + { value: "approved", label: "تأییدشده" }, + { value: "rejected", label: "ردشده" }, + ]} + /> + reset(() => setVerified(v))} + options={[ + { value: "", label: "احراز: همه" }, + { value: "true", label: "احرازشده" }, + { value: "false", label: "احرازنشده" }, + ]} + /> + + + s.id} + /> + +
+ ); +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index 65c63cb..bfc99d1 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -23,10 +23,10 @@ interface NavItem { const NAV: NavItem[] = [ { label: "نمای کلی", to: "/admin", icon: LayoutDashboard, exact: true }, - { label: "سفارش‌ها", to: "/admin/orders", icon: ClipboardList, soon: true }, - { label: "فروشگاه‌ها", to: "/admin/sellers", icon: Store, soon: true }, - { label: "کاربران", to: "/admin/users", icon: Users, soon: true }, - { label: "محصولات", to: "/admin/products", icon: Package, soon: true }, + { label: "سفارش‌ها", to: "/admin/orders", icon: ClipboardList }, + { label: "فروشگاه‌ها", to: "/admin/sellers", icon: Store }, + { label: "کاربران", to: "/admin/users", icon: Users }, + { label: "محصولات", to: "/admin/products", icon: Package }, { label: "دفتر مالی", to: "/admin/ledger", icon: Landmark, soon: true }, ]; diff --git a/app/routes/admin.users.tsx b/app/routes/admin.users.tsx new file mode 100644 index 0000000..2c89fab --- /dev/null +++ b/app/routes/admin.users.tsx @@ -0,0 +1,167 @@ +import type { MetaFunction } from "@remix-run/node"; +import { useState } from "react"; +import { Loader2, Ban, RotateCcw } from "lucide-react"; +import { + useAdminUsers, + useAdminUserUpdate, +} from "~/requestHandler/use-admin-hooks"; +import type { AdminUserRow } from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; + +export const meta: MetaFunction = () => [ + { title: "کاربران | پنل مدیریت" }, + { name: "robots", content: "noindex, nofollow" }, +]; + +const PAGE_SIZE = 25; + +export default function AdminUsers() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [active, setActive] = useState(""); + const [role, setRole] = useState(""); + const debouncedSearch = useDebouncedValue(search); + const update = useAdminUserUpdate(); + + const { data, isLoading } = useAdminUsers({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + is_active: active, + is_seller: role === "seller" ? "true" : undefined, + is_staff: role === "staff" ? "true" : undefined, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const pendingId = update.isPending ? update.variables?.id : null; + const actBtn = + "inline-flex items-center gap-1 px-2 py-1 rounded-lg text-[12px] font-semibold transition-colors disabled:opacity-40 cursor-pointer"; + + const columns: Column[] = [ + { + header: "تلفن", + render: (u) => ( + + {u.phoneNumber} + + ), + }, + { + header: "نام کاربری", + render: (u) => {u.username || "—"}, + }, + { + header: "نقش", + render: (u) => ( +
+ {u.isSuperuser && } + {u.isStaff && !u.isSuperuser && } + {u.isSeller && } + {!u.isSeller && !u.isStaff && !u.isSuperuser && ( + کاربر + )} +
+ ), + }, + { + header: "وضعیت", + render: (u) => + u.isActive ? ( + + ) : ( + + ), + }, + { + header: "عضویت", + render: (u) => {faDate(u.createdAt)}, + }, + { + header: "عملیات", + className: "text-left", + render: (u) => { + const busy = pendingId === u.id; + if (busy) + return ; + return ( +
+ {u.isActive ? ( + + ) : ( + + )} +
+ ); + }, + }, + ]; + + return ( +
+ کاربران + + reset(() => setSearch(v))} + placeholder="جستجوی تلفن، نام کاربری یا ایمیل…" + /> + reset(() => setActive(v))} + options={[ + { value: "", label: "همه" }, + { value: "true", label: "فعال" }, + { value: "false", label: "مسدود" }, + ]} + /> + reset(() => setRole(v))} + options={[ + { value: "", label: "همه نقش‌ها" }, + { value: "seller", label: "فروشنده" }, + { value: "staff", label: "ادمین" }, + ]} + /> + + + u.id} + /> + +
+ ); +}