first commit
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Users,
|
||||
ShoppingBag,
|
||||
Gamepad2,
|
||||
Coins,
|
||||
Trophy,
|
||||
RotateCcw,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import Shell from "@/components/Shell";
|
||||
import StatCard from "@/components/StatCard";
|
||||
import Badge from "@/components/Badge";
|
||||
import RevenueChart from "@/components/RevenueChart";
|
||||
import { fmtNum } from "@/components/NumberInput";
|
||||
import { api } from "@/lib/api";
|
||||
import { KIND_LABEL, KIND_TONE } from "@/lib/labels";
|
||||
|
||||
type Recent = {
|
||||
id: number;
|
||||
mobile: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
product_id: string;
|
||||
product_title: string;
|
||||
price_toman: number;
|
||||
created_local: string;
|
||||
};
|
||||
|
||||
type Dash = {
|
||||
users: number;
|
||||
purchases: number;
|
||||
games: number;
|
||||
coins: number;
|
||||
tournaments: number;
|
||||
season: number;
|
||||
revenue: number;
|
||||
revenue_today: { count: number; revenue: number };
|
||||
revenue_14d: { date: string; revenue: number; count: number }[];
|
||||
recent: Recent[];
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [d, setD] = useState<Dash | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<Dash>("/dashboard")
|
||||
.then(setD)
|
||||
.catch((e) => setErr(e.message));
|
||||
}, []);
|
||||
|
||||
async function resetSeason() {
|
||||
if (!confirm("فصلِ رتبهبندی صفر شود؟ این کار برگشتناپذیر است.")) return;
|
||||
await api.post("/season/reset");
|
||||
const nd = await api.get<Dash>("/dashboard");
|
||||
setD(nd);
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{ label: "درآمد کل", value: d ? fmtNum(d.revenue) : undefined, suffix: "تومان", icon: Wallet, c: "var(--gold)" },
|
||||
{ label: "کاربران", value: d ? fmtNum(d.users) : undefined, icon: Users, c: "#60a5fa" },
|
||||
{ label: "خریدها", value: d ? fmtNum(d.purchases) : undefined, icon: ShoppingBag, c: "#c084fc" },
|
||||
{ label: "بازیها", value: d ? fmtNum(d.games) : undefined, icon: Gamepad2, c: "#4ade80" },
|
||||
{ label: "کل سکهها", value: d ? fmtNum(d.coins) : undefined, icon: Coins, c: "var(--gold)" },
|
||||
{ label: "تورنومنتها", value: d ? fmtNum(d.tournaments) : undefined, icon: Trophy, c: "#fbbf24" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Shell title="داشبورد">
|
||||
{err && <p className="text-red-400 mb-4">{err}</p>}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
{cards.map((c) => (
|
||||
<StatCard
|
||||
key={c.label}
|
||||
label={c.label}
|
||||
value={c.value}
|
||||
suffix={c.suffix}
|
||||
icon={<c.icon size={18} />}
|
||||
color={c.c}
|
||||
loading={!d && !err}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 mt-6">
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<div className="font-bold">درآمدِ ۱۴ روزِ گذشته</div>
|
||||
<div className="text-xs text-[#8ab4e8]">بر اساسِ پرداختهای تأییدشده</div>
|
||||
</div>
|
||||
{d && (
|
||||
<div className="text-sm">
|
||||
<span dir="ltr" className="font-bold text-[var(--gold)]">
|
||||
{fmtNum(d.revenue_today.revenue)}
|
||||
</span>
|
||||
<span className="text-[#8ab4e8] mr-1">تومان امروز</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{d ? (
|
||||
<RevenueChart days={d.revenue_14d} />
|
||||
) : (
|
||||
<div className="skeleton h-28 w-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card mt-6 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 pt-4 pb-2">
|
||||
<div className="font-bold">آخرین خریدها</div>
|
||||
<Link href="/transactions" className="text-xs text-[var(--gold)] hover:underline">
|
||||
همهی تراکنشها
|
||||
</Link>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>کاربر</th>
|
||||
<th>محصول</th>
|
||||
<th>نوع</th>
|
||||
<th>مبلغ</th>
|
||||
<th>زمان (تهران)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{d?.recent.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>
|
||||
<div className="font-bold">{r.name || "—"}</div>
|
||||
<div className="text-[11px] text-[#6f90bd]" dir="ltr">
|
||||
{r.mobile}
|
||||
</div>
|
||||
</td>
|
||||
<td className="font-bold">{r.product_title || r.product_id}</td>
|
||||
<td>
|
||||
<Badge tone={KIND_TONE[r.kind] ?? "gray"}>
|
||||
{KIND_LABEL[r.kind] ?? (r.kind || "نامشخص")}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<span dir="ltr" className="font-bold text-[var(--gold)]">
|
||||
{fmtNum(r.price_toman)}
|
||||
</span>{" "}
|
||||
<span className="text-[11px] text-[#8ab4e8]">تومان</span>
|
||||
</td>
|
||||
<td className="text-[#a9c2e4]" dir="ltr">
|
||||
{r.created_local}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{d && d.recent.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-[#8aa] py-8">
|
||||
هنوز خریدی ثبت نشده
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 mt-6 flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<div className="font-bold">فصل رتبهبندی: {d?.season ?? "—"}</div>
|
||||
<div className="text-sm text-[#8ab4e8]">
|
||||
با ریست، امتیازِ رتبهی همه صفر و فصلِ جدید آغاز میشود.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-danger flex items-center gap-2" onClick={resetSeason}>
|
||||
<RotateCcw size={16} /> ریست فصل
|
||||
</button>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user