- 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 <noreply@anthropic.com>
168 lines
4.8 KiB
TypeScript
168 lines
4.8 KiB
TypeScript
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<AdminUserRow>[] = [
|
||
{
|
||
header: "تلفن",
|
||
render: (u) => (
|
||
<span className="tabular-nums font-semibold" dir="ltr">
|
||
{u.phoneNumber}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
header: "نام کاربری",
|
||
render: (u) => <span>{u.username || "—"}</span>,
|
||
},
|
||
{
|
||
header: "نقش",
|
||
render: (u) => (
|
||
<div className="flex items-center gap-1 flex-wrap">
|
||
{u.isSuperuser && <StatusBadge label="سوپرادمین" tone="red" />}
|
||
{u.isStaff && !u.isSuperuser && <StatusBadge label="ادمین" tone="blue" />}
|
||
{u.isSeller && <StatusBadge label="فروشنده" tone="gray" />}
|
||
{!u.isSeller && !u.isStaff && !u.isSuperuser && (
|
||
<span className="text-GRAY text-[12px]">کاربر</span>
|
||
)}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
header: "وضعیت",
|
||
render: (u) =>
|
||
u.isActive ? (
|
||
<StatusBadge label="فعال" tone="green" />
|
||
) : (
|
||
<StatusBadge label="مسدود" tone="red" />
|
||
),
|
||
},
|
||
{
|
||
header: "عضویت",
|
||
render: (u) => <span className="text-GRAY whitespace-nowrap">{faDate(u.createdAt)}</span>,
|
||
},
|
||
{
|
||
header: "عملیات",
|
||
className: "text-left",
|
||
render: (u) => {
|
||
const busy = pendingId === u.id;
|
||
if (busy)
|
||
return <Loader2 size={16} className="animate-spin text-GRAY inline" />;
|
||
return (
|
||
<div className="flex justify-end">
|
||
{u.isActive ? (
|
||
<button
|
||
className={`${actBtn} bg-RED/10 text-RED hover:bg-RED/20`}
|
||
onClick={() => update.mutate({ id: u.id, data: { isActive: false } })}
|
||
>
|
||
<Ban size={13} /> مسدود
|
||
</button>
|
||
) : (
|
||
<button
|
||
className={`${actBtn} bg-GREEN/10 text-GREEN hover:bg-GREEN/20`}
|
||
onClick={() => update.mutate({ id: u.id, data: { isActive: true } })}
|
||
>
|
||
<RotateCcw size={13} /> فعالسازی
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<AdminPageTitle>کاربران</AdminPageTitle>
|
||
<AdminToolbar>
|
||
<AdminSearch
|
||
value={search}
|
||
onChange={(v) => reset(() => setSearch(v))}
|
||
placeholder="جستجوی تلفن، نام کاربری یا ایمیل…"
|
||
/>
|
||
<AdminSelect
|
||
value={active}
|
||
onChange={(v) => reset(() => setActive(v))}
|
||
options={[
|
||
{ value: "", label: "همه" },
|
||
{ value: "true", label: "فعال" },
|
||
{ value: "false", label: "مسدود" },
|
||
]}
|
||
/>
|
||
<AdminSelect
|
||
value={role}
|
||
onChange={(v) => reset(() => setRole(v))}
|
||
options={[
|
||
{ value: "", label: "همه نقشها" },
|
||
{ value: "seller", label: "فروشنده" },
|
||
{ value: "staff", label: "ادمین" },
|
||
]}
|
||
/>
|
||
</AdminToolbar>
|
||
|
||
<AdminTable
|
||
columns={columns}
|
||
rows={data?.results || []}
|
||
isLoading={isLoading}
|
||
getRowKey={(u) => u.id}
|
||
/>
|
||
<AdminPagination
|
||
page={page}
|
||
pageSize={PAGE_SIZE}
|
||
count={data?.count || 0}
|
||
onPageChange={setPage}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|