diff --git a/components/analytics/GrowthAnalyticsChart.tsx b/components/analytics/GrowthAnalyticsChart.tsx index 9449b53..25bc98d 100644 --- a/components/analytics/GrowthAnalyticsChart.tsx +++ b/components/analytics/GrowthAnalyticsChart.tsx @@ -8,7 +8,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, @@ -16,6 +15,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { TrendingUp, @@ -24,9 +24,7 @@ import { ShoppingCart, DollarSign, Package, - ArrowUpRight, - ArrowDownRight, - Minus, + RefreshCw, } from "lucide-react"; import { getGrowthAnalyticsWithStore, @@ -34,144 +32,149 @@ import { } from "@/lib/services/analytics-service"; import { formatGBP } from "@/utils/format"; import { - LineChart, + ComposedChart, + Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, - AreaChart, - Area, - BarChart, - Bar, + PieChart, + Pie, + Cell, } from "recharts"; -import { ChartSkeleton } from "./SkeletonLoaders"; interface GrowthAnalyticsChartProps { hideNumbers?: boolean; } +const SEGMENT_COLORS = { + new: "#3b82f6", + returning: "#10b981", + loyal: "#f59e0b", + vip: "#8b5cf6", +}; + export default function GrowthAnalyticsChart({ hideNumbers = false, }: GrowthAnalyticsChartProps) { const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = 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); + setData(response); + } catch (err) { + console.error("Error fetching growth data:", err); + toast({ + title: "Error", + description: "Failed to load growth analytics data.", + variant: "destructive", + }); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + useEffect(() => { - const fetchData = async () => { - try { - setIsLoading(true); - setError(null); - const response = await getGrowthAnalyticsWithStore(period); - setData(response); - } catch (err) { - console.error("Error fetching growth data:", err); - setError("Failed to load growth data"); - toast({ - title: "Error", - description: "Failed to load growth analytics data.", - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }; - fetchData(); - }, [period, toast]); + }, [period]); - const maskValue = (value: string): string => { - if (!hideNumbers) return value; - if (value.includes("£")) return "£***"; - if (value.match(/^\d/) || value.match(/^-?\d/)) return "***"; - return value; + const handleRefresh = () => { + setRefreshing(true); + fetchData(); }; - const formatGrowthRate = (rate: number): string => { - const prefix = rate > 0 ? "+" : ""; - return `${prefix}${rate.toFixed(1)}%`; + const formatCurrency = (value: number) => { + if (hideNumbers) return "£***"; + return new Intl.NumberFormat("en-GB", { + style: "currency", + currency: "GBP", + maximumFractionDigits: 0, + }).format(value); }; - const getGrowthIcon = (rate: number) => { - if (rate > 0) return ; - if (rate < 0) return ; - return ; + const formatNumber = (value: number) => { + if (hideNumbers) return "***"; + return value.toLocaleString(); }; - const getGrowthColor = (rate: number): string => { - if (rate > 0) return "text-green-600"; - if (rate < 0) return "text-red-600"; - return "text-muted-foreground"; + 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 CustomTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { + const CustomTooltip = ({ active, payload }: any) => { + if (active && payload?.length) { const item = payload[0].payload; return ( -
-

{item.date}

-
-

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

-

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

-

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

-

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

-
+
+

{item.date}

+

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

+

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

+

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

); } return null; }; - if (isLoading) { + if (loading && !data) { return ( - +
+
+
); } - if (error || !data) { + if (!data) { return ( - - - - Growth Analytics - - - -
- -

- {error || "No growth data available"} -

+ +
+ No growth data available
@@ -180,343 +183,315 @@ export default function GrowthAnalyticsChart({ const { summary, customerInsights, timeSeries, topGrowingProducts } = data; + // Prepare pie chart data + const segmentData = [ + { + name: "New", + value: customerInsights.newCustomers, + color: SEGMENT_COLORS.new, + }, + { + name: "Returning", + value: customerInsights.returningCustomers, + color: SEGMENT_COLORS.returning, + }, + ]; + return (
- {/* Period Selector */} - - -
-
- - - Growth Analytics - - - Compare performance against the previous period - -
- -
-
-
+ {/* Header */} +
+
+

Store Growth

+

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

+
+
+ + +
+
- {/* Growth Rate Cards */} + {/* Summary Cards */}
- {/* Revenue Growth */} + {/* Orders */} - -
-
- - - Revenue - -
-
- {getGrowthIcon(summary.growthRates.revenue)} - - {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.revenue)} - -
+ +
+ Orders +
-
-
- {maskValue(formatGBP(summary.currentPeriod.revenue))} -
-
- vs {maskValue(formatGBP(summary.previousPeriod.revenue))} previous -
+ + +
+ {formatNumber(summary.currentPeriod.orders)} +
+
+ + vs {formatNumber(summary.previousPeriod.orders)} prev + +
- {/* Orders Growth */} + {/* Revenue */} - -
-
- - - Orders - -
-
- {getGrowthIcon(summary.growthRates.orders)} - - {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.orders)} - -
+ +
+ Revenue +
-
-
- {maskValue(summary.currentPeriod.orders.toString())} -
-
- vs {maskValue(summary.previousPeriod.orders.toString())} previous -
+ + +
+ {formatCurrency(summary.currentPeriod.revenue)} +
+
+ + vs {formatCurrency(summary.previousPeriod.revenue)} prev + +
- {/* AOV Growth */} + {/* Avg Order Value */} - -
-
- - - Avg Order - -
-
- {getGrowthIcon(summary.growthRates.avgOrderValue)} - - {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.avgOrderValue)} - -
+ +
+ Avg Order +
-
-
- {maskValue(formatGBP(summary.currentPeriod.avgOrderValue))} -
-
- vs {maskValue(formatGBP(summary.previousPeriod.avgOrderValue))} previous -
+ + +
+ {formatCurrency(summary.currentPeriod.avgOrderValue)} +
+
+ + vs {formatCurrency(summary.previousPeriod.avgOrderValue)} prev + +
- {/* Customers Growth */} + {/* Customers */} - -
-
- - - Customers - -
-
- {getGrowthIcon(summary.growthRates.customers)} - - {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.customers)} - -
+ +
+ Customers +
-
-
- {maskValue(summary.currentPeriod.customers.toString())} -
-
- vs {maskValue(summary.previousPeriod.customers.toString())} previous -
+ + +
+ {formatNumber(summary.currentPeriod.customers)} +
+
+ + vs {formatNumber(summary.previousPeriod.customers)} prev + +
- {/* Revenue Trend Chart */} + {/* Orders & Revenue Chart */} - Revenue & Orders Over Time + Orders & Revenue Trend - {data.period.granularity === "daily" - ? "Daily" - : data.period.granularity === "weekly" - ? "Weekly" - : "Monthly"}{" "} - breakdown from {data.period.start} to {data.period.end} + Performance over the selected time period - {timeSeries.length === 0 ? ( -
- -

- No data available for this period -

+ {loading || refreshing ? ( +
+
- ) : ( -
+ ) : timeSeries.length > 0 ? ( +
- - - - - - - - hideNumbers ? "***" : `£${(value / 1000).toFixed(0)}k` - } + tick={{ fontSize: 12 }} + tickFormatter={(v) => (hideNumbers ? "***" : v)} /> (hideNumbers ? "***" : value)} + tick={{ fontSize: 12 }} + tickFormatter={(v) => + hideNumbers ? "***" : `£${(v / 1000).toFixed(0)}k` + } /> } /> - - +
+ ) : ( +
+ No data available for the selected period +
)} - {/* Customer Insights */} - - - - - Customer Insights - - - New vs returning customers and engagement metrics - - - -
-
-
- {maskValue(customerInsights.newCustomers.toString())} -
-
New Customers
-
-
-
- {maskValue(customerInsights.returningCustomers.toString())} -
-
Returning
-
-
-
- {maskValue(customerInsights.totalCustomers.toString())} -
-
Total
-
-
-
- {hideNumbers ? "***%" : `${customerInsights.newCustomerRate}%`} -
-
New Rate
-
-
-
- {maskValue(customerInsights.avgOrdersPerCustomer.toString())} -
-
Avg Orders
-
-
-
- {maskValue(formatGBP(customerInsights.avgSpentPerCustomer))} -
-
Avg Spent
-
-
-
-
- - {/* Top Growing Products */} - {topGrowingProducts.length > 0 && ( + {/* Customer Breakdown & Top Products */} +
+ {/* Customer Segments */} - - - Top Growing Products - - - Products with the highest revenue growth compared to previous period - + Customer Breakdown + New vs returning customers -
- {topGrowingProducts.slice(0, 5).map((product, index) => ( -
-
-
- {index + 1} -
-
-
{product.productName}
-
- {maskValue(formatGBP(product.currentPeriodRevenue))} revenue - {" · "} - {maskValue(product.currentPeriodQuantity.toString())} sold -
-
-
-
- = 0 ? "default" : "destructive"} - className="flex items-center gap-1" - > - {product.revenueGrowth >= 0 ? ( - - ) : ( - - )} - {hideNumbers ? "***" : formatGrowthRate(product.revenueGrowth)} - -
+
+
+
+ {formatNumber(customerInsights.newCustomers)}
- ))} +
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" + }`} + > + {hideNumbers + ? "***" + : `${product.revenueGrowth >= 0 ? "+" : ""}${product.revenueGrowth.toFixed(0)}%`} +
+
+ ))} +
+ ) : ( +
+ No product growth data available +
+ )} +
+
+
); }