Vitron-Front/app/routes/admin.users.tsx
Arda Samadi 935c337df0 feat(admin): monitoring boards UI — orders/sellers/users/products (slice 1)
- 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>
2026-07-25 12:17:22 +03:30

168 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}