|
|
|
|
@@ -8,6 +8,9 @@ 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 {
|
|
|
|
|
@@ -18,7 +21,14 @@ interface AnalyticsData {
|
|
|
|
|
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;
|
|
|
|
|
@@ -35,8 +45,9 @@ interface AnalyticsData {
|
|
|
|
|
dailyRevenue?: { date: string; amount: number }[];
|
|
|
|
|
};
|
|
|
|
|
engagement?: {
|
|
|
|
|
totalMessages?: number;
|
|
|
|
|
totalChats?: number;
|
|
|
|
|
activeChats?: number;
|
|
|
|
|
totalMessages?: number;
|
|
|
|
|
dailyMessages?: { date: string; count: number }[];
|
|
|
|
|
};
|
|
|
|
|
products?: {
|
|
|
|
|
@@ -59,29 +70,36 @@ export default function AdminAnalytics() {
|
|
|
|
|
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 token = document.cookie
|
|
|
|
|
.split("; ")
|
|
|
|
|
.find((row) => row.startsWith("Authorization="))
|
|
|
|
|
?.split("=")[1];
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/admin/analytics?range=${dateRange}`, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token}`,
|
|
|
|
|
},
|
|
|
|
|
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)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error("Failed to fetch analytics data");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
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);
|
|
|
|
|
@@ -109,64 +127,89 @@ export default function AdminAnalytics() {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
// 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="w-full flex items-center justify-center text-muted-foreground" style={{ height: `${height}px` }}>
|
|
|
|
|
No data available
|
|
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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>
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Trend indicator component for metric cards
|
|
|
|
|
@@ -189,9 +232,9 @@ export default function AdminAnalytics() {
|
|
|
|
|
|
|
|
|
|
// Format currency
|
|
|
|
|
const formatCurrency = (value: number) => {
|
|
|
|
|
return new Intl.NumberFormat('en-US', {
|
|
|
|
|
return new Intl.NumberFormat('en-GB', {
|
|
|
|
|
style: 'currency',
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
currency: 'GBP',
|
|
|
|
|
maximumFractionDigits: 0
|
|
|
|
|
}).format(value);
|
|
|
|
|
};
|
|
|
|
|
@@ -237,9 +280,74 @@ export default function AdminAnalytics() {
|
|
|
|
|
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>
|
|
|
|
|
@@ -261,13 +369,17 @@ export default function AdminAnalytics() {
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{analyticsData?.orders?.dailyOrders && (
|
|
|
|
|
<div className="mt-3 h-10">
|
|
|
|
|
<Chart
|
|
|
|
|
data={analyticsData.orders.dailyOrders}
|
|
|
|
|
height={40}
|
|
|
|
|
/>
|
|
|
|
|
{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>
|
|
|
|
|
@@ -292,15 +404,17 @@ export default function AdminAnalytics() {
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{analyticsData?.revenue?.dailyRevenue && (
|
|
|
|
|
<div className="mt-3 h-10">
|
|
|
|
|
<Chart
|
|
|
|
|
data={analyticsData.revenue.dailyRevenue}
|
|
|
|
|
valueKey="amount"
|
|
|
|
|
color="#10b981"
|
|
|
|
|
height={40}
|
|
|
|
|
/>
|
|
|
|
|
{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>
|
|
|
|
|
@@ -317,6 +431,10 @@ export default function AdminAnalytics() {
|
|
|
|
|
<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
|
|
|
|
|
@@ -325,14 +443,17 @@ export default function AdminAnalytics() {
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{analyticsData?.vendors?.dailyGrowth && (
|
|
|
|
|
<div className="mt-3 h-10">
|
|
|
|
|
<Chart
|
|
|
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
|
|
|
color="#8b5cf6"
|
|
|
|
|
height={40}
|
|
|
|
|
/>
|
|
|
|
|
{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>
|
|
|
|
|
@@ -359,9 +480,7 @@ export default function AdminAnalytics() {
|
|
|
|
|
<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">
|
|
|
|
|
@@ -369,18 +488,49 @@ export default function AdminAnalytics() {
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Order Trends</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Daily order volume over the selected time period
|
|
|
|
|
Daily order volume and revenue processed over the selected time period
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{analyticsData?.orders?.dailyOrders ? (
|
|
|
|
|
<Chart
|
|
|
|
|
data={analyticsData.orders.dailyOrders}
|
|
|
|
|
height={300}
|
|
|
|
|
/>
|
|
|
|
|
{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
|
|
|
|
|
No order data available for the selected time period
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
@@ -402,46 +552,6 @@ export default function AdminAnalytics() {
|
|
|
|
|
</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>
|
|
|
|
|
@@ -451,71 +561,73 @@ export default function AdminAnalytics() {
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{analyticsData?.vendors?.dailyGrowth ? (
|
|
|
|
|
<Chart
|
|
|
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
|
|
|
color="#8b5cf6"
|
|
|
|
|
height={300}
|
|
|
|
|
/>
|
|
|
|
|
{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
|
|
|
|
|
No vendor data available for the selected time period
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
|
|
|
<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">New Today</div>
|
|
|
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.newToday?.toLocaleString() || '0'}</div>
|
|
|
|
|
<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>
|
|
|
|
|
</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
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|