- /admin opted out of shopper chrome in MyLayout; /admin added to server protectedRoutes; useRequireAdmin() gates to superusers (UserProfile.isSuperuser). - Admin shell (admin.tsx) with sidebar nav (Overview live; other sections stubbed) and a mobile top bar; Overview dashboard (KPI cards + 14-day orders chart) via use-admin-hooks.ts hitting /api/adminpanel/v1/overview/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import type { MetaFunction } from "@remix-run/node";
|
||
import {
|
||
Loader2,
|
||
Wallet,
|
||
Landmark,
|
||
ShoppingBag,
|
||
TrendingUp,
|
||
Users as UsersIcon,
|
||
Store,
|
||
Package,
|
||
Clock,
|
||
} from "lucide-react";
|
||
import { useAdminOverview } from "~/requestHandler/use-admin-hooks";
|
||
import { KpiCard } from "~/components/admin/KpiCard";
|
||
import { OrdersBarChart } from "~/components/admin/OrdersBarChart";
|
||
|
||
export const meta: MetaFunction = () => [
|
||
{ title: "نمای کلی | پنل مدیریت ویترون" },
|
||
{ name: "robots", content: "noindex, nofollow" },
|
||
];
|
||
|
||
const faNum = (n?: number) => Math.round(n || 0).toLocaleString("fa-IR");
|
||
const faToman = (n?: number) => `${faNum(n)} تومان`;
|
||
|
||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<h2 className="text-[15px] font-extrabold mt-7 mb-3 first:mt-0">
|
||
{children}
|
||
</h2>
|
||
);
|
||
}
|
||
|
||
export default function AdminOverview() {
|
||
const { data, isLoading, isError } = useAdminOverview();
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex min-h-[50vh] items-center justify-center">
|
||
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isError || !data) {
|
||
return (
|
||
<div className="flex min-h-[40vh] items-center justify-center">
|
||
<p className="text-RED text-[14px]">خطا در بارگذاری آمار</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const { users, sellers, products, orders, money, ordersSeries } = data;
|
||
|
||
return (
|
||
<div>
|
||
<div className="hidden lg:flex items-end justify-between mb-6">
|
||
<h1 className="text-[28px] font-extrabold">نمای کلی</h1>
|
||
<span className="text-[12px] text-GRAY">بهروزرسانی خودکار هر دقیقه</span>
|
||
</div>
|
||
|
||
{/* Money + today */}
|
||
<SectionTitle>امروز و مالی</SectionTitle>
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||
<KpiCard
|
||
label="درآمد امروز"
|
||
value={faToman(orders.revenueToday)}
|
||
icon={TrendingUp}
|
||
accent="green"
|
||
/>
|
||
<KpiCard
|
||
label="سفارش امروز"
|
||
value={faNum(orders.ordersToday)}
|
||
icon={ShoppingBag}
|
||
/>
|
||
<KpiCard
|
||
label="کل درآمد (پرداختشده)"
|
||
value={faToman(orders.revenueTotal)}
|
||
sub={`${faNum(orders.paidCount)} سفارش پرداختشده`}
|
||
icon={TrendingUp}
|
||
/>
|
||
<KpiCard
|
||
label="برداشتهای در انتظار"
|
||
value={faNum(money.payoutsPendingCount)}
|
||
sub={faToman(money.payoutsPendingAmount)}
|
||
icon={Clock}
|
||
accent={money.payoutsPendingCount > 0 ? "red" : "default"}
|
||
/>
|
||
<KpiCard
|
||
label="موجودی کیف پول کاربران"
|
||
value={faToman(money.walletHeld)}
|
||
icon={Wallet}
|
||
/>
|
||
<KpiCard
|
||
label="مانده قابل پرداخت فروشندگان"
|
||
value={faToman(money.sellerPayable)}
|
||
icon={Landmark}
|
||
/>
|
||
</div>
|
||
|
||
{/* Chart */}
|
||
<div className="mt-4">
|
||
<OrdersBarChart data={ordersSeries} />
|
||
</div>
|
||
|
||
{/* Orders status */}
|
||
<SectionTitle>سفارشها</SectionTitle>
|
||
<div className="grid grid-cols-2 lg:grid-cols-6 gap-3">
|
||
<KpiCard label="کل سفارشها" value={faNum(orders.total)} icon={ShoppingBag} />
|
||
<KpiCard label="در انتظار" value={faNum(orders.pending)} />
|
||
<KpiCard label="تأییدشده" value={faNum(orders.confirmed)} />
|
||
<KpiCard label="ارسالشده" value={faNum(orders.shipped)} />
|
||
<KpiCard label="تحویلشده" value={faNum(orders.delivered)} accent="green" />
|
||
<KpiCard label="لغوشده" value={faNum(orders.cancelled)} accent="red" />
|
||
</div>
|
||
|
||
{/* Sellers + Users + Products */}
|
||
<SectionTitle>فروشگاهها و کاربران</SectionTitle>
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||
<KpiCard
|
||
label="فروشگاهها"
|
||
value={faNum(sellers.total)}
|
||
sub={`${faNum(sellers.verified)} تأییدشده`}
|
||
icon={Store}
|
||
/>
|
||
<KpiCard
|
||
label="در انتظار تأیید"
|
||
value={faNum(sellers.pending)}
|
||
icon={Clock}
|
||
accent={sellers.pending > 0 ? "red" : "default"}
|
||
/>
|
||
<KpiCard
|
||
label="کاربران"
|
||
value={faNum(users.total)}
|
||
sub={`${faNum(users.active)} فعال`}
|
||
icon={UsersIcon}
|
||
/>
|
||
<KpiCard
|
||
label="کاربران جدید (۳۰ روز)"
|
||
value={faNum(users.new30d)}
|
||
sub={`${faNum(users.new7d)} در ۷ روز`}
|
||
icon={UsersIcon}
|
||
/>
|
||
<KpiCard
|
||
label="محصولات"
|
||
value={faNum(products.total)}
|
||
sub={`${faNum(products.active)} فعال`}
|
||
icon={Package}
|
||
/>
|
||
<KpiCard label="فروشندگان" value={faNum(users.sellers)} icon={Store} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|