Files
ember-market-frontend/components/admin/AdminAnalytics.tsx
g 4b0bd2cf8c Revamp admin dashboard analytics and UI
Refactored the admin dashboard to use tabbed navigation for analytics and management. Enhanced AdminAnalytics with Recharts visualizations, added top vendors by revenue, and improved chart tooltips. Removed unused columns from vendor table. Updated layout and notification context to exclude admin pages from dashboard-specific UI and notifications. Minor debug logging added to SystemStatusCard.
2025-11-28 19:08:40 +00:00

637 lines
26 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";
import { fetchClient } from "@/lib/api-client";
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts';
import { formatGBP } from "@/utils/format";
// API response data structure
interface AnalyticsData {
vendors?: {
total?: number;
newToday?: number;
newThisWeek?: number;
activeToday?: number;
active?: number;
stores?: number;
activeStores?: number;
dailyGrowth?: { date: string; count: number }[];
topVendors?: Array<{
vendorId: string;
vendorName: string;
totalRevenue: number;
orderCount: 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?: {
totalChats?: number;
activeChats?: number;
totalMessages?: 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 [showDebug, setShowDebug] = useState(false);
const fetchAnalyticsData = async () => {
try {
setLoading(true);
setErrorMessage(null);
const data = await fetchClient<AnalyticsData>(`/admin/analytics?range=${dateRange}`);
console.log('=== ADMIN ANALYTICS DATA ===');
console.log('Date Range:', dateRange);
console.log('Full Response:', JSON.stringify(data, null, 2));
console.log('Orders:', {
total: data?.orders?.total,
dailyOrders: data?.orders?.dailyOrders,
dailyOrdersLength: data?.orders?.dailyOrders?.length,
sample: data?.orders?.dailyOrders?.slice(0, 3)
});
console.log('Revenue:', {
total: data?.revenue?.total,
dailyRevenue: data?.revenue?.dailyRevenue,
dailyRevenueLength: data?.revenue?.dailyRevenue?.length,
sample: data?.revenue?.dailyRevenue?.slice(0, 3)
});
console.log('Vendors:', {
total: data?.vendors?.total,
dailyGrowth: data?.vendors?.dailyGrowth,
dailyGrowthLength: data?.vendors?.dailyGrowth?.length,
sample: data?.vendors?.dailyGrowth?.slice(0, 3)
});
console.log('===========================');
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>
);
}
// Helper to transform data for recharts
const transformChartData = (data: Array<{ date: string; [key: string]: any }>, valueKey: string = "count") => {
if (!data || data.length === 0) return [];
return data.map(item => {
const dateStr = item.date;
// Parse YYYY-MM-DD format
const parts = dateStr.split('-');
const date = parts.length === 3
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
: new Date(dateStr);
return {
date: dateStr,
formattedDate: date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }),
value: Number(item[valueKey]) || 0,
[valueKey]: Number(item[valueKey]) || 0
};
});
};
// Helper to combine orders and revenue data for dual-axis chart
const combineOrdersAndRevenue = (orders: Array<{ date: string; count: number }>, revenue: Array<{ date: string; amount: number }>) => {
if (!orders || orders.length === 0) return [];
// Create a map of revenue by date for quick lookup
const revenueMap = new Map<string, number>();
if (revenue && revenue.length > 0) {
revenue.forEach(r => {
revenueMap.set(r.date, r.amount || 0);
});
}
return orders.map(order => {
const dateStr = order.date;
const parts = dateStr.split('-');
const date = parts.length === 3
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
: new Date(dateStr);
return {
date: dateStr,
formattedDate: date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }),
orders: order.count || 0,
revenue: revenueMap.get(dateStr) || 0
};
});
};
// Custom tooltip for charts
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
const dataKey = payload[0].dataKey;
const isDualAxis = data.orders !== undefined && data.revenue !== undefined;
// Determine if this is a currency amount or a count
// transformChartData creates both 'value' and the original key (count/amount)
// So we check the original key to determine the type
const isAmount = dataKey === 'amount' || dataKey === 'revenue' ||
(dataKey === 'value' && data.amount !== undefined && data.count === undefined);
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="text-sm font-medium mb-2">{data.formattedDate || label}</p>
{isDualAxis ? (
<div className="space-y-1">
<p className="text-sm text-blue-600">
Orders: <span className="font-semibold">{data.orders}</span>
</p>
<p className="text-sm text-green-600">
Revenue: <span className="font-semibold">{formatGBP(data.revenue)}</span>
</p>
</div>
) : (
<p className="text-sm text-primary">
{isAmount ? formatGBP(data.value || data.amount || 0) : `${data.value || data.count || 0}`}
</p>
)}
</div>
);
}
return null;
};
// 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-GB', {
style: 'currency',
currency: 'GBP',
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>
<Button
variant="outline"
size="sm"
onClick={() => setShowDebug(!showDebug)}
>
{showDebug ? 'Hide' : 'Show'} Debug
</Button>
</div>
</div>
{showDebug && analyticsData && (
<Card className="mt-4">
<CardHeader>
<CardTitle>Debug: Raw Data</CardTitle>
<CardDescription>Date Range: {dateRange}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4 text-xs font-mono">
<div>
<div className="font-semibold mb-2">Orders:</div>
<div className="pl-4 space-y-1">
<div>Total: {analyticsData?.orders?.total || 'N/A'}</div>
<div>Today: {analyticsData?.orders?.totalToday || 'N/A'}</div>
<div>Daily Orders Array Length: {analyticsData?.orders?.dailyOrders?.length || 0}</div>
<div>First 3 Daily Orders:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
{JSON.stringify(analyticsData?.orders?.dailyOrders?.slice(0, 3), null, 2)}
</pre>
</div>
</div>
<div>
<div className="font-semibold mb-2">Revenue:</div>
<div className="pl-4 space-y-1">
<div>Total: {analyticsData?.revenue?.total || 'N/A'}</div>
<div>Today: {analyticsData?.revenue?.today || 'N/A'}</div>
<div>Daily Revenue Array Length: {analyticsData?.revenue?.dailyRevenue?.length || 0}</div>
<div>First 3 Daily Revenue:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
{JSON.stringify(analyticsData?.revenue?.dailyRevenue?.slice(0, 3), null, 2)}
</pre>
</div>
</div>
<div>
<div className="font-semibold mb-2">Vendors:</div>
<div className="pl-4 space-y-1">
<div>Total: {analyticsData?.vendors?.total || 'N/A'}</div>
<div>Daily Growth Array Length: {analyticsData?.vendors?.dailyGrowth?.length || 0}</div>
<div>First 3 Daily Growth:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
{JSON.stringify(analyticsData?.vendors?.dailyGrowth?.slice(0, 3), null, 2)}
</pre>
</div>
</div>
<details className="mt-4">
<summary className="font-semibold cursor-pointer">Full JSON Response</summary>
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
{JSON.stringify(analyticsData, null, 2)}
</pre>
</details>
</div>
</CardContent>
</Card>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl: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 && analyticsData.orders.dailyOrders.length > 0 ? (
<div className="mt-3 h-12">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={transformChartData(analyticsData.orders.dailyOrders, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">No chart data available</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 && analyticsData.revenue.dailyRevenue.length > 0 ? (
<div className="mt-3 h-12">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={transformChartData(analyticsData.revenue.dailyRevenue, 'amount')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">No chart data available</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>Active: {analyticsData?.vendors?.active || 0}</span>
<span className="ml-2">Stores: {analyticsData?.vendors?.activeStores || 0}</span>
</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 && analyticsData.vendors.dailyGrowth.length > 0 ? (
<div className="mt-3 h-12">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">No chart data available</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="vendors">Vendors</TabsTrigger>
</TabsList>
<TabsContent value="orders" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Order Trends</CardTitle>
<CardDescription>
Daily order volume and revenue processed over the selected time period
</CardDescription>
</CardHeader>
<CardContent>
{analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={combineOrdersAndRevenue(
analyticsData.orders.dailyOrders,
analyticsData.revenue?.dailyRevenue || []
)}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="formattedDate"
tick={{ fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
yAxisId="left"
tick={{ fontSize: 12 }}
label={{ value: 'Orders', angle: -90, position: 'insideLeft' }}
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
tickFormatter={(value) => `£${(value / 1000).toFixed(0)}k`}
label={{ value: 'Revenue', angle: 90, position: 'insideRight' }}
/>
<Tooltip content={<CustomTooltip />} />
<Bar yAxisId="left" dataKey="orders" fill="#3b82f6" radius={[2, 2, 0, 0]} name="Orders" />
<Line yAxisId="right" type="monotone" dataKey="revenue" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 4 }} name="Revenue" />
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
No order data available for the selected time period
</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="vendors" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Vendor Growth</CardTitle>
<CardDescription>
New vendor registrations over time
</CardDescription>
</CardHeader>
<CardContent>
{analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="formattedDate"
tick={{ fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
No vendor data available for the selected time period
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-4 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">Active Vendors</div>
<div className="text-2xl font-bold">{analyticsData?.vendors?.active?.toLocaleString() || '0'}</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<div className="text-sm font-medium mb-1">Active Stores</div>
<div className="text-2xl font-bold">{analyticsData?.vendors?.activeStores?.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>
{/* Top Vendors by Revenue */}
{analyticsData?.vendors?.topVendors && analyticsData.vendors.topVendors.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-4">Top Vendors by Revenue</h3>
<div className="space-y-2">
{analyticsData.vendors.topVendors.map((vendor, index) => (
<div key={vendor.vendorId} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
{index + 1}
</div>
<div>
<div className="font-medium">{vendor.vendorName}</div>
<div className="text-xs text-muted-foreground">{vendor.orderCount} orders</div>
</div>
</div>
<div className="text-right">
<div className="font-semibold text-green-600">{formatCurrency(vendor.totalRevenue)}</div>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}