Vitron-Front/app/routes/admin.products.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

161 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { 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<AdminProductRow>[] = [
{
header: "محصول",
render: (p) => (
<Link
to={`/product/${p.id}`}
className="flex items-center gap-2 min-w-[180px] hover:opacity-80"
>
<div className="h-10 w-10 rounded-lg bg-WHITE3 overflow-hidden shrink-0">
{p.primaryImage && (
<img src={p.primaryImage} alt="" className="h-full w-full object-cover" />
)}
</div>
<p className="font-semibold truncate max-w-[220px]">{p.title}</p>
</Link>
),
},
{
header: "فروشگاه",
render: (p) => <span>{p.sellerName || p.sellerUsername || "—"}</span>,
},
{
header: "دسته",
render: (p) => <span className="text-GRAY">{p.categoryName || "—"}</span>,
},
{
header: "قیمت",
render: (p) => <span className="tabular-nums whitespace-nowrap">{faToman(p.basePrice)}</span>,
},
{
header: "وضعیت",
render: (p) =>
p.isActive ? (
<StatusBadge label="فعال" tone="green" />
) : (
<StatusBadge label="غیرفعال" tone="gray" />
),
},
{
header: "تاریخ",
render: (p) => <span className="text-GRAY whitespace-nowrap">{faDate(p.createdAt)}</span>,
},
{
header: "عملیات",
className: "text-left",
render: (p) => {
const busy = pendingId === p.id;
if (busy)
return <Loader2 size={16} className="animate-spin text-GRAY inline" />;
return (
<div className="flex justify-end">
{p.isActive ? (
<button
className={`${actBtn} bg-WHITE2 text-BLACK2 hover:bg-WHITE3`}
onClick={() => update.mutate({ id: p.id, data: { isActive: false } })}
>
<EyeOff size={13} /> غیرفعال
</button>
) : (
<button
className={`${actBtn} bg-GREEN/10 text-GREEN hover:bg-GREEN/20`}
onClick={() => update.mutate({ id: p.id, data: { isActive: true } })}
>
<Eye 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: "غیرفعال" },
]}
/>
</AdminToolbar>
<AdminTable
columns={columns}
rows={data?.results || []}
isLoading={isLoading}
getRowKey={(p) => p.id}
/>
<AdminPagination
page={page}
pageSize={PAGE_SIZE}
count={data?.count || 0}
onPageChange={setPage}
/>
</div>
);
}