From 2cd6de3b4a5b1dd5b8f8f83b9e2dd628d39de6fd Mon Sep 17 00:00:00 2001 From: g Date: Tue, 6 Jan 2026 00:00:26 +0000 Subject: [PATCH] Add platform growth analytics tab with charts Introduces a new 'Growth Since Launch' tab to the admin analytics page, displaying cumulative stats, monthly revenue and orders, customer segment breakdowns, profit trends, and a detailed monthly growth table. Fetches and visualizes growth data since platform launch using various Recharts components. --- components/admin/AdminAnalytics.tsx | 595 ++++++++++++++++++++++++++++ 1 file changed, 595 insertions(+) diff --git a/components/admin/AdminAnalytics.tsx b/components/admin/AdminAnalytics.tsx index 140ea93..cf0bc1d 100644 --- a/components/admin/AdminAnalytics.tsx +++ b/components/admin/AdminAnalytics.tsx @@ -44,6 +44,77 @@ import { ComposedChart, } from "recharts"; import { formatGBP } from "@/utils/format"; +import { PieChart, Pie, Cell, Legend, AreaChart, Area } 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; + }; + }; + profit: { + monthly: Array<{ + month: string; + revenue: number; + trackedRevenue: number; + cost: number; + profit: number; + profitMargin: number; + costDataCoverage: number; + }>; + totals: { + revenue: number; + trackedRevenue: number; + cost: number; + profit: number; + profitMargin: number; + }; + }; + cumulative: { + orders: number; + revenue: number; + customers: number; + vendors: number; + products: number; + avgOrderValue: number; + }; +} interface AnalyticsData { meta?: { @@ -123,8 +194,18 @@ export default function AdminAnalytics() { const [errorMessage, setErrorMessage] = useState(null); const [refreshing, setRefreshing] = useState(false); const [showDebug, setShowDebug] = useState(false); + const [growthData, setGrowthData] = useState(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 () => { @@ -161,11 +242,34 @@ export default function AdminAnalytics() { 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(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 (
@@ -820,6 +924,7 @@ export default function AdminAnalytics() { Orders Vendors + Growth Since Launch @@ -1072,6 +1177,496 @@ export default function AdminAnalytics() { + + + {/* Growth Header */} +
+
+

+ Platform Growth Since Launch +

+

+ {growthData?.launchDate + ? `Tracking since ${new Date(growthData.launchDate).toLocaleDateString("en-GB", { month: "long", year: "numeric" })}` + : "February 2025"} + {growthData?.generatedAt && ( + + • Last updated:{" "} + {new Date(growthData.generatedAt).toLocaleTimeString( + "en-GB", + { hour: "2-digit", minute: "2-digit" }, + )} + + )} +

+
+ +
+ + {/* Cumulative Stats Cards */} + {growthData?.cumulative && ( +
+ + +
+ Total Orders +
+
+ {growthData.cumulative.orders.toLocaleString()} +
+
+
+ + +
+ Total Revenue +
+
+ {formatCurrency(growthData.cumulative.revenue)} +
+
+
+ + +
+ Customers +
+
+ {growthData.cumulative.customers.toLocaleString()} +
+
+
+ + +
+ Vendors +
+
+ {growthData.cumulative.vendors.toLocaleString()} +
+
+
+ + +
+ Products +
+
+ {growthData.cumulative.products.toLocaleString()} +
+
+
+ + +
+ Avg Order Value +
+
+ {formatCurrency(growthData.cumulative.avgOrderValue)} +
+
+
+
+ )} + + {/* Monthly Revenue & Orders Chart */} + + + Monthly Revenue & Orders + + Platform performance by month since launch + + + + {growthLoading ? ( +
+
+
+ ) : growthData?.monthly && growthData.monthly.length > 0 ? ( +
+ + ({ + ...m, + formattedMonth: new Date( + m.month + "-01", + ).toLocaleDateString("en-GB", { + month: "short", + year: "2-digit", + }), + }))} + margin={{ top: 5, right: 30, left: 20, bottom: 5 }} + > + + + + + `£${(value / 1000).toFixed(0)}k` + } + /> + { + if (active && payload?.length) { + const data = payload[0].payload; + return ( +
+

{data.month}

+

+ Orders: {data.orders.toLocaleString()} +

+

+ Revenue: {formatCurrency(data.revenue)} +

+

+ Customers: {data.customers.toLocaleString()} +

+

+ New Vendors: {data.newVendors} +

+
+ ); + } + return null; + }} + /> + + +
+
+
+ ) : ( +
+ No growth data available +
+ )} +
+
+ +
+ {/* Customer Segments Pie Chart */} + + + Customer Segments + + Breakdown by purchase behavior + + + + {growthLoading ? ( +
+
+
+ ) : growthData?.customers ? ( +
+ + + + `${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) => ( + + ))} + + { + if (active && payload?.length) { + const data = payload[0].payload; + const details = + growthData.customers.segmentDetails[ + data.name.split(" ")[0].toLowerCase() + ]; + return ( +
+

{data.name}

+

+ Count: {data.value.toLocaleString()} +

+ {details && ( + <> +

+ Revenue:{" "} + {formatCurrency(details.totalRevenue)} +

+

+ Avg Orders: {details.avgOrderCount} +

+ + )} +
+ ); + } + return null; + }} + /> +
+
+
+ ) : ( +
+ No customer data available +
+ )} + + {/* Segment Stats */} + {growthData?.customers && ( +
+
+
+ {growthData.customers.segments.new} +
+
New
+
+
+
+ {growthData.customers.segments.returning} +
+
+ Returning +
+
+
+
+ {growthData.customers.segments.loyal} +
+
Loyal
+
+
+
+ {growthData.customers.segments.vip} +
+
VIP
+
+
+ )} +
+
+ + {/* Monthly Profit Chart */} + + + Monthly Profit Trends + + Profit margins over time + {growthData?.profit?.totals && ( + + (Total: {formatCurrency(growthData.profit.totals.profit)}{" "} + at {growthData.profit.totals.profitMargin}% margin) + + )} + + + + {growthLoading ? ( +
+
+
+ ) : growthData?.profit?.monthly && + growthData.profit.monthly.length > 0 ? ( +
+ + ({ + ...m, + formattedMonth: new Date( + m.month + "-01", + ).toLocaleDateString("en-GB", { + month: "short", + year: "2-digit", + }), + }))} + margin={{ top: 5, right: 30, left: 20, bottom: 5 }} + > + + + + `£${(value / 1000).toFixed(0)}k` + } + /> + { + if (active && payload?.length) { + const data = payload[0].payload; + return ( +
+

+ {data.month} +

+

+ Revenue: {formatCurrency(data.revenue)} +

+

+ Cost: {formatCurrency(data.cost)} +

+

+ Profit: {formatCurrency(data.profit)} +

+

+ Margin: {data.profitMargin}% +

+

+ Cost data coverage: {data.costDataCoverage}% +

+
+ ); + } + return null; + }} + /> + +
+
+
+ ) : ( +
+ No profit data available (add costPerUnit to products) +
+ )} +
+
+
+ + {/* Monthly Growth Table */} + {growthData?.monthly && growthData.monthly.length > 0 && ( + + + Monthly Breakdown + Detailed metrics by month + + +
+ + + + + + + + + + + + + + {growthData.monthly.map((month) => ( + + + + + + + + + + ))} + +
MonthOrdersRevenue + Customers + + Avg Order + + New Vendors + + New Customers +
+ {new Date(month.month + "-01").toLocaleDateString( + "en-GB", + { month: "long", year: "numeric" }, + )} + + {month.orders.toLocaleString()} + + {formatCurrency(month.revenue)} + + {month.customers.toLocaleString()} + + {formatCurrency(month.avgOrderValue)} + {month.newVendors} + {month.newCustomers} +
+
+
+
+ )} +
);