525 lines
20 KiB
TypeScript
525 lines
20 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Button } from "@/components/ui/button";
|
|
import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart,
|
|
TrendingUp, TrendingDown, DollarSign, Package } from "lucide-react";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
|
|
// API response data structure
|
|
interface AnalyticsData {
|
|
vendors?: {
|
|
total?: number;
|
|
newToday?: number;
|
|
newThisWeek?: number;
|
|
activeToday?: number;
|
|
active?: number;
|
|
stores?: number;
|
|
dailyGrowth?: { date: string; count: number }[];
|
|
};
|
|
orders?: {
|
|
total?: number;
|
|
totalToday?: number;
|
|
totalThisWeek?: number;
|
|
pending?: number;
|
|
completed?: number;
|
|
dailyOrders?: { date: string; count: number }[];
|
|
};
|
|
revenue?: {
|
|
total?: number;
|
|
today?: number;
|
|
thisWeek?: number;
|
|
dailyRevenue?: { date: string; amount: number }[];
|
|
};
|
|
engagement?: {
|
|
totalMessages?: number;
|
|
activeChats?: number;
|
|
dailyMessages?: { date: string; count: number }[];
|
|
};
|
|
products?: {
|
|
total?: number;
|
|
recent?: number;
|
|
};
|
|
stores?: {
|
|
total?: number;
|
|
active?: number;
|
|
};
|
|
sessions?: {
|
|
total?: number;
|
|
active?: 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];
|
|
|
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/admin/analytics?range=${dateRange}`, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch analytics data");
|
|
}
|
|
|
|
const data = await response.json();
|
|
setAnalyticsData(data);
|
|
} catch (error) {
|
|
console.error("Error fetching analytics data:", error);
|
|
setErrorMessage("Failed to load analytics data. Please try again.");
|
|
} 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>
|
|
);
|
|
}
|
|
|
|
// Chart component for line/area charts
|
|
const Chart = ({ data, valueKey = "count", color = "#3b82f6", height = 200 }:
|
|
{ data: any[]; valueKey?: string; color?: string; height?: number }) => {
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<div className="w-full flex items-center justify-center text-muted-foreground" style={{ height: `${height}px` }}>
|
|
No data available
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Find min and max for scaling
|
|
const values = data.map(d => d[valueKey] || 0);
|
|
const max = Math.max(...values, 1);
|
|
const min = Math.min(...values, 0);
|
|
const range = max - min || 1;
|
|
|
|
return (
|
|
<div className="w-full relative" style={{ height: `${height}px` }}>
|
|
<div className="absolute inset-0 flex items-end">
|
|
{data.map((item, index) => {
|
|
const value = item[valueKey] || 0;
|
|
const normalizedHeight = ((value - min) / range) * 90 + 10; // Scale to 10%-100%
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="group flex-1 relative"
|
|
>
|
|
<div
|
|
className="w-full rounded-t transition-all duration-200"
|
|
style={{
|
|
height: `${normalizedHeight}%`,
|
|
backgroundColor: color,
|
|
opacity: 0.7
|
|
}}
|
|
/>
|
|
<div className="absolute bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 bg-background text-xs text-center rounded p-1 transform -translate-y-full pointer-events-none z-10 transition-opacity">
|
|
{valueKey === "amount" ?
|
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) :
|
|
value}
|
|
<div className="text-[10px] text-muted-foreground">
|
|
{new Date(item.date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-border"></div>
|
|
|
|
{/* X-axis labels */}
|
|
<div className="absolute bottom-[-24px] 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>
|
|
);
|
|
};
|
|
|
|
// Trend indicator component for metric cards
|
|
const TrendIndicator = ({ current, previous }: { current: number, previous: number }) => {
|
|
if (!current || !previous) return null;
|
|
|
|
const percentChange = ((current - previous) / previous) * 100;
|
|
|
|
if (Math.abs(percentChange) < 0.1) return null;
|
|
|
|
return (
|
|
<div className={`flex items-center text-xs font-medium ${percentChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
|
{percentChange >= 0 ?
|
|
<TrendingUp className="h-3 w-3 mr-1" /> :
|
|
<TrendingDown className="h-3 w-3 mr-1" />}
|
|
{Math.abs(percentChange).toFixed(1)}%
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Format currency
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: 0
|
|
}).format(value);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{errorMessage && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Error</AlertTitle>
|
|
<AlertDescription>{errorMessage}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Dashboard Analytics</h2>
|
|
<p className="text-muted-foreground">Overview of your marketplace performance</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={dateRange}
|
|
onValueChange={setDateRange}
|
|
>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue placeholder="Last 7 days" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
|
<SelectItem value="7days">Last 7 days</SelectItem>
|
|
<SelectItem value="30days">Last 30 days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Orders Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
|
|
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.orders?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>Today: {analyticsData?.orders?.totalToday || 0}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.orders?.totalToday || 0}
|
|
previous={(analyticsData?.orders?.total || 0) / 30} // Rough estimate
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.orders?.dailyOrders && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.orders.dailyOrders}
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Revenue Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{formatCurrency(analyticsData?.revenue?.total || 0)}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>Today: {formatCurrency(analyticsData?.revenue?.today || 0)}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.revenue?.today || 0}
|
|
previous={(analyticsData?.revenue?.total || 0) / 30} // Rough estimate
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.revenue?.dailyRevenue && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.revenue.dailyRevenue}
|
|
valueKey="amount"
|
|
color="#10b981"
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Vendors Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Vendors</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.vendors?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.vendors?.newToday || 0}
|
|
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7} // Average per day
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.vendors?.dailyGrowth && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
color="#8b5cf6"
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Products Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Products</CardTitle>
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.products?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>New This Week: {analyticsData?.products?.recent || 0}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Tabs defaultValue="orders" className="mt-6">
|
|
<TabsList>
|
|
<TabsTrigger value="orders">Orders</TabsTrigger>
|
|
<TabsTrigger value="revenue">Revenue</TabsTrigger>
|
|
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
|
<TabsTrigger value="engagement">Engagement</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="orders" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Order Trends</CardTitle>
|
|
<CardDescription>
|
|
Daily order volume over the selected time period
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.orders?.dailyOrders ? (
|
|
<Chart
|
|
data={analyticsData.orders.dailyOrders}
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No order data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Orders</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.orders?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Pending Orders</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.orders?.pending?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Completed Orders</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.orders?.completed?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="revenue" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Revenue Trends</CardTitle>
|
|
<CardDescription>
|
|
Daily revenue over the selected time period
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.revenue?.dailyRevenue ? (
|
|
<Chart
|
|
data={analyticsData.revenue.dailyRevenue}
|
|
valueKey="amount"
|
|
color="#10b981"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No revenue data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Revenue</div>
|
|
<div className="text-2xl font-bold">{formatCurrency(analyticsData?.revenue?.total || 0)}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Today's Revenue</div>
|
|
<div className="text-2xl font-bold">{formatCurrency(analyticsData?.revenue?.today || 0)}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">This Week's Revenue</div>
|
|
<div className="text-2xl font-bold">{formatCurrency(analyticsData?.revenue?.thisWeek || 0)}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="vendors" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Vendor Growth</CardTitle>
|
|
<CardDescription>
|
|
New vendor registrations over time
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.vendors?.dailyGrowth ? (
|
|
<Chart
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
color="#8b5cf6"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No vendor data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Vendors</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">New Today</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.newToday?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">New This Week</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.newThisWeek?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="engagement" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>User Engagement</CardTitle>
|
|
<CardDescription>
|
|
Chat and message activity
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.engagement?.dailyMessages ? (
|
|
<Chart
|
|
data={analyticsData.engagement.dailyMessages}
|
|
color="#ec4899"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No engagement data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Messages</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.engagement?.totalMessages?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Active Chats</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.engagement?.activeChats?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Sessions</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.sessions?.active?.toLocaleString() || '0'} / {analyticsData?.sessions?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|