"use client"; import React, { useState, useEffect } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/common/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/common/select"; import { Button } from "@/components/common/button"; import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart, TrendingUp, TrendingDown, DollarSign, Package, Trophy, PieChart as PieChartIcon, Zap, } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert"; import { fetchClient } from "@/lib/api/api-client"; import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart, AreaChart, Area, } from "recharts"; import { formatGBP, formatNumber, formatCurrency } from "@/lib/utils/format"; import { PieChart, Pie, Cell, Legend, Sector } from "recharts"; import { AdminStatCard } from "./AdminStatCard"; import { TrendIndicator } from "./TrendIndicator"; import { ChartSkeleton, CustomerInsightsSkeleton, MetricsCardSkeleton, TableSkeleton } from "../analytics/SkeletonLoaders"; const renderActiveShape = (props: any) => { const RADIAN = Math.PI / 180; const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value, } = props; const sin = Math.sin(-RADIAN * midAngle); const cos = Math.cos(-RADIAN * midAngle); const sx = cx + (outerRadius + 10) * cos; const sy = cy + (outerRadius + 10) * sin; const mx = cx + (outerRadius + 30) * cos; const my = cy + (outerRadius + 30) * sin; const ex = mx + (cos >= 0 ? 1 : -1) * 22; const ey = my; const textAnchor = cos >= 0 ? "start" : "end"; return ( {payload.name.split(" ")[0]} = 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#888" fontSize={12} >{`Count ${value}`} = 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#999" fontSize={10} > {`(${(percent * 100).toFixed(0)}%)`} ); }; const getSmartInsights = (data?: AnalyticsData | null, growth?: GrowthData | null) => { const insights: Array<{ type: 'positive' | 'neutral' | 'information'; message: string; icon: any; color: string; bg: string; }> = []; if (!data) return insights; // 1. AI Forecast (Always high priority if available) if (data.predictions && data.predictions.predicted > 0) { insights.push({ type: 'positive', message: `AI Forecast: Projected ${formatCurrency(data.predictions.predicted)} revenue over the next 7 days.`, icon: Zap, color: 'text-purple-500', bg: 'bg-purple-500/10' }); } // 2. Comprehensive Comparisons Data if (data.comparisons) { const rawInsights: Array<{ score: number; // Importance score type: 'positive' | 'neutral' | 'information'; message: string; icon: any; color: string; bg: string; }> = []; const periods = Object.entries(data.comparisons); periods.forEach(([label, metrics]) => { const timeframeName = label === '1w' ? 'last week' : label === '2w' ? 'last 2 weeks' : label === '1m' ? 'last month' : label === '3m' ? 'last 3 months' : label === '6m' ? 'last 6 months' : label === '1y' ? 'last year' : 'last 6 months'; // --- Revenue Insight --- if (metrics.revenue.previous > 0) { const revDiff = metrics.revenue.current - metrics.revenue.previous; const revPercent = (revDiff / metrics.revenue.previous) * 100; if (Math.abs(revPercent) > 5) { rawInsights.push({ score: Math.abs(revPercent) * (label === '1w' ? 1.5 : 1), // Weight recent changes higher type: revPercent > 0 ? 'positive' : 'neutral', message: `Revenue is ${revPercent > 0 ? 'up' : 'down'} ${Math.abs(revPercent).toFixed(1)}% vs ${timeframeName} (${formatCurrency(Math.abs(revDiff))} difference).`, icon: revPercent > 0 ? TrendingUp : TrendingDown, color: revPercent > 0 ? 'text-green-500' : 'text-orange-500', bg: revPercent > 0 ? 'bg-green-500/10' : 'bg-orange-500/10' }); } } // --- Order Volume Insight --- if (metrics.orders.previous > 0) { const ordDiff = metrics.orders.current - metrics.orders.previous; const ordPercent = (ordDiff / metrics.orders.previous) * 100; if (Math.abs(ordPercent) > 10) { rawInsights.push({ score: Math.abs(ordPercent) * 0.8, type: ordPercent > 0 ? 'positive' : 'neutral', message: `Order volume has ${ordPercent > 0 ? 'increased' : 'decreased'} by ${Math.abs(ordPercent).toFixed(0)}% compared to ${timeframeName}.`, icon: ShoppingCart, color: ordPercent > 0 ? 'text-blue-500' : 'text-slate-400', bg: ordPercent > 0 ? 'bg-blue-500/10' : 'bg-slate-400/10' }); } } // --- New Customer Insight --- if (metrics.customers.previous > 0) { const custDiff = metrics.customers.current - metrics.customers.previous; const custPercent = (custDiff / metrics.customers.previous) * 100; if (custPercent > 5) { rawInsights.push({ score: custPercent * 1.2, type: 'positive', message: `New customer acquisition is up ${custPercent.toFixed(1)}% vs ${timeframeName}.`, icon: Users, color: 'text-emerald-500', bg: 'bg-emerald-500/10' }); } } }); // Sort by importance (score) and pick top ones rawInsights.sort((a, b) => b.score - a.score); // Add up to 3 most interesting comparison insights rawInsights.slice(0, 3).forEach(ri => { insights.push({ type: ri.type, message: ri.message, icon: ri.icon, color: ri.color, bg: ri.bg }); }); } // 3. Fallback / AI Trends if we have space if (insights.length < 3 && data.predictions?.features?.trends) { const { growthRate } = data.predictions.features.trends; if (growthRate > 0.05) { insights.push({ type: 'positive', message: `AI confirms a steady growth trend of ${(growthRate * 100).toFixed(1)}% in the current market.`, icon: Zap, color: 'text-purple-500', bg: 'bg-purple-500/10' }); } } // 4. Customer Loyalty Insight (from growth data) if (insights.length < 4 && growth?.customers?.segments) { const segments = growth.customers.segments; const totalActive = segments.new + segments.returning + segments.loyal + segments.vip; if (totalActive > 0) { const returningRate = (segments.returning + segments.loyal + segments.vip) / totalActive; if (returningRate > 0.4) { insights.push({ type: 'information', message: `${(returningRate * 100).toFixed(0)}% of your audience consists of returning customers.`, icon: Trophy, color: 'text-amber-500', bg: 'bg-amber-500/10' }); } } } // Ensure we have at least one message if (insights.length === 0) { insights.push({ type: 'neutral', message: 'Dashboard data calibrated. Marketplace performance is stable.', icon: RefreshCw, color: 'text-muted-foreground', bg: 'bg-muted/10' }); } return insights.slice(0, 4); }; interface ComparisonPeriod { orders: { current: number; previous: number }; revenue: { current: number; previous: number }; customers: { current: number; previous: number }; } 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; totalPreviousWeek?: number; totalThisMonth?: number; totalLastMonth?: number; pending?: number; completed?: number; dailyOrders?: { date: string; count: number }[]; }; revenue?: { total?: number; today?: number; thisWeek?: number; previousWeek?: number; thisMonth?: number; lastMonth?: 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; }; comparisons?: { [key: string]: ComparisonPeriod; }; predictions?: { predicted: number; confidence: "high" | "medium" | "low"; confidenceScore: number; dailyPredictions: Array<{ date: string; predicted: number; confidence: string }>; message?: string; features?: { trends: { growthRate: number; momentum: number; volatility: number; }; seasonality: { weekly: number[]; monthly: number[]; }; }; }; } export default function AdminAnalytics() { const [analyticsData, setAnalyticsData] = useState( null, ); const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState("7days"); const [selectedYear, setSelectedYear] = useState( new Date().getFullYear(), ); const [availableYears, setAvailableYears] = useState([ new Date().getFullYear(), ]); 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 insights = React.useMemo(() => getSmartInsights(analyticsData, growthData), [analyticsData, growthData]); const [activeIndex, setActiveIndex] = useState(0); const onPieEnter = (_: any, index: number) => { setActiveIndex(index); }; 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( `/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(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 (
); } // 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 (

