feat(admin): integrate Next.js admin panel (admin-next/) from feat/nextjs-admin

- Add admin-next/: full Next.js + Prisma + shadcn admin panel (45 API routes, 76 components)
- docker-compose: tg_shop_admin service (port 3000, shared db/shop.db, prisma), bot talks to it via ADMIN_CHAT_URL=http://tg_shop_admin:3000/api/chat
- tor-proxy now proxies onion admin to tg_shop_admin:3000 (new panel)
- chatbotService: ADMIN_CHAT_URL default = http://tg_shop_admin:3000/api/chat (removed localhost:3100 anachronism)
- .env admin secrets gitignored (admin-next/.env)
This commit is contained in:
NW
2026-08-08 01:31:45 +01:00
parent 62534dbe85
commit a4a5fd449d
161 changed files with 25681 additions and 5 deletions

View File

@@ -0,0 +1,909 @@
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { ActivityFeed } from "@/components/layout/activity-feed";
import {
Users,
Package,
ShoppingCart,
DollarSign,
TrendingUp,
Percent,
CheckCircle,
Clock,
XCircle,
Tag,
RefreshCw,
ShieldBan,
Wallet,
ArrowRight,
} from "lucide-react";
import {
ResponsiveContainer,
AreaChart,
Area,
BarChart,
Bar,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
// Chart colors
const CHART_1 = "#f97316";
const CHART_2 = "#06b6d4";
const CHART_3 = "#8b5cf6";
const CHART_4 = "#eab308";
const CHART_5 = "#ec4899";
const PIE_COLORS = [CHART_1, CHART_2, CHART_3, CHART_4, CHART_5];
// ─── Types ───────────────────────────────────────────────
interface RecentPurchase {
username: string;
productName: string;
totalPrice: number;
status: string;
purchaseDate: string;
}
interface DashboardStats {
totalUsers: number;
totalProducts: number;
totalPurchases: number;
totalRevenue: number;
totalSubcategories: number;
aov: number;
conversionRate: number;
completedPurchases: number;
pendingPurchases: number;
cancelledPurchases: number;
bannedUsers: number;
activeWallets: number;
}
interface ChartData {
days: string[];
revenueData: number[];
usersData: number[];
days30: string[];
revenueData30: number[];
}
interface TopProduct {
name: string;
qty: number;
revenue: number;
}
interface TopSpender {
username: string;
spent: number;
}
interface RevenueByCategory {
name: string;
value: number;
}
interface TopCountry {
country: string;
productCount: number;
}
interface WalletSummary {
walletType: string;
count: number;
totalBalance: number;
totalBalanceUsd: number;
}
interface DashboardData {
stats: DashboardStats;
chartData: ChartData;
topProducts: TopProduct[];
topSpenders: TopSpender[];
revenueByCategory: RevenueByCategory[];
topCountries: TopCountry[];
walletSummary: WalletSummary[];
recentPurchases: RecentPurchase[];
}
// ─── Helpers ─────────────────────────────────────────────
function formatCurrency(val: number): string {
return `$${val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function relativeTime(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diffMs = now - then;
const diffMin = Math.floor(diffMs / 60000);
const diffHr = Math.floor(diffMs / 3600000);
const diffDay = Math.floor(diffMs / 86400000);
if (diffMin < 1) return 'just now';
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHr < 24) return `${diffHr}h ago`;
if (diffDay < 7) return `${diffDay}d ago`;
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function statusBadge(status: string): { label: string; cls: string } {
switch (status) {
case 'completed':
return { label: 'Completed', cls: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' };
case 'pending':
return { label: 'Pending', cls: 'bg-yellow-500/15 text-yellow-600 dark:text-yellow-400' };
case 'cancelled':
return { label: 'Cancelled', cls: 'bg-red-500/15 text-red-600 dark:text-red-400' };
default:
return { label: status, cls: 'bg-muted text-muted-foreground' };
}
}
function formatCrypto(val: number): string {
return val.toFixed(8);
}
function shortDate(dateStr: string): string {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
// ─── Mini Sparkline ─────────────────────────────────────
function MiniSparkline({ data, color }: { data: number[]; color: string }) {
if (data.length < 2) return null;
const chartData = data.map((v, i) => ({ i, v }));
return (
<div className="absolute bottom-0 left-0 right-0 h-12 opacity-20 pointer-events-none">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<YAxis domain={["dataMin - 2", "dataMax + 2"]} hide />
<Area type="monotone" dataKey="v" stroke={color} fill={color} strokeWidth={1.5} />
</AreaChart>
</ResponsiveContainer>
</div>
);
}
function generateSparkData(value: number, points: number = 8): number[] {
const data: number[] = [];
let current = value * 0.6;
for (let i = 0; i < points; i++) {
current += (value - current) * (0.2 + Math.random() * 0.3);
data.push(Math.round(current * 10) / 10);
}
return data;
}
// ─── KPI Card ────────────────────────────────────────────
function KpiCard({
title,
value,
icon: Icon,
color,
sparklineColor,
sparklineValue,
}: {
title: string;
value: string;
icon: React.ComponentType<{ className?: string }>;
color: string;
sparklineColor?: string;
sparklineValue?: number;
}) {
const sparkData = sparklineValue !== undefined ? generateSparkData(sparklineValue) : undefined;
return (
<Card className="card-hover kpi-shimmer border-l-4 transition-transform hover:scale-[1.02] relative overflow-hidden" style={{ borderLeftColor: color }}>
<div
className="h-[2px] w-full rounded-t-lg"
style={{
background: `linear-gradient(to right, ${color}, ${color}66, transparent)`,
}}
/>
<CardContent className="p-4 flex items-center gap-4">
<div
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${color}15` }}
>
<Icon className="h-6 w-6" style={{ color }} />
</div>
<div className="min-w-0">
<p className="text-sm text-muted-foreground truncate">{title}</p>
<p className="text-xl font-bold truncate tabular-nums stat-value count-up">{value}</p>
</div>
</CardContent>
{sparkData && sparklineColor && <MiniSparkline data={sparkData} color={sparklineColor} />}
</Card>
);
}
// ─── Skeleton Loader ─────────────────────────────────────
function DashboardSkeleton() {
return (
<div className="p-4 md:p-6 space-y-6">
<Skeleton className="h-8 w-48" />
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
{Array.from({ length: 10 }).map((_, i) => (
<Skeleton key={i} className="h-24 rounded-xl" />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-72 rounded-xl" />
))}
</div>
</div>
);
}
// ─── Chart Card wrapper ──────────────────────────────────
function ChartCard({
title,
children,
accentColor,
icon: ChartIcon,
}: {
title: string;
children: React.ReactNode;
accentColor?: string;
icon?: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
}) {
return (
<Card className="card-hover overflow-hidden">
<div
className="h-1 w-full"
style={{
background: `linear-gradient(to right, ${accentColor ?? CHART_1}, ${accentColor ?? CHART_1}44, transparent)`,
}}
/>
<CardHeader className="p-4 pb-0">
<CardTitle className="text-sm font-medium flex items-center gap-2">
{ChartIcon && <ChartIcon className="size-4" style={{ color: accentColor ?? CHART_1 }} />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-2">
<div className="h-72">{children}</div>
</CardContent>
</Card>
);
}
// ─── Main Component ──────────────────────────────────────
export function DashboardPage() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [autoRefresh, setAutoRefresh] = useState(false);
const [lastUpdated, setLastUpdated] = useState<number>(Date.now());
const [refreshing, setRefreshing] = useState(false);
const autoRefreshRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fetchDashboard = useCallback(async () => {
try {
setRefreshing(true);
setError(null);
const res = await fetch("/api/stats/dashboard");
if (!res.ok) {
throw new Error("Failed to load dashboard data");
}
const json = await res.json();
setData(json);
setLastUpdated(Date.now());
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
fetchDashboard();
}, [fetchDashboard]);
// Auto-refresh toggle
useEffect(() => {
if (autoRefresh) {
autoRefreshRef.current = setInterval(fetchDashboard, 30000);
}
return () => {
if (autoRefreshRef.current) clearInterval(autoRefreshRef.current);
};
}, [autoRefresh, fetchDashboard]);
// "X seconds ago" ticker
const [secondsAgo, setSecondsAgo] = useState(0);
useEffect(() => {
const tick = setInterval(() => {
setSecondsAgo(Math.floor((Date.now() - lastUpdated) / 1000));
}, 1000);
return () => clearInterval(tick);
}, [lastUpdated]);
if (loading) return <DashboardSkeleton />;
if (error) {
return (
<div className="p-6">
<Card className="border-destructive">
<CardContent className="p-6">
<p className="text-destructive font-medium">{error}</p>
</CardContent>
</Card>
</div>
);
}
if (!data) return null;
const { stats, chartData, topProducts, topSpenders, revenueByCategory, walletSummary, recentPurchases } = data;
// Prepare chart datasets
const revenue7Data = chartData.days.map((day, i) => ({
date: shortDate(day),
revenue: chartData.revenueData[i],
}));
const revenue30Data = chartData.days30.map((day, i) => ({
date: shortDate(day),
revenue: chartData.revenueData30[i],
}));
const users7Data = chartData.days.map((day, i) => ({
date: shortDate(day),
users: chartData.usersData[i],
}));
const productsData = [...topProducts].reverse(); // reverse for horizontal bar
const spendersData = [...topSpenders].reverse();
const walletChartData = walletSummary.map((w) => ({
name: w.walletType,
count: w.count,
}));
// KPI definitions
const kpis = [
{ title: "Total Users", value: stats.totalUsers.toLocaleString(), icon: Users, color: CHART_1, sparklineColor: "#64748b", sparklineValue: stats.totalUsers },
{ title: "Total Products", value: stats.totalProducts.toLocaleString(), icon: Package, color: CHART_2, sparklineColor: "#64748b", sparklineValue: stats.totalProducts },
{ title: "Total Purchases", value: stats.totalPurchases.toLocaleString(), icon: ShoppingCart, color: CHART_3, sparklineColor: "#64748b", sparklineValue: stats.totalPurchases },
{ title: "Pending", value: stats.pendingPurchases.toLocaleString(), icon: Clock, color: "#eab308", sparklineColor: "#eab308", sparklineValue: stats.pendingPurchases },
{ title: "Total Revenue", value: formatCurrency(stats.totalRevenue), icon: DollarSign, color: "#22c55e", sparklineColor: "#22c55e", sparklineValue: stats.totalRevenue },
{ title: "Avg Order Value", value: formatCurrency(stats.aov), icon: TrendingUp, color: CHART_1, sparklineColor: "#64748b", sparklineValue: stats.aov },
{ title: "Conversion Rate", value: `${stats.conversionRate.toFixed(1)}%`, icon: Percent, color: CHART_5, sparklineColor: "#64748b", sparklineValue: stats.conversionRate },
{ title: "Completed", value: stats.completedPurchases.toLocaleString(), icon: CheckCircle, color: "#22c55e", sparklineColor: "#64748b", sparklineValue: stats.completedPurchases },
{ title: "Cancelled", value: stats.cancelledPurchases.toLocaleString(), icon: XCircle, color: "#ef4444", sparklineColor: "#64748b", sparklineValue: stats.cancelledPurchases },
{ title: "Banned Users", value: stats.bannedUsers.toLocaleString(), icon: ShieldBan, color: "#ef4444", sparklineColor: "#ef4444", sparklineValue: stats.bannedUsers },
{ title: "Active Wallets", value: stats.activeWallets.toLocaleString(), icon: Wallet, color: CHART_2, sparklineColor: "#06b6d4", sparklineValue: stats.activeWallets },
{ title: "Subcategories", value: stats.totalSubcategories.toLocaleString(), icon: Tag, color: CHART_2, sparklineColor: "#64748b", sparklineValue: stats.totalSubcategories },
];
return (
<div className="space-y-6 page-enter">
{/* ── Page Title ── */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-muted-foreground">Overview of your Telegram Shop</p>
<div className="flex items-center gap-4">
<span className="text-xs text-muted-foreground">
Last updated: {secondsAgo < 1 ? "just now" : `${secondsAgo} seconds ago`}
</span>
<button
type="button"
onClick={fetchDashboard}
disabled={refreshing}
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-accent transition-colors disabled:opacity-50"
aria-label="Refresh dashboard"
>
<RefreshCw className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
</button>
<div className="flex items-center gap-2">
<Switch
id="auto-refresh"
checked={autoRefresh}
onCheckedChange={setAutoRefresh}
/>
<label
htmlFor="auto-refresh"
className="text-xs text-muted-foreground cursor-pointer select-none"
>
Auto-refresh
</label>
</div>
</div>
</div>
{/* ── KPI Cards ── */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{kpis.map((kpi) => (
<KpiCard key={kpi.title} {...kpi} icon={kpi.icon} />
))}
</div>
<Separator />
{/* ── Charts Grid ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 1. Revenue 7 days */}
<ChartCard title="Revenue — Last 7 Days" accentColor={CHART_1} icon={TrendingUp}>
{revenue7Data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={revenue7Data}>
<defs>
<linearGradient id="rev7grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_1} stopOpacity={0.3} />
<stop offset="95%" stopColor={CHART_1} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
/>
<Area
type="monotone"
dataKey="revenue"
stroke={CHART_1}
fill="url(#rev7grad)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 2. Revenue 30 days */}
<ChartCard title="Revenue — Last 30 Days" accentColor={CHART_2} icon={TrendingUp}>
{revenue30Data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={revenue30Data}>
<defs>
<linearGradient id="rev30grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_2} stopOpacity={0.3} />
<stop offset="95%" stopColor={CHART_2} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="hsl(var(--muted-foreground))" interval={4} />
<YAxis tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
/>
<Area
type="monotone"
dataKey="revenue"
stroke={CHART_2}
fill="url(#rev30grad)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 3. New Users 7 days */}
<ChartCard title="New Users — Last 7 Days" accentColor={CHART_3} icon={Users}>
{users7Data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={users7Data}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis allowDecimals={false} tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
/>
<Bar dataKey="users" fill={CHART_3} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 4. Top 5 Products */}
<ChartCard title="Top 5 Products by Quantity Sold" accentColor={CHART_4} icon={Package}>
{productsData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={productsData} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" horizontal={false} />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis type="category" dataKey="name" width={120} tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number, name: string) => {
if (name === "qty") return [value, "Quantity"];
return [formatCurrency(value), "Revenue"];
}}
/>
<Bar dataKey="qty" fill={CHART_4} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 5. Top 5 Spenders */}
<ChartCard title="Top 5 Spenders" accentColor={CHART_5} icon={DollarSign}>
{spendersData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={spendersData} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" horizontal={false} />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis type="category" dataKey="username" width={120} tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [formatCurrency(value), "Spent"]}
/>
<Bar dataKey="spent" fill={CHART_5} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 6. Revenue by Category (Pie/Donut) */}
<ChartCard title="Revenue by Category" accentColor={CHART_1} icon={Tag}>
{revenueByCategory.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={revenueByCategory}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={90}
paddingAngle={2}
dataKey="value"
nameKey="name"
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
labelLine={true}
fontSize={11}
>
{revenueByCategory.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={PIE_COLORS[index % PIE_COLORS.length]}
/>
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [formatCurrency(value), "Revenue"]}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</ChartCard>
{/* 7. Purchase Status Distribution */}
<ChartCard title="Purchase Status Distribution" accentColor="#eab308" icon={ShoppingCart}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={[
{ name: 'Pending', value: stats.pendingPurchases },
{ name: 'Completed', value: stats.completedPurchases },
{ name: 'Cancelled', value: stats.cancelledPurchases },
]}
cx="50%"
cy="50%"
innerRadius={55}
outerRadius={90}
paddingAngle={3}
dataKey="value"
nameKey="name"
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
labelLine={true}
fontSize={11}
>
<Cell fill="#eab308" />
<Cell fill="#10b981" />
<Cell fill="#ef4444" />
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [value, "Purchases"]}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
</ChartCard>
</div>
{/* ── Analytics Cards: Revenue Trend + User Funnel ── */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Card A: Revenue Trend (30-day area chart) */}
<Card className="card-hover overflow-hidden md:col-span-2">
<div
className="h-1 w-full"
style={{
background: `linear-gradient(to right, ${CHART_2}, ${CHART_2}44, transparent)`,
}}
/>
<CardHeader className="p-4 pb-0 flex flex-row items-center gap-2">
<TrendingUp className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">Revenue Trend</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-2">
<div className="h-80">
{revenue30Data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={revenue30Data}>
<defs>
<linearGradient id="revTrendGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_2} stopOpacity={0.4} />
<stop offset="95%" stopColor={CHART_2} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="hsl(var(--muted-foreground))" interval={4} />
<YAxis tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v: number) => `$${v}`} />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [formatCurrency(value), "Revenue"]}
/>
<Area
type="monotone"
dataKey="revenue"
stroke={CHART_2}
fill="url(#revTrendGrad)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No data</div>
)}
</div>
</CardContent>
</Card>
{/* Card B: Conversion Funnel (horizontal bar) */}
<Card className="card-hover overflow-hidden md:col-span-1">
<div
className="h-1 w-full"
style={{
background: `linear-gradient(to right, #64748b, #64748b44, transparent)`,
}}
/>
<CardHeader className="p-4 pb-0 flex flex-row items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">User Funnel</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-2">
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={[
{ name: "Total Users", value: stats.totalUsers },
{ name: "Users with Purchases", value: stats.totalPurchases },
{ name: "Users with Wallets", value: stats.activeWallets },
]}
layout="vertical"
margin={{ left: 10, right: 20 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" horizontal={false} />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis
type="category"
dataKey="name"
width={120}
tick={{ fontSize: 11 }}
stroke="hsl(var(--muted-foreground))"
/>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
/>
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
<Cell fill="#64748b" />
<Cell fill="#10b981" />
<Cell fill="#06b6d4" />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
<Separator />
{/* ── Recent Purchases Table ── */}
<Card className="card-hover">
<CardHeader className="p-4 pb-0 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Recent Purchases</CardTitle>
<button
type="button"
onClick={() => { window.location.hash = '#/purchases'; }}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
View all
<ArrowRight className="h-3 w-3" />
</button>
</CardHeader>
<CardContent className="p-4">
{data.recentPurchases.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm alternate-rows table-header-gradient">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-2 font-medium text-muted-foreground text-xs">Product</th>
<th className="text-left py-2 px-2 font-medium text-muted-foreground text-xs">User</th>
<th className="text-right py-2 px-2 font-medium text-muted-foreground text-xs">Amount</th>
<th className="text-center py-2 px-2 font-medium text-muted-foreground text-xs">Status</th>
<th className="text-right py-2 px-2 font-medium text-muted-foreground text-xs">Date</th>
</tr>
</thead>
<tbody>
{data.recentPurchases.map((p, i) => {
const badge = statusBadge(p.status);
return (
<tr key={i} className="border-b last:border-0">
<td className="py-2 px-2 text-xs font-medium truncate max-w-[140px]">{p.productName}</td>
<td className="py-2 px-2 text-xs text-muted-foreground truncate max-w-[100px]">{p.username}</td>
<td className="py-2 px-2 text-xs text-right font-mono tabular-nums">{formatCurrency(p.totalPrice)}</td>
<td className="py-2 px-2 text-center">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap ${badge.cls}`}>
{badge.label}
</span>
</td>
<td className="py-2 px-2 text-xs text-right text-muted-foreground whitespace-nowrap">{relativeTime(p.purchaseDate)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<div className="h-24 flex items-center justify-center text-muted-foreground text-sm">No recent purchases</div>
)}
</CardContent>
</Card>
<Separator />
{/* ── Bottom Section: Wallet Summary + Wallet Chart ── */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Wallet Summary Table */}
<Card className="card-hover">
<CardHeader className="p-4 pb-0">
<CardTitle className="text-sm font-medium">Wallet Summary</CardTitle>
</CardHeader>
<CardContent className="p-4">
{walletSummary.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-3 font-medium text-muted-foreground">Type</th>
<th className="text-right py-2 px-3 font-medium text-muted-foreground">Count</th>
<th className="text-right py-2 px-3 font-medium text-muted-foreground">Balance</th>
<th className="text-right py-2 px-3 font-medium text-muted-foreground">USD (mock)</th>
</tr>
</thead>
<tbody>
{walletSummary.map((w) => (
<tr key={w.walletType} className="border-b last:border-0">
<td className="py-2 px-3 font-medium">{w.walletType}</td>
<td className="py-2 px-3 text-right text-muted-foreground">{w.count}</td>
<td className="py-2 px-3 text-right font-mono text-xs">{formatCrypto(w.totalBalance)}</td>
<td className="py-2 px-3 text-right">{formatCurrency(w.totalBalanceUsd)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="h-48 flex items-center justify-center text-muted-foreground text-sm">No wallet data</div>
)}
</CardContent>
</Card>
{/* Wallet Count by Type Chart */}
<ChartCard title="Wallet Count by Type" accentColor={CHART_2} icon={Wallet}>
{walletChartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={walletChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="name" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<YAxis allowDecimals={false} tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [value, "Wallets"]}
/>
<Bar dataKey="count" fill={CHART_2} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">No wallet data</div>
)}
</ChartCard>
</div>
{/* ── Activity Feed (full width) ── */}
<ActivityFeed />
</div>
);
}