"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 (
); } 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 (

{title}

{value}

{sparkData && sparklineColor && } ); } // ─── Skeleton Loader ───────────────────────────────────── function DashboardSkeleton() { return (
{Array.from({ length: 10 }).map((_, i) => ( ))}
{Array.from({ length: 4 }).map((_, i) => ( ))}
); } // ─── 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 (
{ChartIcon && } {title}
{children}
); } // ─── Main Component ────────────────────────────────────── export function DashboardPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [autoRefresh, setAutoRefresh] = useState(false); const [lastUpdated, setLastUpdated] = useState(Date.now()); const [refreshing, setRefreshing] = useState(false); const autoRefreshRef = useRef | 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 ; if (error) { return (

{error}

); } 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 (
{/* ── Page Title ── */}

Overview of your Telegram Shop

Last updated: {secondsAgo < 1 ? "just now" : `${secondsAgo} seconds ago`}
{/* ── KPI Cards ── */}
{kpis.map((kpi) => ( ))}
{/* ── Charts Grid ── */}
{/* 1. Revenue 7 days */} {revenue7Data.length > 0 ? ( ) : (
No data
)}
{/* 2. Revenue 30 days */} {revenue30Data.length > 0 ? ( ) : (
No data
)}
{/* 3. New Users 7 days */} {users7Data.length > 0 ? ( ) : (
No data
)}
{/* 4. Top 5 Products */} {productsData.length > 0 ? ( { if (name === "qty") return [value, "Quantity"]; return [formatCurrency(value), "Revenue"]; }} /> ) : (
No data
)}
{/* 5. Top 5 Spenders */} {spendersData.length > 0 ? ( [formatCurrency(value), "Spent"]} /> ) : (
No data
)}
{/* 6. Revenue by Category (Pie/Donut) */} {revenueByCategory.length > 0 ? ( `${name} ${(percent * 100).toFixed(0)}%` } labelLine={true} fontSize={11} > {revenueByCategory.map((_, index) => ( ))} [formatCurrency(value), "Revenue"]} /> ) : (
No data
)}
{/* 7. Purchase Status Distribution */} `${name} ${(percent * 100).toFixed(0)}%` } labelLine={true} fontSize={11} > [value, "Purchases"]} />
{/* ── Analytics Cards: Revenue Trend + User Funnel ── */}
{/* Card A: Revenue Trend (30-day area chart) */}
Revenue Trend
{revenue30Data.length > 0 ? ( `$${v}`} /> [formatCurrency(value), "Revenue"]} /> ) : (
No data
)}
{/* Card B: Conversion Funnel (horizontal bar) */}
User Funnel
{/* ── Recent Purchases Table ── */} Recent Purchases {data.recentPurchases.length > 0 ? (
{data.recentPurchases.map((p, i) => { const badge = statusBadge(p.status); return ( ); })}
Product User Amount Status Date
{p.productName} {p.username} {formatCurrency(p.totalPrice)} {badge.label} {relativeTime(p.purchaseDate)}
) : (
No recent purchases
)}
{/* ── Bottom Section: Wallet Summary + Wallet Chart ── */}
{/* Wallet Summary Table */} Wallet Summary {walletSummary.length > 0 ? (
{walletSummary.map((w) => ( ))}
Type Count Balance USD (mock)
{w.walletType} {w.count} {formatCrypto(w.totalBalance)} {formatCurrency(w.totalBalanceUsd)}
) : (
No wallet data
)}
{/* Wallet Count by Type Chart */} {walletChartData.length > 0 ? ( [value, "Wallets"]} /> ) : (
No wallet data
)}
{/* ── Activity Feed (full width) ── */}
); }