Files
ember-market-frontend/components/admin/AdminAnalytics.tsx
2025-03-19 14:06:58 +01:00

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