937 lines
38 KiB
TypeScript
937 lines
38 KiB
TypeScript
"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>
|
|
);
|
|
}
|