diff --git a/components/analytics/AnalyticsDashboard.tsx b/components/analytics/AnalyticsDashboard.tsx index 87cc37b..fb4b352 100644 --- a/components/analytics/AnalyticsDashboard.tsx +++ b/components/analytics/AnalyticsDashboard.tsx @@ -1,31 +1,46 @@ -"use client" +"use client"; -import { useState, useEffect, Suspense } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useState, useEffect, Suspense } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { - TrendingUp, - ShoppingCart, - Users, - Package, - DollarSign, +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + TrendingUp, + ShoppingCart, + Users, + Package, + DollarSign, BarChart3, PieChart, Activity, RefreshCw, Eye, EyeOff, - Calculator + Calculator, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import MetricsCard from "./MetricsCard"; -import { getAnalyticsOverviewWithStore, type AnalyticsOverview } from "@/lib/services/analytics-service"; +import { + getAnalyticsOverviewWithStore, + type AnalyticsOverview, +} from "@/lib/services/analytics-service"; import { formatGBP } from "@/utils/format"; -import { MetricsCardSkeleton } from './SkeletonLoaders'; -import dynamic from 'next/dynamic'; +import { MetricsCardSkeleton } from "./SkeletonLoaders"; +import dynamic from "next/dynamic"; import { Skeleton } from "@/components/ui/skeleton"; import { DateRangePicker } from "@/components/ui/date-picker"; import { DateRange } from "react-day-picker"; @@ -33,24 +48,31 @@ import { addDays, startOfDay, endOfDay } from "date-fns"; import type { DateRange as ProfitDateRange } from "@/lib/services/profit-analytics-service"; // Lazy load chart components -const RevenueChart = dynamic(() => import('./RevenueChart'), { - loading: () => +const RevenueChart = dynamic(() => import("./RevenueChart"), { + loading: () => , }); -const ProductPerformanceChart = dynamic(() => import('./ProductPerformanceChart'), { - loading: () => +const ProductPerformanceChart = dynamic( + () => import("./ProductPerformanceChart"), + { + loading: () => , + }, +); + +const CustomerInsightsChart = dynamic(() => import("./CustomerInsightsChart"), { + loading: () => , }); -const CustomerInsightsChart = dynamic(() => import('./CustomerInsightsChart'), { - loading: () => +const OrderAnalyticsChart = dynamic(() => import("./OrderAnalyticsChart"), { + loading: () => , }); -const OrderAnalyticsChart = dynamic(() => import('./OrderAnalyticsChart'), { - loading: () => +const ProfitAnalyticsChart = dynamic(() => import("./ProfitAnalyticsChart"), { + loading: () => , }); -const ProfitAnalyticsChart = dynamic(() => import('./ProfitAnalyticsChart'), { - loading: () => +const GrowthAnalyticsChart = dynamic(() => import("./GrowthAnalyticsChart"), { + loading: () => , }); // Chart loading skeleton @@ -77,32 +99,36 @@ interface AnalyticsDashboardProps { initialData: AnalyticsOverview; } -export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardProps) { +export default function AnalyticsDashboard({ + initialData, +}: AnalyticsDashboardProps) { const [data, setData] = useState(initialData); const [isLoading, setIsLoading] = useState(false); - const [timeRange, setTimeRange] = useState('30'); + const [timeRange, setTimeRange] = useState("30"); const [hideNumbers, setHideNumbers] = useState(false); - const [profitDateRange, setProfitDateRange] = useState({ - from: startOfDay(addDays(new Date(), -29)), - to: endOfDay(new Date()) - }); + const [profitDateRange, setProfitDateRange] = useState( + { + from: startOfDay(addDays(new Date(), -29)), + to: endOfDay(new Date()), + }, + ); const { toast } = useToast(); // Function to mask sensitive numbers const maskValue = (value: string): string => { if (!hideNumbers) return value; - + // For currency values (£X.XX), show £*** - if (value.includes('£')) { - return '£***'; + if (value.includes("£")) { + return "£***"; } - + // For regular numbers, replace with asterisks maintaining similar length if (value.match(/^\d/)) { - const numLength = value.replace(/[,\.]/g, '').length; - return '*'.repeat(Math.min(numLength, 4)); + const numLength = value.replace(/[,\.]/g, "").length; + return "*".repeat(Math.min(numLength, 4)); } - + return value; }; @@ -132,16 +158,18 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr value: maskValue(formatGBP(data.revenue.total)), description: "All-time revenue", icon: DollarSign, - trend: data.revenue.monthly > 0 ? "up" as const : "neutral" as const, - trendValue: hideNumbers ? "Hidden" : `${formatGBP(data.revenue.monthly)} this month` + trend: data.revenue.monthly > 0 ? ("up" as const) : ("neutral" as const), + trendValue: hideNumbers + ? "Hidden" + : `${formatGBP(data.revenue.monthly)} this month`, }, { title: "Total Orders", value: maskValue(data.orders.total.toLocaleString()), description: "All-time orders", icon: ShoppingCart, - trend: data.orders.completed > 0 ? "up" as const : "neutral" as const, - trendValue: hideNumbers ? "Hidden" : `${data.orders.completed} completed` + trend: data.orders.completed > 0 ? ("up" as const) : ("neutral" as const), + trendValue: hideNumbers ? "Hidden" : `${data.orders.completed} completed`, }, { title: "Unique Customers", @@ -149,7 +177,7 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr description: "Total customers", icon: Users, trend: "neutral" as const, - trendValue: "Lifetime customers" + trendValue: "Lifetime customers", }, { title: "Products", @@ -157,8 +185,8 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr description: "Active products", icon: Package, trend: "neutral" as const, - trendValue: "In your store" - } + trendValue: "In your store", + }, ]; return ( @@ -166,7 +194,9 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr {/* Header with Privacy Toggle */}
-

Analytics Dashboard

+

+ Analytics Dashboard +

Overview of your store's performance and metrics.

@@ -197,7 +227,9 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr disabled={isLoading} className="flex items-center gap-2" > - + Refresh
@@ -205,15 +237,11 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr {/* Key Metrics Cards */}
- {isLoading ? ( - [...Array(4)].map((_, i) => ( - - )) - ) : ( - metrics.map((metric) => ( - - )) - )} + {isLoading + ? [...Array(4)].map((_, i) => ) + : metrics.map((metric) => ( + + ))}
{/* Completion Rate Card */} @@ -243,14 +271,20 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
-
- {hideNumbers ? "** / **" : `${data.orders.completed} / ${data.orders.total}`} + {hideNumbers + ? "** / **" + : `${data.orders.completed} / ${data.orders.total}`}
)} @@ -262,7 +296,8 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr

