HMM
This commit is contained in:
937
components/admin/AdminAnalytics.tsx
Normal file
937
components/admin/AdminAnalytics.tsx
Normal file
@@ -0,0 +1,937 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { AlertCircle, BarChart as BarChartIcon, LineChart, RefreshCw, Users, ShoppingCart,
|
||||
TrendingUp, TrendingDown, DollarSign, MessageSquare, Clock, Landmark } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, Legend, XAxis, YAxis, BarChart, Bar } from "recharts";
|
||||
|
||||
// API response data structure
|
||||
interface AnalyticsData {
|
||||
vendors?: {
|
||||
total?: number;
|
||||
newToday?: number;
|
||||
newThisWeek?: number;
|
||||
activeToday?: number;
|
||||
active?: number;
|
||||
stores?: number;
|
||||
dailyGrowth?: { date: string; count: number }[];
|
||||
data?: { date: string; count: number }[];
|
||||
};
|
||||
orders?: {
|
||||
total?: number;
|
||||
totalToday?: number;
|
||||
totalThisWeek?: number;
|
||||
recent?: number;
|
||||
pending?: number;
|
||||
completed?: number;
|
||||
dailyOrders?: { date: string; count: number }[];
|
||||
data?: { date: string; count: number }[];
|
||||
};
|
||||
revenue?: {
|
||||
total?: number;
|
||||
today?: number;
|
||||
thisWeek?: number;
|
||||
dailyRevenue?: { date: string; amount: number }[];
|
||||
};
|
||||
engagement?: {
|
||||
totalMessages?: number;
|
||||
activeChats?: number;
|
||||
avgResponseTime?: number;
|
||||
dailyMessages?: { date: string; count: number }[];
|
||||
};
|
||||
products?: {
|
||||
total?: number;
|
||||
recent?: number;
|
||||
};
|
||||
stores?: {
|
||||
total?: number;
|
||||
active?: number;
|
||||
};
|
||||
sessions?: {
|
||||
total?: number;
|
||||
active?: number;
|
||||
};
|
||||
promotions?: {
|
||||
total?: number;
|
||||
active?: number;
|
||||
used?: number;
|
||||
totalDiscountAmount?: number;
|
||||
discountPercentage?: number;
|
||||
topPromotions?: { _id: string; count: number; totalDiscount: number }[];
|
||||
discountByType?: {
|
||||
percentage?: { count: number; totalDiscount: number };
|
||||
fixed?: { count: number; totalDiscount: number };
|
||||
};
|
||||
};
|
||||
chats?: {
|
||||
totalChats?: number;
|
||||
activeChats?: number;
|
||||
totalMessages?: number;
|
||||
recentMessages?: number;
|
||||
messagesBySender?: { buyer: number; vendor: number };
|
||||
avgResponseTimeMin?: number;
|
||||
messagesByHour?: number[];
|
||||
};
|
||||
telegram?: {
|
||||
totalUsers?: number;
|
||||
totalStoreConnections?: number;
|
||||
avgStoresPerUser?: number;
|
||||
multiStoreUsers?: number;
|
||||
};
|
||||
escrow?: {
|
||||
total?: number;
|
||||
active?: number;
|
||||
released?: number;
|
||||
disputed?: number;
|
||||
disputeRate?: number;
|
||||
totalByCurrency?: { ltc: number; btc: number; xmr: number };
|
||||
heldByCurrency?: { ltc: number; btc: number; xmr: number };
|
||||
avgReleaseTimeHours?: number;
|
||||
};
|
||||
security?: {
|
||||
blockedUsers?: {
|
||||
total?: number;
|
||||
recentlyBlocked?: number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminAnalytics() {
|
||||
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dateRange, setDateRange] = useState("7days");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchAnalyticsData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("Authorization="))
|
||||
?.split("=")[1];
|
||||
|
||||
console.log("Token from cookie:", token ? token.substring(0, 10) + "..." : "not found");
|
||||
console.log("API URL:", `${process.env.NEXT_PUBLIC_API_URL}/admin/analytics?range=${dateRange}`);
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/admin/analytics?range=${dateRange}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Response status:", response.status);
|
||||
console.log("Response ok:", response.ok);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Error response:", errorText);
|
||||
throw new Error(`Failed to fetch analytics data: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Analytics data received:", data);
|
||||
setAnalyticsData(data);
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching analytics data:", error);
|
||||
setErrorMessage(`Failed to load analytics data: ${error.message}`);
|
||||
|
||||
// For demo purposes, load mock data if API fails
|
||||
setAnalyticsData({
|
||||
vendors: {
|
||||
total: 1248,
|
||||
newToday: 24,
|
||||
newThisWeek: 124,
|
||||
activeToday: 356,
|
||||
stores: 100,
|
||||
dailyGrowth: [
|
||||
{ date: "2023-11-01", count: 15 },
|
||||
{ date: "2023-11-02", count: 18 },
|
||||
{ date: "2023-11-03", count: 12 },
|
||||
{ date: "2023-11-04", count: 22 },
|
||||
{ date: "2023-11-05", count: 26 },
|
||||
{ date: "2023-11-06", count: 17 },
|
||||
{ date: "2023-11-07", count: 14 },
|
||||
],
|
||||
},
|
||||
orders: {
|
||||
total: 5672,
|
||||
totalToday: 86,
|
||||
totalThisWeek: 432,
|
||||
pending: 47,
|
||||
completed: 5625,
|
||||
dailyOrders: [
|
||||
{ date: "2023-11-01", count: 54 },
|
||||
{ date: "2023-11-02", count: 62 },
|
||||
{ date: "2023-11-03", count: 58 },
|
||||
{ date: "2023-11-04", count: 71 },
|
||||
{ date: "2023-11-05", count: 68 },
|
||||
{ date: "2023-11-06", count: 65 },
|
||||
{ date: "2023-11-07", count: 54 },
|
||||
],
|
||||
},
|
||||
revenue: {
|
||||
total: 156320,
|
||||
today: 3280,
|
||||
thisWeek: 18642,
|
||||
dailyRevenue: [
|
||||
{ date: "2023-11-01", amount: 2345 },
|
||||
{ date: "2023-11-02", amount: 2762 },
|
||||
{ date: "2023-11-03", amount: 2458 },
|
||||
{ date: "2023-11-04", amount: 3121 },
|
||||
{ date: "2023-11-05", amount: 2968 },
|
||||
{ date: "2023-11-06", amount: 2708 },
|
||||
{ date: "2023-11-07", amount: 2280 },
|
||||
],
|
||||
},
|
||||
engagement: {
|
||||
totalMessages: 32450,
|
||||
activeChats: 123,
|
||||
avgResponseTime: 5.2,
|
||||
dailyMessages: [
|
||||
{ date: "2023-11-01", count: 432 },
|
||||
{ date: "2023-11-02", count: 512 },
|
||||
{ date: "2023-11-03", count: 478 },
|
||||
{ date: "2023-11-04", count: 541 },
|
||||
{ date: "2023-11-05", count: 498 },
|
||||
{ date: "2023-11-06", count: 465 },
|
||||
{ date: "2023-11-07", count: 423 },
|
||||
],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnalyticsData();
|
||||
}, [dateRange]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchAnalyticsData();
|
||||
};
|
||||
|
||||
if (loading && !analyticsData) {
|
||||
return (
|
||||
<div className="flex justify-center my-8">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// These would normally be real chart components
|
||||
// For this example, we'll use simplified representations
|
||||
const SimpleAreaChart = ({ data, label, color }: { data: any[]; label: string; color: string }) => {
|
||||
// Handle empty data or undefined
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-32 flex items-center justify-center text-muted-foreground">
|
||||
No data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-32 relative mt-2">
|
||||
{/* Simplified chart visualization */}
|
||||
<div className="absolute bottom-0 left-0 right-0 top-0 flex items-end">
|
||||
{data.map((item, index) => {
|
||||
// Normalize height between 10% and 90%
|
||||
const values = data.map(d => d.count || d.amount);
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const range = max - min;
|
||||
const value = item.count || item.amount;
|
||||
const normalizedHeight = range === 0
|
||||
? 50
|
||||
: 10 + (((value - min) / range) * 80);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-1 mx-0.5 rounded-t"
|
||||
style={{
|
||||
height: `${normalizedHeight}%`,
|
||||
backgroundColor: color,
|
||||
opacity: 0.7
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-border"></div>
|
||||
<div className="absolute bottom-[-20px] left-0 right-0 flex justify-between text-xs text-muted-foreground">
|
||||
<span>{new Date(data[0].date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}</span>
|
||||
<span>{new Date(data[data.length - 1].date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Function to render trend indicator
|
||||
const TrendIndicator = ({ value, prefix = "", suffix = "" }: { value: number, prefix?: string, suffix?: string }) => {
|
||||
if (value === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center text-xs font-medium ml-1 ${value > 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{value > 0 ? <TrendingUp className="h-3 w-3 mr-1" /> : <TrendingDown className="h-3 w-3 mr-1" />}
|
||||
{prefix}{Math.abs(value).toFixed(1)}{suffix}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Mini sparkline component for metric cards
|
||||
const MiniSparkline = ({ data, color = "#3b82f6" }: { data: any[], color?: string }) => {
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
const values = data.map(item => item.count || item.amount || 0);
|
||||
const max = Math.max(...values, 1);
|
||||
|
||||
return (
|
||||
<div className="flex items-end h-8 space-x-px mt-2">
|
||||
{values.map((value, index) => {
|
||||
const height = (value / max) * 100;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-1 rounded-sm transition-all duration-200"
|
||||
style={{
|
||||
height: `${height}%`,
|
||||
backgroundColor: color,
|
||||
minHeight: '2px',
|
||||
opacity: 0.7 + ((index / values.length) * 0.3) // gradually increase opacity
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold">Platform Analytics</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Select value={dateRange} onValueChange={setDateRange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timeframe" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7days">Last 7 Days</SelectItem>
|
||||
<SelectItem value="30days">Last 30 Days</SelectItem>
|
||||
<SelectItem value="90days">Last 90 Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="orders">
|
||||
<TabsList className="grid grid-cols-6 mb-6">
|
||||
<TabsTrigger value="orders">Orders</TabsTrigger>
|
||||
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
||||
<TabsTrigger value="revenue">Revenue</TabsTrigger>
|
||||
<TabsTrigger value="promotions">Promotions</TabsTrigger>
|
||||
<TabsTrigger value="communication">Communication</TabsTrigger>
|
||||
<TabsTrigger value="payments">Payments</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="orders">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Analytics</CardTitle>
|
||||
<CardDescription>Order volume and revenue metrics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{analyticsData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Total Orders</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.orders?.total?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Orders Today</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.orders?.totalToday?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Pending Orders</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.orders?.pending?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Revenue</div>
|
||||
<div className="text-2xl font-semibold">${analyticsData.revenue?.total?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-4">Order & Revenue Trends</h3>
|
||||
{analyticsData && (
|
||||
<div className="h-[300px] flex items-center justify-center border rounded-md">
|
||||
<p className="text-muted-foreground">Order and revenue charts would render here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vendors">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Vendor Analytics</CardTitle>
|
||||
<CardDescription>Detailed vendor metrics and store statistics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{analyticsData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Total Vendors</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.vendors?.total?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">New Today</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.vendors?.newToday?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">New This Week</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.vendors?.newThisWeek?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Active Stores</div>
|
||||
<div className="text-2xl font-semibold">{analyticsData.vendors?.stores?.toLocaleString() || analyticsData.stores?.total?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-4">Vendor Growth Trend</h3>
|
||||
{analyticsData && (
|
||||
<div className="h-[300px] flex items-center justify-center border rounded-md">
|
||||
<p className="text-muted-foreground">Vendor growth chart would render here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="revenue">
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Revenue Analytics</CardTitle>
|
||||
<CardDescription>Revenue and financial metrics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{analyticsData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="p-4 border rounded-md bg-gradient-to-r from-purple-950/30 to-slate-900/30">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Total Processed</div>
|
||||
<div className="text-2xl font-semibold">${analyticsData.revenue?.total?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md bg-gradient-to-r from-purple-950/30 to-slate-900/30">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Today's Revenue</div>
|
||||
<div className="text-2xl font-semibold">${analyticsData.revenue?.today?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md bg-gradient-to-r from-purple-950/30 to-slate-900/30">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">This Week</div>
|
||||
<div className="text-2xl font-semibold">${analyticsData.revenue?.thisWeek?.toLocaleString() || '0'}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md bg-gradient-to-r from-purple-950/30 to-slate-900/30">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">Average Per Order</div>
|
||||
<div className="text-2xl font-semibold">
|
||||
${analyticsData.revenue && analyticsData.orders?.total
|
||||
? Math.round((analyticsData.revenue.total || 0) / analyticsData.orders.total).toLocaleString()
|
||||
: '0'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-4">Revenue Trend</h3>
|
||||
{analyticsData && (
|
||||
<SimpleAreaChart
|
||||
data={analyticsData.revenue?.dailyRevenue || []}
|
||||
label="Revenue"
|
||||
color="#8b5cf6" // purple
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="promotions">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Active Promotions
|
||||
</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.promotions?.active || 0}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
out of {analyticsData?.promotions?.total || 0} total
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Discount Amount
|
||||
</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
${analyticsData?.promotions?.totalDiscountAmount?.toLocaleString() || 0}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData?.promotions?.discountPercentage || 0}% of revenue
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Promotion Uses
|
||||
</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.promotions?.used || 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Top Promotions</CardTitle>
|
||||
<CardDescription>Most used promotion codes</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{analyticsData?.promotions?.topPromotions?.map((promo, i) => (
|
||||
<div key={i} className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{promo._id}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{promo.count} uses · ${promo.totalDiscount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
${(promo.totalDiscount / promo.count).toFixed(2)}
|
||||
<span className="text-xs text-muted-foreground ml-1">avg</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Discount Types</CardTitle>
|
||||
<CardDescription>Breakdown by discount type</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-[200px]">
|
||||
{analyticsData?.promotions?.discountByType && (
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Percentage Discounts</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData.promotions.discountByType.percentage?.count || 0} uses
|
||||
</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
${analyticsData.promotions.discountByType.percentage?.totalDiscount?.toLocaleString() || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Fixed Amount Discounts</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData.promotions.discountByType.fixed?.count || 0} uses
|
||||
</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
${analyticsData.promotions.discountByType.fixed?.totalDiscount?.toLocaleString() || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="communication">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Chats
|
||||
</CardTitle>
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.chats?.totalChats || 0}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData?.chats?.activeChats || 0} active in selected period
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Messages
|
||||
</CardTitle>
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.chats?.totalMessages?.toLocaleString() || 0}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData?.chats?.recentMessages?.toLocaleString() || 0} in selected period
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Avg Response Time
|
||||
</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.chats?.avgResponseTimeMin || 0} min
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Message Volume by Hour</CardTitle>
|
||||
<CardDescription>Chat activity distribution</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[200px]">
|
||||
{analyticsData?.chats?.messagesByHour && (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={analyticsData.chats.messagesByHour.map((count, hour) => ({ hour, count }))}>
|
||||
<XAxis dataKey="hour" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="count" fill="#6366f1" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Message Breakdown</CardTitle>
|
||||
<CardDescription>By sender type</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[200px] flex items-center justify-center">
|
||||
{analyticsData?.chats?.messagesBySender && (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={[
|
||||
{ name: 'Buyer', value: analyticsData.chats.messagesBySender.buyer || 0 },
|
||||
{ name: 'Vendor', value: analyticsData.chats.messagesBySender.vendor || 0 }
|
||||
]}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
label
|
||||
>
|
||||
<Cell fill="#6366f1" />
|
||||
<Cell fill="#22c55e" />
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Telegram Users</CardTitle>
|
||||
<CardDescription>Bot user statistics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Total Users</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.telegram?.totalUsers || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Store Connections</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.telegram?.totalStoreConnections || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Avg Stores per User</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.telegram?.avgStoresPerUser?.toFixed(2) || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Multi-Store Users</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.telegram?.multiStoreUsers || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Security</CardTitle>
|
||||
<CardDescription>Blocked users and security metrics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Total Blocked Users</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.security?.blockedUsers?.total || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Recently Blocked</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
In selected period
|
||||
</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.security?.blockedUsers?.recentlyBlocked || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="payments">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Active Escrows
|
||||
</CardTitle>
|
||||
<Landmark className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.escrow?.active || 0}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
out of {analyticsData?.escrow?.total || 0} total
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Released Escrows
|
||||
</CardTitle>
|
||||
<Landmark className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.escrow?.released || 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Dispute Rate
|
||||
</CardTitle>
|
||||
<Landmark className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsData?.escrow?.disputeRate?.toFixed(1) || 0}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{analyticsData?.escrow?.disputed || 0} disputed escrows
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Escrow by Currency</CardTitle>
|
||||
<CardDescription>Currently held amounts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{analyticsData?.escrow?.heldByCurrency && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Bitcoin (BTC)</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData.escrow.heldByCurrency.btc.toFixed(4)} BTC
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Litecoin (LTC)</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData.escrow.heldByCurrency.ltc.toFixed(2)} LTC
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Monero (XMR)</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData.escrow.heldByCurrency.xmr.toFixed(2)} XMR
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-medium">Escrow Metrics</CardTitle>
|
||||
<CardDescription>Release time and amounts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Avg Release Time</p>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{analyticsData?.escrow?.avgReleaseTimeHours ? (
|
||||
<>
|
||||
{Math.floor(analyticsData.escrow.avgReleaseTimeHours / 24)} days, {analyticsData.escrow.avgReleaseTimeHours % 24} hours
|
||||
</>
|
||||
) : (
|
||||
"8 days, 0 hours"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{analyticsData?.escrow?.totalByCurrency && (
|
||||
<div className="pt-4">
|
||||
<p className="text-sm font-medium mb-2">Total Processed (lifetime)</p>
|
||||
<div className="space-y-2 pl-2">
|
||||
<div className="text-xs flex justify-between">
|
||||
<span>BTC:</span>
|
||||
<span>{analyticsData.escrow.totalByCurrency.btc.toFixed(4)} BTC</span>
|
||||
</div>
|
||||
<div className="text-xs flex justify-between">
|
||||
<span>LTC:</span>
|
||||
<span>{analyticsData.escrow.totalByCurrency.ltc.toFixed(2)} LTC</span>
|
||||
</div>
|
||||
<div className="text-xs flex justify-between">
|
||||
<span>XMR:</span>
|
||||
<span>{analyticsData.escrow.totalByCurrency.xmr.toFixed(2)} XMR</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user