Files
ember-market-frontend/components/admin/AdminAnalytics.tsx
2026-01-06 17:59:30 +00:00

1558 lines
56 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,
Trophy,
} 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";
import { PieChart, Pie, Cell, Legend } from "recharts";
interface GrowthData {
launchDate: string;
generatedAt: string;
daily: Array<{
date: string;
orders: number;
revenue: number;
customers: number;
avgOrderValue: number;
}>;
monthly: Array<{
month: string;
orders: number;
revenue: number;
customers: number;
avgOrderValue: number;
newVendors: number;
newCustomers: number;
}>;
customers: {
total: number;
segments: {
new: number;
returning: number;
loyal: number;
vip: number;
};
segmentDetails: {
[key: string]: {
count: number;
totalRevenue: number;
avgOrderCount: number;
avgSpent: number;
};
};
segmentPercentages: {
new: number;
returning: number;
loyal: number;
vip: number;
};
};
cumulative: {
orders: number;
revenue: number;
customers: number;
vendors: number;
products: number;
avgOrderValue: number;
};
}
interface AnalyticsData {
meta?: {
year: number;
currentYear: number;
availableYears: number[];
range: string;
isCurrentYear: boolean;
};
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;
orders?: number;
avgOrderValue?: 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 [selectedYear, setSelectedYear] = useState<number>(
new Date().getFullYear(),
);
const [availableYears, setAvailableYears] = useState<number[]>([
new Date().getFullYear(),
]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const [growthData, setGrowthData] = useState<GrowthData | null>(null);
const [growthLoading, setGrowthLoading] = useState(false);
const currentYear = new Date().getFullYear();
// Segment colors for pie chart
const SEGMENT_COLORS = {
new: "#3b82f6", // blue
returning: "#10b981", // green
loyal: "#f59e0b", // amber
vip: "#8b5cf6", // purple
};
const isViewingCurrentYear = selectedYear === currentYear;
const fetchAnalyticsData = async () => {
try {
setLoading(true);
setErrorMessage(null);
// Build query params - include year if not current year
const params = new URLSearchParams();
params.set("range", dateRange);
if (selectedYear !== currentYear) {
params.set("year", selectedYear.toString());
}
const data = await fetchClient<AnalyticsData>(
`/admin/analytics?${params.toString()}`,
);
setAnalyticsData(data);
// Update available years from response if provided
if (data.meta?.availableYears && data.meta.availableYears.length > 0) {
setAvailableYears(data.meta.availableYears);
}
} 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, selectedYear]);
// Fetch growth data (cached, since launch)
const fetchGrowthData = async (forceRefresh = false) => {
try {
setGrowthLoading(true);
const url = forceRefresh ? "/admin/growth?refresh=true" : "/admin/growth";
const data = await fetchClient<GrowthData>(url);
setGrowthData(data);
} catch (error) {
console.error("Error fetching growth data:", error);
} finally {
setGrowthLoading(false);
}
};
// Fetch growth data on mount
useEffect(() => {
fetchGrowthData();
}, []);
const handleRefresh = () => {
setRefreshing(true);
fetchAnalyticsData();
};
const handleGrowthRefresh = () => {
fetchGrowthData(true);
};
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);
// Format with day of week: "Mon, Nov 21"
const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" });
const monthDay = date.toLocaleDateString("en-GB", {
month: "short",
day: "numeric",
});
return {
date: dateStr,
formattedDate: `${dayOfWeek}, ${monthDay}`,
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;
orders?: number;
avgOrderValue?: number;
}>,
) => {
if (!orders || orders.length === 0) return [];
// Create a map of revenue and AOV by date for quick lookup
const revenueMap = new Map<
string,
{ amount: number; avgOrderValue: number }
>();
if (revenue && revenue.length > 0) {
revenue.forEach((r) => {
revenueMap.set(r.date, {
amount: r.amount || 0,
avgOrderValue: r.avgOrderValue || 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);
// Format with day of week: "Mon, Nov 21"
const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" });
const monthDay = date.toLocaleDateString("en-GB", {
month: "short",
day: "numeric",
});
const revenueData = revenueMap.get(dateStr) || {
amount: 0,
avgOrderValue: 0,
};
return {
date: dateStr,
formattedDate: `${dayOfWeek}, ${monthDay}`,
orders: order.count || 0,
revenue: revenueData.amount,
avgOrderValue: revenueData.avgOrderValue,
};
});
};
// 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>
{data.avgOrderValue !== undefined && data.avgOrderValue > 0 && (
<p className="text-sm text-purple-600">
Avg Order Value:{" "}
<span className="font-semibold">
{formatGBP(data.avgOrderValue)}
</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);
};
// Calculate best month from daily data (for YTD, full year, or previous years)
const calculateBestMonth = () => {
// Show best month for YTD, full year view, or when viewing previous years
const showBestMonth =
dateRange === "ytd" || dateRange === "year" || !isViewingCurrentYear;
if (
!showBestMonth ||
!analyticsData?.revenue?.dailyRevenue ||
analyticsData.revenue.dailyRevenue.length === 0
) {
return null;
}
// Group daily revenue by month
const monthlyTotals = new Map<
string,
{ revenue: number; orders: number }
>();
analyticsData.revenue.dailyRevenue.forEach((day) => {
const date = new Date(day.date);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
monthlyTotals.set(monthKey, {
revenue: current.revenue + (day.amount || 0),
orders: current.orders,
});
});
// Also group orders by month
if (analyticsData.orders?.dailyOrders) {
analyticsData.orders.dailyOrders.forEach((day) => {
const date = new Date(day.date);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
const current = monthlyTotals.get(monthKey) || {
revenue: 0,
orders: 0,
};
monthlyTotals.set(monthKey, {
revenue: current.revenue,
orders: current.orders + (day.count || 0),
});
});
}
// Find the month with highest revenue
let bestMonth: { month: string; revenue: number; orders: number } | null =
null;
monthlyTotals.forEach((totals, monthKey) => {
if (!bestMonth || totals.revenue > bestMonth.revenue) {
const [year, month] = monthKey.split("-");
const monthName = new Date(
parseInt(year),
parseInt(month) - 1,
1,
).toLocaleDateString("en-GB", { month: "long", year: "numeric" });
bestMonth = {
month: monthName,
revenue: totals.revenue,
orders: totals.orders,
};
}
});
return bestMonth;
};
const bestMonth = calculateBestMonth();
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
{!isViewingCurrentYear && (
<span className="ml-2 text-lg font-normal text-muted-foreground">
({selectedYear})
</span>
)}
</h2>
<p className="text-muted-foreground">
{isViewingCurrentYear
? "Overview of your marketplace performance"
: `Historical data for ${selectedYear}`}
</p>
</div>
<div className="flex items-center gap-2">
{/* Year selector */}
<Select
value={selectedYear.toString()}
onValueChange={(value) => setSelectedYear(parseInt(value, 10))}
>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{availableYears.map((year) => (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Date range selector - only show options for current year */}
<Select
value={isViewingCurrentYear ? dateRange : "year"}
onValueChange={setDateRange}
disabled={!isViewingCurrentYear}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Last 7 days" />
</SelectTrigger>
<SelectContent>
{isViewingCurrentYear ? (
<>
<SelectItem value="24hours">Last 24 hours</SelectItem>
<SelectItem value="7days">Last 7 days</SelectItem>
<SelectItem value="30days">Last 30 days</SelectItem>
<SelectItem value="ytd">Year to Date</SelectItem>
<SelectItem value="year">Full Year</SelectItem>
</>
) : (
<SelectItem value="year">Full Year</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>
)}
{/* Best Month Card (show for YTD, full year, or previous years) */}
{bestMonth && (
<Card className="border-green-500/50 bg-green-500/5">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-green-500/20">
<Trophy className="h-5 w-5 text-green-600" />
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">
Best Month{" "}
{!isViewingCurrentYear
? `of ${selectedYear}`
: dateRange === "year"
? "(Full Year)"
: "(YTD)"}
</div>
<div className="text-xl font-bold text-green-600">
{bestMonth.month}
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-muted-foreground">
Revenue
</div>
<div className="text-xl font-bold">
{formatCurrency(bestMonth.revenue)}
</div>
<div className="text-xs text-muted-foreground mt-1">
{bestMonth.orders.toLocaleString()} orders
</div>
</div>
</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}
/>
</div>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</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>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</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>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</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>
<TabsTrigger value="growth">Growth Since Launch</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>
{loading || refreshing ? (
<div className="flex items-center justify-center h-80">
<div className="flex flex-col items-center gap-2">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
<p className="text-sm text-muted-foreground">
Loading chart data...
</p>
</div>
</div>
) : 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 / AOV",
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"
/>
<Line
yAxisId="right"
type="monotone"
dataKey="avgOrderValue"
stroke="#a855f7"
strokeWidth={2}
strokeDasharray="5 5"
dot={{ fill: "#a855f7", r: 3 }}
name="Avg Order Value"
/>
</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>
)}
{/* Calculate totals for the selected period */}
{analyticsData?.orders?.dailyOrders &&
analyticsData?.revenue?.dailyRevenue && (
<div className="grid grid-cols-1 md:grid-cols-2 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 text-green-600">
{formatCurrency(
analyticsData.revenue.dailyRevenue.reduce(
(sum, day) => sum + (day.amount || 0),
0,
),
)}
</div>
</div>
<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 text-blue-600">
{analyticsData.orders.dailyOrders
.reduce((sum, day) => sum + (day.count || 0), 0)
.toLocaleString()}
</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>
{loading || refreshing ? (
<div className="flex items-center justify-center h-80">
<div className="flex flex-col items-center gap-2">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
<p className="text-sm text-muted-foreground">
Loading chart data...
</p>
</div>
</div>
) : 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>
<TabsContent value="growth" className="mt-4 space-y-6">
{/* Growth Header */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">
Platform Growth Since Launch
</h3>
<p className="text-sm text-muted-foreground">
{growthData?.launchDate
? `Tracking since ${new Date(growthData.launchDate).toLocaleDateString("en-GB", { month: "long", year: "numeric" })}`
: "February 2025"}
{growthData?.generatedAt && (
<span className="ml-2">
Last updated:{" "}
{new Date(growthData.generatedAt).toLocaleTimeString(
"en-GB",
{ hour: "2-digit", minute: "2-digit" },
)}
</span>
)}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleGrowthRefresh}
disabled={growthLoading}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${growthLoading ? "animate-spin" : ""}`}
/>
Refresh
</Button>
</div>
{/* Cumulative Stats Cards */}
{growthData?.cumulative && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Orders
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.orders.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Revenue
</div>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(growthData.cumulative.revenue)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Customers
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.customers.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Vendors
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.vendors.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Products
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.products.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Avg Order Value
</div>
<div className="text-2xl font-bold">
{formatCurrency(growthData.cumulative.avgOrderValue)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Monthly Revenue & Orders Chart */}
<Card>
<CardHeader>
<CardTitle>Monthly Revenue & Orders</CardTitle>
<CardDescription>
Platform performance by month since launch
</CardDescription>
</CardHeader>
<CardContent>
{growthLoading ? (
<div className="flex items-center justify-center h-80">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
) : growthData?.monthly && growthData.monthly.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={growthData.monthly.map((m) => ({
...m,
formattedMonth: new Date(
m.month + "-01",
).toLocaleDateString("en-GB", {
month: "short",
year: "2-digit",
}),
}))}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="formattedMonth" tick={{ fontSize: 12 }} />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} />
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
tickFormatter={(value) =>
`£${(value / 1000).toFixed(0)}k`
}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{data.month}</p>
<p className="text-sm text-blue-600">
Orders: {data.orders.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {formatCurrency(data.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers: {data.customers.toLocaleString()}
</p>
<p className="text-sm text-amber-600">
New Vendors: {data.newVendors}
</p>
</div>
);
}
return null;
}}
/>
<Bar
yAxisId="left"
dataKey="orders"
fill="#3b82f6"
radius={[4, 4, 0, 0]}
name="Orders"
/>
<Line
yAxisId="right"
type="monotone"
dataKey="revenue"
stroke="#10b981"
strokeWidth={3}
dot={{ fill: "#10b981", r: 4 }}
name="Revenue"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-80 text-muted-foreground">
No growth data available
</div>
)}
</CardContent>
</Card>
{/* Customer Segments Pie Chart */}
<Card>
<CardHeader>
<CardTitle>Customer Segments</CardTitle>
<CardDescription>Breakdown by purchase behavior</CardDescription>
</CardHeader>
<CardContent>
{growthLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
) : growthData?.customers ? (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={[
{
name: "New (1 order)",
value: growthData.customers.segments.new,
color: SEGMENT_COLORS.new,
},
{
name: "Returning (2+)",
value: growthData.customers.segments.returning,
color: SEGMENT_COLORS.returning,
},
{
name: "Loyal (£300+/4+)",
value: growthData.customers.segments.loyal,
color: SEGMENT_COLORS.loyal,
},
{
name: "VIP (£1k+/10+)",
value: growthData.customers.segments.vip,
color: SEGMENT_COLORS.vip,
},
]}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${(percent * 100).toFixed(0)}%`
}
labelLine={false}
>
{[
{ color: SEGMENT_COLORS.new },
{ color: SEGMENT_COLORS.returning },
{ color: SEGMENT_COLORS.loyal },
{ color: SEGMENT_COLORS.vip },
].map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
const details =
growthData.customers.segmentDetails[
data.name.split(" ")[0].toLowerCase()
];
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium">{data.name}</p>
<p className="text-sm">
Count: {data.value.toLocaleString()}
</p>
{details && (
<>
<p className="text-sm text-green-600">
Revenue:{" "}
{formatCurrency(details.totalRevenue)}
</p>
<p className="text-sm">
Avg Orders: {details.avgOrderCount}
</p>
</>
)}
</div>
);
}
return null;
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
No customer data available
</div>
)}
{/* Segment Stats */}
{growthData?.customers && (
<div className="grid grid-cols-2 gap-2 mt-4">
<div className="p-2 rounded bg-blue-500/10 text-center">
<div className="text-lg font-bold text-blue-600">
{growthData.customers.segments.new}
</div>
<div className="text-xs text-muted-foreground">New</div>
</div>
<div className="p-2 rounded bg-green-500/10 text-center">
<div className="text-lg font-bold text-green-600">
{growthData.customers.segments.returning}
</div>
<div className="text-xs text-muted-foreground">
Returning
</div>
</div>
<div className="p-2 rounded bg-amber-500/10 text-center">
<div className="text-lg font-bold text-amber-600">
{growthData.customers.segments.loyal}
</div>
<div className="text-xs text-muted-foreground">Loyal</div>
</div>
<div className="p-2 rounded bg-purple-500/10 text-center">
<div className="text-lg font-bold text-purple-600">
{growthData.customers.segments.vip}
</div>
<div className="text-xs text-muted-foreground">VIP</div>
</div>
</div>
)}
</CardContent>
</Card>
{/* Monthly Growth Table */}
{growthData?.monthly && growthData.monthly.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Monthly Breakdown</CardTitle>
<CardDescription>Detailed metrics by month</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left p-2 font-medium">Month</th>
<th className="text-right p-2 font-medium">Orders</th>
<th className="text-right p-2 font-medium">Revenue</th>
<th className="text-right p-2 font-medium">
Customers
</th>
<th className="text-right p-2 font-medium">
Avg Order
</th>
<th className="text-right p-2 font-medium">
New Vendors
</th>
<th className="text-right p-2 font-medium">
New Customers
</th>
</tr>
</thead>
<tbody>
{growthData.monthly.map((month) => (
<tr
key={month.month}
className="border-b hover:bg-muted/50"
>
<td className="p-2 font-medium">
{new Date(month.month + "-01").toLocaleDateString(
"en-GB",
{ month: "long", year: "numeric" },
)}
</td>
<td className="text-right p-2">
{month.orders.toLocaleString()}
</td>
<td className="text-right p-2 text-green-600">
{formatCurrency(month.revenue)}
</td>
<td className="text-right p-2">
{month.customers.toLocaleString()}
</td>
<td className="text-right p-2">
{formatCurrency(month.avgOrderValue)}
</td>
<td className="text-right p-2">{month.newVendors}</td>
<td className="text-right p-2">
{month.newCustomers}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
);
}