"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 (
{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 (
);
}
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 ? (
| Product |
User |
Amount |
Status |
Date |
{data.recentPurchases.map((p, i) => {
const badge = statusBadge(p.status);
return (
| {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 ? (
| Type |
Count |
Balance |
USD (mock) |
{walletSummary.map((w) => (
| {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) ── */}
);
}