Files
ember-market-frontend/components/analytics/AnalyticsDashboard.tsx
NotII 130ecac208 Add Chromebook compatibility fixes and optimizations
Implemented comprehensive Chromebook-specific fixes including viewport adjustments, enhanced touch and keyboard detection, improved scrolling and keyboard navigation hooks, and extensive CSS optimizations for better usability. Updated chat and dashboard interfaces for larger touch targets, better focus management, and responsive layouts. Added documentation in docs/CHROMEBOOK-FIXES.md and new hooks for Chromebook scroll and keyboard handling.
2025-10-26 18:29:23 +00:00

331 lines
11 KiB
TypeScript

"use client"
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,
BarChart3,
PieChart,
Activity,
RefreshCw,
Eye,
EyeOff,
Calculator
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import MetricsCard from "./MetricsCard";
import { getAnalyticsOverviewWithStore, type AnalyticsOverview } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { MetricsCardSkeleton } from './SkeletonLoaders';
import dynamic from 'next/dynamic';
import { Skeleton } from "@/components/ui/skeleton";
// Lazy load chart components
const RevenueChart = dynamic(() => import('./RevenueChart'), {
loading: () => <ChartSkeleton />
});
const ProductPerformanceChart = dynamic(() => import('./ProductPerformanceChart'), {
loading: () => <ChartSkeleton />
});
const CustomerInsightsChart = dynamic(() => import('./CustomerInsightsChart'), {
loading: () => <ChartSkeleton />
});
const OrderAnalyticsChart = dynamic(() => import('./OrderAnalyticsChart'), {
loading: () => <ChartSkeleton />
});
const ProfitAnalyticsChart = dynamic(() => import('./ProfitAnalyticsChart'), {
loading: () => <ChartSkeleton />
});
// Chart loading skeleton
function ChartSkeleton() {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-72" />
</CardHeader>
<CardContent>
<div className="h-80 w-full flex items-center justify-center">
<div className="flex flex-col items-center gap-2">
<BarChart3 className="h-8 w-8 text-muted-foreground animate-pulse" />
<Skeleton className="h-4 w-32" />
</div>
</div>
</CardContent>
</Card>
);
}
interface AnalyticsDashboardProps {
initialData: AnalyticsOverview;
}
export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardProps) {
const [data, setData] = useState<AnalyticsOverview>(initialData);
const [isLoading, setIsLoading] = useState(false);
const [timeRange, setTimeRange] = useState('30');
const [hideNumbers, setHideNumbers] = useState(false);
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 '£***';
}
// 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));
}
return value;
};
const refreshData = async () => {
try {
setIsLoading(true);
const newData = await getAnalyticsOverviewWithStore();
setData(newData);
toast({
title: "Data refreshed",
description: "Analytics data has been updated successfully.",
});
} catch (error) {
toast({
title: "Error",
description: "Failed to refresh analytics data.",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const metrics = [
{
title: "Total Revenue",
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`
},
{
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`
},
{
title: "Unique Customers",
value: maskValue(data.customers.unique.toLocaleString()),
description: "Total customers",
icon: Users,
trend: "neutral" as const,
trendValue: "Lifetime customers"
},
{
title: "Products",
value: maskValue(data.products.total.toLocaleString()),
description: "Active products",
icon: Package,
trend: "neutral" as const,
trendValue: "In your store"
}
];
return (
<div className="space-y-6">
{/* Header with Privacy Toggle */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Analytics Dashboard</h2>
<p className="text-muted-foreground">
Overview of your store's performance and metrics.
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setHideNumbers(!hideNumbers)}
className="flex items-center gap-2"
>
{hideNumbers ? (
<>
<EyeOff className="h-4 w-4" />
Show Numbers
</>
) : (
<>
<Eye className="h-4 w-4" />
Hide Numbers
</>
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={refreshData}
disabled={isLoading}
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</div>
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{isLoading ? (
[...Array(4)].map((_, i) => (
<MetricsCardSkeleton key={i} />
))
) : (
metrics.map((metric) => (
<MetricsCard key={metric.title} {...metric} />
))
)}
</div>
{/* Completion Rate Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Order Completion Rate
</CardTitle>
<CardDescription>
Percentage of orders that have been successfully completed
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center gap-4">
<div className="h-12 w-16 bg-muted/20 rounded animate-pulse" />
<div className="flex-1">
<div className="w-full bg-muted/20 rounded-full h-2 animate-pulse" />
</div>
<div className="h-6 w-16 bg-muted/20 rounded animate-pulse" />
</div>
) : (
<div className="flex items-center gap-4">
<div className="text-3xl font-bold">
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
</div>
<div className="flex-1">
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: hideNumbers ? "0%" : `${data.orders.completionRate}%` }}
/>
</div>
</div>
<Badge variant="secondary">
{hideNumbers ? "** / **" : `${data.orders.completed} / ${data.orders.total}`}
</Badge>
</div>
)}
</CardContent>
</Card>
{/* Time Period Selector */}
<div className="flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
<div>
<h3 className="text-lg font-semibold">Time Period</h3>
<p className="text-sm text-muted-foreground">
Revenue, Profit, and Orders tabs use time filtering. Products and Customers show all-time data.
</p>
</div>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
</div>
{/* Analytics Tabs */}
<div className="space-y-6">
<Tabs defaultValue="revenue" className="space-y-6">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-5">
<TabsTrigger value="revenue" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Revenue
</TabsTrigger>
<TabsTrigger value="profit" className="flex items-center gap-2">
<Calculator className="h-4 w-4" />
Profit
</TabsTrigger>
<TabsTrigger value="products" className="flex items-center gap-2">
<Package className="h-4 w-4" />
Products
</TabsTrigger>
<TabsTrigger value="customers" className="flex items-center gap-2">
<Users className="h-4 w-4" />
Customers
</TabsTrigger>
<TabsTrigger value="orders" className="flex items-center gap-2">
<BarChart3 className="h-4 w-4" />
Orders
</TabsTrigger>
</TabsList>
<TabsContent value="revenue" className="space-y-6">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
</Suspense>
</TabsContent>
<TabsContent value="profit" className="space-y-6">
<Suspense fallback={<ChartSkeleton />}>
<ProfitAnalyticsChart timeRange={timeRange} hideNumbers={hideNumbers} />
</Suspense>
</TabsContent>
<TabsContent value="products" className="space-y-6">
<Suspense fallback={<ChartSkeleton />}>
<ProductPerformanceChart />
</Suspense>
</TabsContent>
<TabsContent value="customers" className="space-y-6">
<Suspense fallback={<ChartSkeleton />}>
<CustomerInsightsChart />
</Suspense>
</TabsContent>
<TabsContent value="orders" className="space-y-6">
<Suspense fallback={<ChartSkeleton />}>
<OrderAnalyticsChart timeRange={timeRange} />
</Suspense>
</TabsContent>
</Tabs>
</div>
</div>
);
}