All checks were successful
Build Frontend / build (push) Successful in 1m11s
Enhanced the AnalyticsDashboard layout with a premium glassmorphism UI, improved toolbar, and reorganized tabs for better clarity. MetricsCard now features dynamic color coding and trend badges. PredictionsChart received scenario simulation UI upgrades, disabled future ranges based on available history, and improved chart tooltips and visuals. ProfitAnalyticsChart added error handling for product images and minor UI refinements. Updated globals.css with new premium utility classes and improved dark mode color variables.
453 lines
16 KiB
TypeScript
453 lines
16 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";
|
|
import { DateRangePicker } from "@/components/ui/date-picker";
|
|
import { DateRange } from "react-day-picker";
|
|
import { addDays, startOfDay, endOfDay } from "date-fns";
|
|
import type { DateRange as ProfitDateRange } from "@/lib/services/profit-analytics-service";
|
|
import { MotionWrapper } from "@/components/ui/motion-wrapper";
|
|
import { motion } from "framer-motion";
|
|
|
|
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 />,
|
|
});
|
|
|
|
const GrowthAnalyticsChart = dynamic(() => import("./GrowthAnalyticsChart"), {
|
|
loading: () => <ChartSkeleton />,
|
|
});
|
|
|
|
const PredictionsChart = dynamic(() => import("./PredictionsChart"), {
|
|
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("90");
|
|
const [hideNumbers, setHideNumbers] = useState(false);
|
|
const [profitDateRange, setProfitDateRange] = useState<DateRange | undefined>(
|
|
{
|
|
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 "£***";
|
|
}
|
|
|
|
// 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-10 pb-20">
|
|
{/* Header with Integrated Toolbar */}
|
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-3xl font-bold tracking-tight">
|
|
Analytics Dashboard
|
|
</h2>
|
|
<p className="text-muted-foreground mt-1">
|
|
Real-time performance metrics and AI-driven insights.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 p-1.5 glass-morphism rounded-2xl border border-white/5 shadow-2xl backdrop-blur-xl ring-1 ring-white/5">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setHideNumbers(!hideNumbers)}
|
|
className={`flex items-center gap-2 rounded-xl transition-all font-medium px-4 ${hideNumbers ? 'bg-primary text-primary-foreground shadow-lg' : 'hover:bg-white/5'}`}
|
|
>
|
|
{hideNumbers ? (
|
|
<>
|
|
<EyeOff className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Numbers Hidden</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Eye className="h-4 w-4 text-primary/70" />
|
|
<span className="hidden sm:inline">Hide Numbers</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<div className="w-px h-5 bg-white/10 mx-1" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={refreshData}
|
|
disabled={isLoading}
|
|
className="flex items-center gap-2 rounded-xl hover:bg-white/5 font-medium px-4"
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${isLoading ? "animate-spin text-primary" : "text-primary/70"}`}
|
|
/>
|
|
<span className="hidden sm:inline">Refresh Data</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<MotionWrapper className="space-y-12">
|
|
{/* Analytics Tabs Setup */}
|
|
<Tabs defaultValue="overview" className="space-y-10">
|
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 pb-2">
|
|
<TabsList className="bg-transparent h-auto p-0 flex flex-wrap gap-2 lg:gap-4">
|
|
<TabsTrigger
|
|
value="overview"
|
|
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
|
|
>
|
|
<Activity className="h-4 w-4" />
|
|
Overview
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="financials"
|
|
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
|
|
>
|
|
<DollarSign className="h-4 w-4" />
|
|
Financials
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="performance"
|
|
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
|
|
>
|
|
<BarChart3 className="h-4 w-4" />
|
|
Performance
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="ai"
|
|
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
|
|
>
|
|
<Calculator className="h-4 w-4" />
|
|
AI Insights
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Contextual Time Range Selector */}
|
|
<div className="flex items-center gap-3 bg-muted/30 p-1 rounded-xl border border-border/20">
|
|
<span className="text-xs font-semibold text-muted-foreground px-2">Range</span>
|
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
|
<SelectTrigger className="w-[130px] h-8 border-none bg-transparent shadow-none focus:ring-0">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent className="rounded-xl border-border/40">
|
|
<SelectItem value="7">Last 7 days</SelectItem>
|
|
<SelectItem value="30">Last 30 days</SelectItem>
|
|
<SelectItem value="90">Last 90 days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<TabsContent value="overview" className="space-y-10 outline-none">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
className="space-y-10"
|
|
>
|
|
{/* Key Metrics Cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{isLoading
|
|
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
|
|
: metrics.map((metric) => (
|
|
<MetricsCard key={metric.title} {...metric} />
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Completion Rate Card */}
|
|
<Card className="lg:col-span-1 glass-morphism premium-card">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Activity className="h-5 w-5 text-emerald-500" />
|
|
Order Completion
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Successfully processed orders
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<Skeleton className="h-24 w-full rounded-2xl" />
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-4xl font-extrabold tracking-tight">
|
|
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
|
|
</div>
|
|
<Badge variant="outline" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 px-3 py-1 text-xs font-bold">
|
|
{hideNumbers
|
|
? "** / **"
|
|
: `${data.orders.completed} / ${data.orders.total}`}
|
|
</Badge>
|
|
</div>
|
|
<div className="w-full bg-secondary/50 rounded-full h-3 overflow-hidden border border-border/20">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
animate={{ width: hideNumbers ? "0%" : `${data.orders.completionRate}%` }}
|
|
transition={{ duration: 1, ease: "circOut" }}
|
|
className="bg-gradient-to-r from-emerald-500 to-teal-400 h-full rounded-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Growth Chart Snippet (Simplified) */}
|
|
<div className="lg:col-span-2 min-w-0">
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
|
|
</Suspense>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="financials" className="space-y-8 outline-none">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="grid grid-cols-1 xl:grid-cols-2 gap-8"
|
|
>
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<div className="min-w-0">
|
|
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
|
|
</div>
|
|
</Suspense>
|
|
|
|
<div className="space-y-8">
|
|
<Card className="glass-morphism">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Profit Range</CardTitle>
|
|
<CardDescription>
|
|
Custom date selection for analysis
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DateRangePicker
|
|
dateRange={profitDateRange}
|
|
onDateRangeChange={setProfitDateRange}
|
|
placeholder="Select date range"
|
|
showPresets={true}
|
|
className="w-full"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<ProfitAnalyticsChart
|
|
dateRange={
|
|
profitDateRange?.from && profitDateRange?.to
|
|
? {
|
|
from: profitDateRange.from,
|
|
to: profitDateRange.to,
|
|
}
|
|
: undefined
|
|
}
|
|
hideNumbers={hideNumbers}
|
|
/>
|
|
</Suspense>
|
|
</div>
|
|
</motion.div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="performance" className="space-y-8 outline-none">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="grid grid-cols-1 lg:grid-cols-2 gap-8"
|
|
>
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<div className="min-w-0">
|
|
<ProductPerformanceChart />
|
|
</div>
|
|
</Suspense>
|
|
<div className="space-y-8 min-w-0">
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<OrderAnalyticsChart timeRange={timeRange} />
|
|
</Suspense>
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<CustomerInsightsChart />
|
|
</Suspense>
|
|
</div>
|
|
</motion.div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="ai" className="space-y-8 outline-none">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="min-w-0"
|
|
>
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<PredictionsChart timeRange={parseInt(timeRange)} />
|
|
</Suspense>
|
|
</motion.div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</MotionWrapper>
|
|
</div>
|
|
);
|
|
}
|