{data.formattedDate || label}

{isDualAxis ? (
Orders {data.orders}
Revenue {formatGBP(data.revenue)}
{data.avgOrderValue !== undefined && data.avgOrderValue > 0 && (
Avg Order Value {formatGBP(data.avgOrderValue)}
)}
) : (
{isAmount ? "Revenue" : "Count"} {isAmount ? formatGBP(data.value || data.amount || 0) : `${data.value || data.count || 0}`}
)}
); } return null; }; // 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 = (): { month: string; revenue: number; orders: number } | null => { // 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 (
{errorMessage && ( Error {errorMessage} )}

Dashboard Analytics {!isViewingCurrentYear && ( ({selectedYear}) )}

{isViewingCurrentYear ? "Overview of your marketplace performance" : `Historical data for ${selectedYear}`}

{/* Year selector */}
{/* Date range selector - only show options for current year */}
{showDebug && analyticsData && ( Debug: Raw Data Date Range: {dateRange}
Orders:
Total: {analyticsData?.orders?.total || "N/A"}
Today: {analyticsData?.orders?.totalToday || "N/A"}
Daily Orders Array Length:{" "} {analyticsData?.orders?.dailyOrders?.length || 0}
{/* ... Existing Debug details kept for brevity ... */}
First 3 Daily Orders:
                    {JSON.stringify(
                      analyticsData?.orders?.dailyOrders?.slice(0, 3),
                      null,
                      2,
                    )}
                  
{/* Simplified debug view for code brevity in replacement, focusing on style changes */}
Full JSON Response
                  {JSON.stringify(analyticsData, null, 2)}
                
)} {/* Best Month Card (show for YTD, full year, or previous years) */} {/* Best Month Card (show for YTD, full year, or previous years) */} {bestMonth && (
Best Month{" "} {!isViewingCurrentYear ? `of ${selectedYear}` : dateRange === "year" ? "(Full Year)" : "(YTD)"}
{bestMonth.month}
Revenue
{formatCurrency(bestMonth.revenue)}
{formatNumber(bestMonth.orders)} orders
)}
{/* Orders Card */} Today: {analyticsData?.orders?.totalToday || 0} } trend={{ current: analyticsData?.orders?.totalToday || 0, previous: (analyticsData?.orders?.total || 0) / 30, // Approx simple moving average }} loading={loading || refreshing} chartData={transformChartData( analyticsData?.orders?.dailyOrders || [], "count" )} chartColor="#3b82f6" chartGradientId="colorOrdersStat" /> {/* Revenue Card */} Today: {formatCurrency(analyticsData?.revenue?.today || 0)} } trend={{ current: analyticsData?.revenue?.today || 0, previous: (analyticsData?.revenue?.total || 0) / 30, }} loading={loading || refreshing} chartData={transformChartData( analyticsData?.revenue?.dailyRevenue || [], "amount" )} chartColor="#10b981" chartGradientId="colorRevenueStat" tooltipPrefix="£" /> {/* Vendors Card */} New: {analyticsData?.vendors?.newToday || 0}} trend={{ current: analyticsData?.vendors?.newToday || 0, previous: (analyticsData?.vendors?.newThisWeek || 0) / 7, }} loading={loading || refreshing} chartData={transformChartData( analyticsData?.vendors?.dailyGrowth || [], "count" )} chartColor="#8b5cf6" chartGradientId="colorVendorsStat" >
Active: {analyticsData?.vendors?.active || 0} Stores: {analyticsData?.vendors?.activeStores || 0}
{/* Products Card */}
{/* AI Performance Insights */} {insights.length > 0 && !loading && !refreshing && (
{insights.map((insight, index) => (

Insight {insight.type === 'positive' && }

{insight.message}

))}
)} Orders Vendors Growth Since Launch {loading || refreshing ? ( ) : ( Order Trends Daily order volume and revenue processed over the selected time period {analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
`£${(value / 1000).toFixed(0)}k` } label={{ value: "Revenue", angle: 90, position: "insideRight", style: { fill: 'hsl(var(--muted-foreground))' } }} /> } cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }} />
) : (
No order data available for the selected time period
)} {/* Calculate totals for the selected period */} {analyticsData?.orders?.dailyOrders && analyticsData?.revenue?.dailyRevenue && (
Total Revenue
{formatCurrency( analyticsData.revenue.dailyRevenue.reduce( (sum, day) => sum + (day.amount || 0), 0, ), )}
Total Orders
{analyticsData.orders.dailyOrders .reduce((sum, day) => sum + (day.count || 0), 0) .toLocaleString()}
)}
)}
{loading || refreshing ? ( ) : ( Vendor Growth New vendor registrations over time {analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? (
} />
) : (
No vendor data available for the selected time period
)}
Total Vendors
{formatNumber(analyticsData?.vendors?.total)}
Active Vendors
{formatNumber(analyticsData?.vendors?.active)}
Active Stores
{formatNumber(analyticsData?.vendors?.activeStores)}
New This Week
{formatNumber(analyticsData?.vendors?.newThisWeek)}
{/* Top Vendors by Revenue */} {analyticsData?.vendors?.topVendors && analyticsData.vendors.topVendors.length > 0 && (

Top Vendors by Revenue

{analyticsData.vendors.topVendors.map((vendor, index) => (
{index + 1}
{vendor.vendorName}
{vendor.orderCount} orders
{formatCurrency(vendor.totalRevenue)}
))}
)}
)}
{/* 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
{formatNumber(growthData.cumulative.orders)}
Total Revenue
{formatCurrency(growthData.cumulative.revenue)}
Customers
{formatNumber(growthData.cumulative.customers)}
Vendors
{formatNumber(growthData.cumulative.vendors)}
Products
{formatNumber(growthData.cumulative.products)}
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` } label={{ value: "Revenue", angle: 90, position: "insideRight", style: { fill: 'hsl(var(--muted-foreground))' } }} /> { if (active && payload?.length) { const data = payload[0].payload; return (

{data.month}

Orders {formatNumber(data.orders)}
Revenue {formatCurrency(data.revenue)}
Customers {formatNumber(data.customers)}
New Vendors {data.newVendors}
); } return null; }} />
) : (
No growth data available
)} {/* Customer Segments Pie Chart */} Customer Segments By purchase behavior {growthLoading ? ( ) : growthData?.customers ? ( <>
{[ { color: SEGMENT_COLORS.new }, { color: SEGMENT_COLORS.returning }, { color: SEGMENT_COLORS.loyal }, { color: SEGMENT_COLORS.vip }, ].map((entry, index) => ( ))}
{/* Segment Stats */}
{growthData.customers.segments.new}
New
{growthData.customers.segments.returning}
Returning
{growthData.customers.segments.loyal}
Loyal
{growthData.customers.segments.vip}
VIP
) : (
No customer data available
)}
{/* Monthly Growth Table */} {growthData?.monthly && growthData.monthly.length > 0 && ( Monthly Breakdown Detailed metrics by month
{growthData.monthly.map((month, i) => ( ))}
Month Orders Revenue 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}
)}
); }