Vitron-Front/app/routes/admin._index.tsx
Arda Samadi 0360780411 feat(admin): admin-panel foundation shell + overview (slice 0)
- /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>
2026-07-25 12:00:49 +03:30

154 lines
5.0 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 {
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>
);
}