From f17d6235703e49d53b240e1aa71993174538ffe7 Mon Sep 17 00:00:00 2001 From: g Date: Wed, 7 Jan 2026 12:58:52 +0000 Subject: [PATCH] Revamp growth analytics to show all-time cumulative data Refactors GrowthAnalyticsChart to display all-time growth since first sale, removes period selection, and introduces tabbed charts for daily, monthly, and customer segment analytics. Updates the GrowthAnalytics interface and service to return cumulative and segmented data, and simplifies API usage to always fetch all-time analytics. Improves customer segment breakdown and chart visualizations. --- components/analytics/GrowthAnalyticsChart.tsx | 769 ++++++++++-------- lib/services/analytics-service.ts | 114 ++- 2 files changed, 500 insertions(+), 383 deletions(-) diff --git a/components/analytics/GrowthAnalyticsChart.tsx b/components/analytics/GrowthAnalyticsChart.tsx index 25bc98d..14be0d9 100644 --- a/components/analytics/GrowthAnalyticsChart.tsx +++ b/components/analytics/GrowthAnalyticsChart.tsx @@ -8,23 +8,17 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { - TrendingUp, - TrendingDown, Users, ShoppingCart, DollarSign, Package, RefreshCw, + Calendar, + TrendingUp, } from "lucide-react"; import { getGrowthAnalyticsWithStore, @@ -43,6 +37,8 @@ import { PieChart, Pie, Cell, + BarChart, + Legend, } from "recharts"; interface GrowthAnalyticsChartProps { @@ -56,19 +52,25 @@ const SEGMENT_COLORS = { vip: "#8b5cf6", }; +const SEGMENT_LABELS = { + new: "New (1 order)", + returning: "Returning (2-3 orders)", + loyal: "Loyal (4+ orders or £300+)", + vip: "VIP (10+ orders or £1000+)", +}; + export default function GrowthAnalyticsChart({ hideNumbers = false, }: GrowthAnalyticsChartProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [period, setPeriod] = useState("30"); const [refreshing, setRefreshing] = useState(false); const { toast } = useToast(); const fetchData = async () => { try { setLoading(true); - const response = await getGrowthAnalyticsWithStore(period); + const response = await getGrowthAnalyticsWithStore(); setData(response); } catch (err) { console.error("Error fetching growth data:", err); @@ -85,7 +87,7 @@ export default function GrowthAnalyticsChart({ useEffect(() => { fetchData(); - }, [period]); + }, []); const handleRefresh = () => { setRefreshing(true); @@ -106,59 +108,22 @@ export default function GrowthAnalyticsChart({ return value.toLocaleString(); }; - const TrendIndicator = ({ - value, - suffix = "%", - }: { - value: number; - suffix?: string; - }) => { - if (hideNumbers) return ***; - - const isPositive = value > 0; - const isNeutral = value === 0; - - return ( -
- {isPositive ? ( - - ) : isNeutral ? null : ( - - )} - {isPositive ? "+" : ""} - {value.toFixed(1)} - {suffix} -
- ); + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); }; - const CustomTooltip = ({ active, payload }: any) => { - if (active && payload?.length) { - const item = payload[0].payload; - return ( -
-

{item.date}

-

- Orders: {hideNumbers ? "***" : item.orders.toLocaleString()} -

-

- Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)} -

-

- Customers: {hideNumbers ? "***" : item.uniqueCustomers} -

-
- ); - } - return null; + const getDaysSinceLaunch = () => { + if (!data?.launchDate) return 0; + const launch = new Date(data.launchDate); + const now = new Date(); + return Math.floor( + (now.getTime() - launch.getTime()) / (1000 * 60 * 60 * 24), + ); }; if (loading && !data) { @@ -174,324 +139,486 @@ export default function GrowthAnalyticsChart({
- No growth data available + No growth data available. Complete your first sale to see analytics.
); } - const { summary, customerInsights, timeSeries, topGrowingProducts } = data; + // Prepare chart data + const recentDaily = data.daily.slice(-30); // Last 30 days for daily chart - // Prepare pie chart data - const segmentData = [ - { - name: "New", - value: customerInsights.newCustomers, - color: SEGMENT_COLORS.new, - }, - { - name: "Returning", - value: customerInsights.returningCustomers, - color: SEGMENT_COLORS.returning, - }, - ]; + // Prepare segment pie chart data + const segmentData = Object.entries(data.customers.segments) + .filter(([_, value]) => value > 0) + .map(([key, value]) => ({ + name: key.charAt(0).toUpperCase() + key.slice(1), + value, + color: SEGMENT_COLORS[key as keyof typeof SEGMENT_COLORS], + label: SEGMENT_LABELS[key as keyof typeof SEGMENT_LABELS], + })); + + const CustomTooltip = ({ active, payload }: any) => { + if (active && payload?.length) { + const item = payload[0].payload; + return ( +
+

{item.date || item.month}

+

+ Orders: {hideNumbers ? "***" : item.orders?.toLocaleString()} +

+

+ Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)} +

+

+ Customers: {hideNumbers ? "***" : item.customers} +

+ {item.avgOrderValue && ( +

+ Avg Order: {hideNumbers ? "£***" : formatGBP(item.avgOrderValue)} +

+ )} +
+ ); + } + return null; + }; + + const MonthlyTooltip = ({ active, payload }: any) => { + if (active && payload?.length) { + const item = payload[0].payload; + return ( +
+

{item.month}

+

+ Orders: {hideNumbers ? "***" : item.orders?.toLocaleString()} +

+

+ Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)} +

+

+ Customers: {hideNumbers ? "***" : item.customers} +

+

+ New Customers: {hideNumbers ? "***" : item.newCustomers} +

+
+ ); + } + return null; + }; return (
{/* Header */}
-

Store Growth

-

- {data.period.start} to {data.period.end} ({data.period.granularity}) +

+ + Growth Since First Sale +

+

+ + Started {formatDate(data.launchDate)} ({getDaysSinceLaunch()} days + ago)

-
- - -
+
- {/* Summary Cards */} -
- {/* Orders */} + {/* Cumulative Summary Cards */} +
- Orders + + Total Orders +
- {formatNumber(summary.currentPeriod.orders)} -
-
- - vs {formatNumber(summary.previousPeriod.orders)} prev - - + {formatNumber(data.cumulative.orders)}
- {/* Revenue */}
- Revenue + + Total Revenue +
- {formatCurrency(summary.currentPeriod.revenue)} -
-
- - vs {formatCurrency(summary.previousPeriod.revenue)} prev - - + {formatCurrency(data.cumulative.revenue)}
- {/* Avg Order Value */}
- Avg Order - -
-
- -
- {formatCurrency(summary.currentPeriod.avgOrderValue)} -
-
- - vs {formatCurrency(summary.previousPeriod.avgOrderValue)} prev - - -
-
-
- - {/* Customers */} - - -
- Customers + + Total Customers +
- {formatNumber(summary.currentPeriod.customers)} + {formatNumber(data.cumulative.customers)}
-
- - vs {formatNumber(summary.previousPeriod.customers)} prev - - + + + + + +
+ Products + +
+
+ +
+ {formatNumber(data.cumulative.products)} +
+
+
+ + + +
+ Avg Order + +
+
+ +
+ {formatCurrency(data.cumulative.avgOrderValue)}
- {/* Orders & Revenue Chart */} - - - Orders & Revenue Trend - - Performance over the selected time period - - - - {loading || refreshing ? ( -
-
-
- ) : timeSeries.length > 0 ? ( -
- - - - - (hideNumbers ? "***" : v)} - /> - - hideNumbers ? "***" : `£${(v / 1000).toFixed(0)}k` - } - /> - } /> - - - - -
- ) : ( -
- No data available for the selected period -
- )} -
-
+ {/* Tabbed Charts */} + + + Daily (Last 30 Days) + Monthly Growth + Customer Segments + - {/* Customer Breakdown & Top Products */} -
- {/* Customer Segments */} - - - Customer Breakdown - New vs returning customers - - -
-
-
- {formatNumber(customerInsights.newCustomers)} + {/* Daily Chart */} + + + + Daily Orders & Revenue + Last 30 days of activity + + + {loading || refreshing ? ( +
+
-
New
-
-
-
- {formatNumber(customerInsights.returningCustomers)} -
-
Returning
-
-
-
- {formatNumber(customerInsights.totalCustomers)} -
-
Total
-
-
-
-
-
- {hideNumbers - ? "***" - : customerInsights.avgOrdersPerCustomer.toFixed(1)} -
-
- Avg Orders/Customer -
-
-
-
- {formatCurrency(customerInsights.avgSpentPerCustomer)} -
-
- Avg Spent/Customer -
-
-
- - - - {/* Top Growing Products */} - - - Top Growing Products - - Highest revenue growth vs previous period - - - - {topGrowingProducts.length > 0 ? ( -
- {topGrowingProducts.slice(0, 5).map((product, index) => ( -
-
-
- {index + 1} -
-
-
- {product.productName} -
-
- {formatCurrency(product.currentPeriodRevenue)} -
-
-
-
= 0 - ? "text-green-600" - : "text-red-600" - }`} + ) : recentDaily.length > 0 ? ( +
+ + - {hideNumbers - ? "***" - : `${product.revenueGrowth >= 0 ? "+" : ""}${product.revenueGrowth.toFixed(0)}%`} + + { + const d = new Date(v); + return `${d.getDate()}/${d.getMonth() + 1}`; + }} + /> + (hideNumbers ? "***" : v)} + /> + + hideNumbers ? "***" : `£${(v / 1000).toFixed(0)}k` + } + /> + } /> + + + + + +
+ ) : ( +
+ No daily data available yet +
+ )} + + + + + {/* Monthly Chart */} + + + + Monthly Growth + + Orders, revenue, and new customers by month + + + + {loading || refreshing ? ( +
+
+
+ ) : data.monthly.length > 0 ? ( +
+ + + + { + const [year, month] = v.split("-"); + const date = new Date( + parseInt(year), + parseInt(month) - 1, + ); + return date.toLocaleDateString("en-GB", { + month: "short", + year: "2-digit", + }); + }} + /> + (hideNumbers ? "***" : v)} + /> + + hideNumbers ? "***" : `£${(v / 1000).toFixed(0)}k` + } + /> + } /> + + + + + + +
+ ) : ( +
+ No monthly data available yet +
+ )} +
+
+
+ + {/* Customer Segments */} + +
+ {/* Pie Chart */} + + + Customer Segments + + Breakdown by purchase behavior + + + + {segmentData.length > 0 ? ( +
+ + + + hideNumbers + ? name + : `${name} ${(percent * 100).toFixed(0)}%` + } + > + {segmentData.map((entry, index) => ( + + ))} + + + hideNumbers ? "***" : value + } + /> + + +
+ ) : ( +
+ No customer data yet +
+ )} +
+
+ + {/* Segment Details */} + + + Segment Details + Customer value by segment + + +
+ {Object.entries(data.customers.segments).map( + ([segment, count]) => { + const details = + data.customers.segmentDetails[segment] || {}; + const percentage = + data.customers.segmentPercentages[ + segment as keyof typeof data.customers.segmentPercentages + ] || 0; + + return ( +
+
+
+
+
+ {segment} +
+
+ {SEGMENT_LABELS[ + segment as keyof typeof SEGMENT_LABELS + ] || segment} +
+
+
+
+
+ {hideNumbers ? "***" : count} customers +
+
+ {hideNumbers ? "***" : `${percentage}%`} |{" "} + {hideNumbers + ? "£***" + : formatGBP(details.totalRevenue || 0)}{" "} + revenue +
+
+
+ ); + }, + )} +
+ + {/* Summary Stats */} +
+
+
+ {formatNumber(data.customers.total)} +
+
+ Total Customers
- ))} -
- ) : ( -
- No product growth data available -
- )} - - -
+
+
+ {formatCurrency( + data.cumulative.revenue / (data.customers.total || 1), + )} +
+
+ Avg Revenue/Customer +
+
+
+ + +
+ +
); } diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts index 0dbe615..1118c49 100644 --- a/lib/services/analytics-service.ts +++ b/lib/services/analytics-service.ts @@ -87,58 +87,53 @@ export interface OrderAnalytics { } export interface GrowthAnalytics { - period: { - start: string; - end: string; - days: number; - granularity: "daily" | "weekly" | "monthly"; - }; - summary: { - currentPeriod: { - revenue: number; - orders: number; - avgOrderValue: number; - customers: number; - }; - previousPeriod: { - revenue: number; - orders: number; - avgOrderValue: number; - customers: number; - }; - growthRates: { - revenue: number; - orders: number; - avgOrderValue: number; - customers: number; - }; - }; - customerInsights: { - newCustomers: number; - returningCustomers: number; - totalCustomers: number; - newCustomerRate: number; - avgOrdersPerCustomer: number; - avgSpentPerCustomer: number; - }; - timeSeries: Array<{ + launchDate: string; + generatedAt: string; + daily: Array<{ date: string; - revenue: number; orders: number; + revenue: number; + customers: number; avgOrderValue: number; - uniqueCustomers: number; - cumulativeRevenue: number; - cumulativeOrders: number; }>; - topGrowingProducts: Array<{ - productId: string; - productName: string; - currentPeriodRevenue: number; - previousPeriodRevenue: number; - revenueGrowth: number; - currentPeriodQuantity: number; - previousPeriodQuantity: number; + monthly: Array<{ + month: string; + orders: number; + revenue: number; + customers: number; + avgOrderValue: 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; + products: number; + avgOrderValue: number; + }; } // Analytics Service Functions @@ -223,21 +218,18 @@ export const getOrderAnalytics = async ( }; /** - * Get growth analytics data - * @param period Time period: "7", "30", "90", "365", or "all" (default: "30") - * @param granularity Data granularity: "daily", "weekly", "monthly" (auto-selected if not specified) + * Get growth analytics data (since first order) * @param storeId Optional storeId for staff users */ export const getGrowthAnalytics = async ( - period: string = "30", - granularity?: string, storeId?: string, ): Promise => { - const params = new URLSearchParams({ period }); - if (granularity) params.append("granularity", granularity); + const params = new URLSearchParams(); if (storeId) params.append("storeId", storeId); - const url = `/analytics/growth?${params.toString()}`; + const url = params.toString() + ? `/analytics/growth?${params.toString()}` + : "/analytics/growth"; return clientFetch(url); }; @@ -292,13 +284,11 @@ export const getOrderAnalyticsWithStore = async ( return getOrderAnalytics(period, storeId); }; -export const getGrowthAnalyticsWithStore = async ( - period: string = "30", - granularity?: string, -): Promise => { - const storeId = getStoreIdForUser(); - return getGrowthAnalytics(period, granularity, storeId); -}; +export const getGrowthAnalyticsWithStore = + async (): Promise => { + const storeId = getStoreIdForUser(); + return getGrowthAnalytics(storeId); + }; export function formatGBP(value: number) { return value.toLocaleString("en-GB", {