first commit

This commit is contained in:
2026-08-07 09:40:16 +03:30
commit 0c51b30059
37 changed files with 5146 additions and 0 deletions
+180
View File
@@ -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>
);
}