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", {