This commit is contained in:
NotII
2025-07-01 01:49:49 +01:00
parent 20ae136e37
commit 18e87721e2
8 changed files with 458 additions and 110 deletions

View File

@@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard'; import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton';
import StoreSelector from '@/components/analytics/StoreSelector'; import StoreSelector from '@/components/analytics/StoreSelector';
import { getAnalyticsOverviewServer } from '@/lib/server-api'; import { getAnalyticsOverviewServer } from '@/lib/server-api';
import { fetchServer } from '@/lib/api'; import { fetchServer } from '@/lib/api';
@@ -65,14 +66,7 @@ export default async function AnalyticsPage({
<Dashboard> <Dashboard>
<div className="space-y-6"> <div className="space-y-6">
{/* Analytics Content */} {/* Analytics Content */}
<Suspense fallback={ <Suspense fallback={<AnalyticsDashboardSkeleton />}>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading analytics...</p>
</div>
</div>
}>
<AnalyticsDashboard initialData={initialData} /> <AnalyticsDashboard initialData={initialData} />
</Suspense> </Suspense>
</div> </div>

View File

@@ -25,6 +25,7 @@ import OrderAnalyticsChart from "./OrderAnalyticsChart";
import MetricsCard from "./MetricsCard"; 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 { formatGBP } from "@/utils/format";
import { MetricsCardSkeleton } from './SkeletonLoaders';
interface AnalyticsDashboardProps { interface AnalyticsDashboardProps {
initialData: AnalyticsOverview; initialData: AnalyticsOverview;
@@ -95,9 +96,15 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
<div className="space-y-6"> <div className="space-y-6">
{/* Key Metrics Cards */} {/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{metrics.map((metric) => ( {isLoading ? (
<MetricsCard key={metric.title} {...metric} /> [...Array(4)].map((_, i) => (
))} <MetricsCardSkeleton key={i} />
))
) : (
metrics.map((metric) => (
<MetricsCard key={metric.title} {...metric} />
))
)}
</div> </div>
{/* Completion Rate Card */} {/* Completion Rate Card */}
@@ -112,25 +119,55 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center gap-4"> {isLoading ? (
<div className="text-3xl font-bold"> <div className="flex items-center gap-4">
{data.orders.completionRate}% <div className="h-12 w-16 bg-muted/20 rounded animate-pulse" />
</div> <div className="flex-1">
<div className="flex-1"> <div className="w-full bg-muted/20 rounded-full h-2 animate-pulse" />
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${data.orders.completionRate}%` }}
/>
</div> </div>
<div className="h-6 w-16 bg-muted/20 rounded animate-pulse" />
</div> </div>
<Badge variant="secondary"> ) : (
{data.orders.completed} / {data.orders.total} <div className="flex items-center gap-4">
</Badge> <div className="text-3xl font-bold">
</div> {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: `${data.orders.completionRate}%` }}
/>
</div>
</div>
<Badge variant="secondary">
{data.orders.completed} / {data.orders.total}
</Badge>
</div>
)}
</CardContent> </CardContent>
</Card> </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 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 */} {/* Analytics Tabs */}
<div className="space-y-6"> <div className="space-y-6">
<Tabs defaultValue="revenue" className="space-y-6"> <Tabs defaultValue="revenue" className="space-y-6">

View File

@@ -0,0 +1,204 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MetricsCardSkeleton } from './SkeletonLoaders';
import {
TrendingUp,
Package,
Users,
BarChart3,
Activity
} from "lucide-react";
export default function AnalyticsDashboardSkeleton() {
return (
<div className="space-y-6">
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<MetricsCardSkeleton key={i} />
))}
</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>
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-16" />
<div className="flex-1">
<Skeleton className="w-full h-2 rounded-full" />
</div>
<Skeleton className="h-6 w-16" />
</div>
</CardContent>
</Card>
{/* Analytics Tabs */}
<div className="space-y-6">
<Tabs defaultValue="revenue" className="space-y-6">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="revenue" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Revenue
</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">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Revenue Trends
</CardTitle>
<CardDescription>
Revenue performance over the selected time period
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Chart area */}
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
{/* Summary stats */}
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
{[...Array(3)].map((_, i) => (
<div key={i} className="text-center space-y-2">
<Skeleton className="h-8 w-20 mx-auto" />
<Skeleton className="h-4 w-24 mx-auto" />
</div>
))}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="products" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Package className="h-5 w-5" />
Product Performance
</CardTitle>
<CardDescription>
Top performing products by revenue and sales
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Table header */}
<div className="grid grid-cols-5 gap-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</div>
{/* Table rows */}
{[...Array(8)].map((_, rowIndex) => (
<div key={rowIndex} className="grid grid-cols-5 gap-4">
{[...Array(5)].map((_, colIndex) => (
<div key={colIndex} className="flex items-center gap-3">
{colIndex === 0 && (
<Skeleton className="h-10 w-10 rounded" />
)}
<Skeleton className="h-4 flex-1" />
</div>
))}
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="customers" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Customer Insights
</CardTitle>
<CardDescription>
Customer segmentation and behavior analysis
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Customer segments */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="text-center space-y-2">
<Skeleton className="h-8 w-16 mx-auto" />
<Skeleton className="h-4 w-20 mx-auto" />
</div>
))}
</div>
{/* Top customers table */}
<div className="space-y-4">
<Skeleton className="h-6 w-32" />
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
<div className="text-right space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-3 w-12" />
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="orders" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Order Analytics
</CardTitle>
<CardDescription>
Order status distribution and trends
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Chart area */}
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Users, Crown, UserPlus, UserCheck, Star } from "lucide-react"; import { Users, Crown, UserPlus, UserCheck, Star } from "lucide-react";
import { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service"; import { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/utils/format";
import { CustomerInsightsSkeleton } from './SkeletonLoaders';
export default function CustomerInsightsChart() { export default function CustomerInsightsChart() {
const [data, setData] = useState<CustomerInsights | null>(null); const [data, setData] = useState<CustomerInsights | null>(null);
@@ -85,26 +86,11 @@ export default function CustomerInsightsChart() {
if (isLoading) { if (isLoading) {
return ( return (
<Card> <CustomerInsightsSkeleton
<CardHeader> title="Customer Insights"
<CardTitle className="flex items-center gap-2"> description="Customer segmentation and behavior analysis"
<Users className="h-5 w-5" /> icon={Users}
Customer Insights />
</CardTitle>
<CardDescription>
Customer segmentation and behavior analysis
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<Skeleton className="h-32 w-full" />
<div className="grid grid-cols-2 gap-4">
<Skeleton className="h-20" />
<Skeleton className="h-20" />
</div>
</div>
</CardContent>
</Card>
); );
} }

View File

@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react"; import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service"; import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/utils/format";
import { ChartSkeleton } from './SkeletonLoaders';
interface OrderAnalyticsChartProps { interface OrderAnalyticsChartProps {
timeRange: string; timeRange: string;
@@ -105,23 +106,12 @@ export default function OrderAnalyticsChart({ timeRange }: OrderAnalyticsChartPr
if (isLoading) { if (isLoading) {
return ( return (
<Card> <ChartSkeleton
<CardHeader> title="Order Analytics"
<CardTitle className="flex items-center gap-2"> description="Order status distribution and trends"
<BarChart3 className="h-5 w-5" /> icon={BarChart3}
Order Analytics showStats={false}
</CardTitle> />
<CardDescription>
Order status distribution and trends
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
</div>
</CardContent>
</Card>
); );
} }

View File

@@ -9,6 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service"; import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/utils/format";
import { TableSkeleton } from './SkeletonLoaders';
export default function ProductPerformanceChart() { export default function ProductPerformanceChart() {
const [data, setData] = useState<ProductPerformance[]>([]); const [data, setData] = useState<ProductPerformance[]>([]);
@@ -41,33 +42,13 @@ export default function ProductPerformanceChart() {
if (isLoading) { if (isLoading) {
return ( return (
<Card> <TableSkeleton
<CardHeader> title="Product Performance"
<CardTitle className="flex items-center gap-2"> description="Top performing products by revenue and sales"
<Package className="h-5 w-5" /> icon={Package}
Product Performance rows={8}
</CardTitle> columns={5}
<CardDescription> />
Top performing products by revenue and sales
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-20" />
</div>
))}
</div>
</CardContent>
</Card>
); );
} }

View File

@@ -8,6 +8,7 @@ import { TrendingUp, DollarSign } from "lucide-react";
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service"; import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/utils/format";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { ChartSkeleton } from './SkeletonLoaders';
interface RevenueChartProps { interface RevenueChartProps {
timeRange: string; timeRange: string;
@@ -95,27 +96,12 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
if (isLoading) { if (isLoading) {
return ( return (
<Card> <ChartSkeleton
<CardHeader> title="Revenue Trends"
<CardTitle className="flex items-center gap-2"> description="Revenue performance over the selected time period"
<TrendingUp className="h-5 w-5" /> icon={TrendingUp}
Revenue Trends showStats={true}
</CardTitle> />
<CardDescription>
Revenue performance over the selected time period
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Skeleton className="h-64 w-full" />
<div className="grid grid-cols-3 gap-4">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
</div>
</CardContent>
</Card>
); );
} }

View File

@@ -0,0 +1,170 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
// Chart skeleton for revenue trends and order analytics
export function ChartSkeleton({
title,
description,
icon: Icon,
showStats = false
}: {
title: string;
description: string;
icon: any;
showStats?: boolean;
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Icon className="h-5 w-5" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Chart area */}
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
{/* Summary stats if applicable */}
{showStats && (
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
{[...Array(3)].map((_, i) => (
<div key={i} className="text-center space-y-2">
<Skeleton className="h-8 w-20 mx-auto" />
<Skeleton className="h-4 w-24 mx-auto" />
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
);
}
// Table skeleton for product performance
export function TableSkeleton({
title,
description,
icon: Icon,
rows = 5,
columns = 5
}: {
title: string;
description: string;
icon: any;
rows?: number;
columns?: number;
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Icon className="h-5 w-5" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Table header */}
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{[...Array(columns)].map((_, i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</div>
{/* Table rows */}
{[...Array(rows)].map((_, rowIndex) => (
<div key={rowIndex} className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{[...Array(columns)].map((_, colIndex) => (
<div key={colIndex} className="flex items-center gap-3">
{colIndex === 0 && (
<Skeleton className="h-10 w-10 rounded" />
)}
<Skeleton className="h-4 flex-1" />
</div>
))}
</div>
))}
</div>
</CardContent>
</Card>
);
}
// Customer insights skeleton with segments
export function CustomerInsightsSkeleton({
title,
description,
icon: Icon
}: {
title: string;
description: string;
icon: any;
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Icon className="h-5 w-5" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Customer segments */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="text-center space-y-2">
<Skeleton className="h-8 w-16 mx-auto" />
<Skeleton className="h-4 w-20 mx-auto" />
</div>
))}
</div>
{/* Top customers table */}
<div className="space-y-4">
<Skeleton className="h-6 w-32" />
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
<div className="text-right space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-3 w-12" />
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
}
// Metrics card skeleton
export function MetricsCardSkeleton() {
return (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-20" />
</div>
<Skeleton className="h-8 w-8 rounded" />
</div>
<div className="mt-4 flex items-center gap-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-20" />
</div>
</CardContent>
</Card>
);
}