Time Period

- Revenue, Profit, and Orders tabs use time filtering. Products and Customers show all-time data. + Revenue, Profit, and Orders tabs use time filtering. Products and + Customers show all-time data.

+ + + + + Last 7 days + Last 30 days + Last 90 days + Last 365 days + All time + + + + + + + {/* Growth Rate Cards */} +
+ {/* Revenue Growth */} + + +
+
+ + + Revenue + +
+
+ {getGrowthIcon(summary.growthRates.revenue)} + + {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.revenue)} + +
+
+
+
+ {maskValue(formatGBP(summary.currentPeriod.revenue))} +
+
+ vs {maskValue(formatGBP(summary.previousPeriod.revenue))} previous +
+
+
+
+ + {/* Orders Growth */} + + +
+
+ + + Orders + +
+
+ {getGrowthIcon(summary.growthRates.orders)} + + {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.orders)} + +
+
+
+
+ {maskValue(summary.currentPeriod.orders.toString())} +
+
+ vs {maskValue(summary.previousPeriod.orders.toString())} previous +
+
+
+
+ + {/* AOV Growth */} + + +
+
+ + + Avg Order + +
+
+ {getGrowthIcon(summary.growthRates.avgOrderValue)} + + {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.avgOrderValue)} + +
+
+
+
+ {maskValue(formatGBP(summary.currentPeriod.avgOrderValue))} +
+
+ vs {maskValue(formatGBP(summary.previousPeriod.avgOrderValue))} previous +
+
+
+
+ + {/* Customers Growth */} + + +
+
+ + + Customers + +
+
+ {getGrowthIcon(summary.growthRates.customers)} + + {hideNumbers ? "***" : formatGrowthRate(summary.growthRates.customers)} + +
+
+
+
+ {maskValue(summary.currentPeriod.customers.toString())} +
+
+ vs {maskValue(summary.previousPeriod.customers.toString())} previous +
+
+
+
+
+ + {/* Revenue Trend Chart */} + + + Revenue & Orders Over Time + + {data.period.granularity === "daily" + ? "Daily" + : data.period.granularity === "weekly" + ? "Weekly" + : "Monthly"}{" "} + breakdown from {data.period.start} to {data.period.end} + + + + {timeSeries.length === 0 ? ( +
+ +

+ No data available for this period +

+
+ ) : ( +
+ + + + + + + + + + + + hideNumbers ? "***" : `£${(value / 1000).toFixed(0)}k` + } + /> + (hideNumbers ? "***" : value)} + /> + } /> + + + + +
+ )} +
+
+ + {/* 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 && ( + + + + + Top Growing Products + + + Products with the highest revenue growth compared to previous period + + + +
+ {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)} + +
+
+ ))} +
+
+
+ )} + + ); +} diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts index e9bb4ac..0dbe615 100644 --- a/lib/services/analytics-service.ts +++ b/lib/services/analytics-service.ts @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import { clientFetch } from '../api-client'; +import { clientFetch } from "../api-client"; // Analytics Types export interface AnalyticsOverview { @@ -22,7 +22,7 @@ export interface AnalyticsOverview { customers: { unique: number; }; - userType?: 'vendor' | 'staff'; + userType?: "vendor" | "staff"; } export interface RevenueData { @@ -86,14 +86,73 @@ 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<{ + date: string; + revenue: number; + orders: 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; + }>; +} + // Analytics Service Functions /** * Get analytics overview data * @param storeId Optional storeId for staff users */ -export const getAnalyticsOverview = async (storeId?: string): Promise => { - const url = storeId ? `/analytics/overview?storeId=${storeId}` : '/analytics/overview'; +export const getAnalyticsOverview = async ( + storeId?: string, +): Promise => { + const url = storeId + ? `/analytics/overview?storeId=${storeId}` + : "/analytics/overview"; return clientFetch(url); }; @@ -102,10 +161,13 @@ export const getAnalyticsOverview = async (storeId?: string): Promise => { +export const getRevenueTrends = async ( + period: string = "30", + storeId?: string, +): Promise => { const params = new URLSearchParams({ period }); - if (storeId) params.append('storeId', storeId); - + if (storeId) params.append("storeId", storeId); + const url = `/analytics/revenue-trends?${params.toString()}`; return clientFetch(url); }; @@ -114,8 +176,12 @@ export const getRevenueTrends = async (period: string = '30', storeId?: string): * Get product performance data * @param storeId Optional storeId for staff users */ -export const getProductPerformance = async (storeId?: string): Promise => { - const url = storeId ? `/analytics/product-performance?storeId=${storeId}` : '/analytics/product-performance'; +export const getProductPerformance = async ( + storeId?: string, +): Promise => { + const url = storeId + ? `/analytics/product-performance?storeId=${storeId}` + : "/analytics/product-performance"; return clientFetch(url); }; @@ -125,13 +191,17 @@ export const getProductPerformance = async (storeId?: string): Promise => { - const params = new URLSearchParams({ - page: page.toString(), - limit: limit.toString() +export const getCustomerInsights = async ( + storeId?: string, + page: number = 1, + limit: number = 10, +): Promise => { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), }); - if (storeId) params.append('storeId', storeId); - + if (storeId) params.append("storeId", storeId); + const url = `/analytics/customer-insights?${params.toString()}`; return clientFetch(url); }; @@ -141,60 +211,100 @@ export const getCustomerInsights = async (storeId?: string, page: number = 1, li * @param period Time period in days (7, 30, 90) * @param storeId Optional storeId for staff users */ -export const getOrderAnalytics = async (period: string = '30', storeId?: string): Promise => { +export const getOrderAnalytics = async ( + period: string = "30", + storeId?: string, +): Promise => { const params = new URLSearchParams({ period }); - if (storeId) params.append('storeId', storeId); - + if (storeId) params.append("storeId", storeId); + const url = `/analytics/order-analytics?${params.toString()}`; return clientFetch(url); }; +/** + * 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) + * @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); + if (storeId) params.append("storeId", storeId); + + const url = `/analytics/growth?${params.toString()}`; + return clientFetch(url); +}; + // Helper function to determine if user is staff and get storeId export const getStoreIdForUser = (): string | undefined => { - if (typeof window === 'undefined') return undefined; - + if (typeof window === "undefined") return undefined; + // Check if user is staff (you might need to adjust this based on your auth system) - const userType = localStorage.getItem('userType'); - const storeId = localStorage.getItem('storeId'); - - if (userType === 'staff' && storeId) { + const userType = localStorage.getItem("userType"); + const storeId = localStorage.getItem("storeId"); + + if (userType === "staff" && storeId) { return storeId; } - + return undefined; }; // Enhanced analytics functions that automatically handle storeId for staff users -export const getAnalyticsOverviewWithStore = async (): Promise => { - const storeId = getStoreIdForUser(); - return getAnalyticsOverview(storeId); -}; +export const getAnalyticsOverviewWithStore = + async (): Promise => { + const storeId = getStoreIdForUser(); + return getAnalyticsOverview(storeId); + }; -export const getRevenueTrendsWithStore = async (period: string = '30'): Promise => { +export const getRevenueTrendsWithStore = async ( + period: string = "30", +): Promise => { const storeId = getStoreIdForUser(); return getRevenueTrends(period, storeId); }; -export const getProductPerformanceWithStore = async (): Promise => { +export const getProductPerformanceWithStore = async (): Promise< + ProductPerformance[] +> => { const storeId = getStoreIdForUser(); return getProductPerformance(storeId); }; -export const getCustomerInsightsWithStore = async (page: number = 1, limit: number = 10): Promise => { +export const getCustomerInsightsWithStore = async ( + page: number = 1, + limit: number = 10, +): Promise => { const storeId = getStoreIdForUser(); return getCustomerInsights(storeId, page, limit); }; -export const getOrderAnalyticsWithStore = async (period: string = '30'): Promise => { +export const getOrderAnalyticsWithStore = async ( + period: string = "30", +): Promise => { const storeId = getStoreIdForUser(); return getOrderAnalytics(period, storeId); }; +export const getGrowthAnalyticsWithStore = async ( + period: string = "30", + granularity?: string, +): Promise => { + const storeId = getStoreIdForUser(); + return getGrowthAnalytics(period, granularity, storeId); +}; + export function formatGBP(value: number) { - return value.toLocaleString('en-GB', { - style: 'currency', - currency: 'GBP', + return value.toLocaleString("en-GB", { + style: "currency", + currency: "GBP", minimumFractionDigits: 2, maximumFractionDigits: 2, }); -} \ No newline at end of file +}