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.
242 lines
8.3 KiB
TypeScript
242 lines
8.3 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { TrendingUp, DollarSign } from "lucide-react";
|
|
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
|
|
import { formatGBP } from "@/utils/format";
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
|
import { ChartSkeleton } from './SkeletonLoaders';
|
|
|
|
interface RevenueChartProps {
|
|
timeRange: string;
|
|
hideNumbers?: boolean;
|
|
}
|
|
|
|
interface ChartDataPoint {
|
|
date: string;
|
|
revenue: number;
|
|
orders: number;
|
|
formattedDate: string;
|
|
}
|
|
|
|
export default function RevenueChart({ timeRange, hideNumbers = false }: RevenueChartProps) {
|
|
const [data, setData] = useState<RevenueData[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
const fetchRevenueData = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
const response = await getRevenueTrendsWithStore(timeRange);
|
|
console.log('Revenue trends response:', response);
|
|
setData(Array.isArray(response) ? response : []);
|
|
} catch (err) {
|
|
console.error('Error fetching revenue data:', err);
|
|
setError('Failed to load revenue data');
|
|
setData([]);
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to load revenue trends data.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRevenueData();
|
|
}, [timeRange, toast]);
|
|
|
|
// Transform data for Recharts
|
|
const chartData: ChartDataPoint[] = data.map(item => {
|
|
// Use UTC to avoid timezone issues
|
|
const date = new Date(Date.UTC(item._id.year, item._id.month - 1, item._id.day));
|
|
return {
|
|
date: date.toISOString().split('T')[0], // YYYY-MM-DD format
|
|
revenue: item.revenue || 0,
|
|
orders: item.orders || 0,
|
|
formattedDate: date.toLocaleDateString('en-GB', {
|
|
weekday: 'short',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
timeZone: 'UTC'
|
|
})
|
|
};
|
|
});
|
|
|
|
// Calculate summary stats
|
|
const safeData = Array.isArray(data) ? data : [];
|
|
const totalRevenue = safeData.reduce((sum, item) => sum + (item.revenue || 0), 0);
|
|
const totalOrders = safeData.reduce((sum, item) => sum + (item.orders || 0), 0);
|
|
const averageRevenue = safeData.length > 0 ? totalRevenue / safeData.length : 0;
|
|
|
|
// 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
|
|
return '***';
|
|
};
|
|
|
|
// Custom tooltip for the chart
|
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
if (active && payload && payload.length) {
|
|
const data = payload[0].payload;
|
|
return (
|
|
<div className="bg-background p-3 border border-border rounded-lg shadow-lg">
|
|
<p className="text-sm font-medium text-foreground">{data.formattedDate}</p>
|
|
<p className="text-sm text-blue-600 dark:text-blue-400">
|
|
Revenue: <span className="font-semibold">{hideNumbers ? '£***' : formatGBP(data.revenue)}</span>
|
|
</p>
|
|
<p className="text-sm text-green-600">
|
|
Orders: <span className="font-semibold">{hideNumbers ? '***' : data.orders}</span>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<ChartSkeleton
|
|
title="Revenue Trends"
|
|
description="Revenue performance over the selected time period"
|
|
icon={TrendingUp}
|
|
showStats={true}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<TrendingUp className="h-5 w-5" />
|
|
Revenue Trends
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-8">
|
|
<DollarSign className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
<p className="text-muted-foreground">Failed to load revenue data</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (chartData.length === 0) {
|
|
return (
|
|
<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="text-center py-8">
|
|
<DollarSign className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
<p className="text-muted-foreground">No revenue data available for this period</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<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 */}
|
|
<div className="h-64">
|
|
<ResponsiveContainer key={timeRange} width="100%" height="100%">
|
|
<AreaChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<defs>
|
|
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#2563eb" stopOpacity={0.8} />
|
|
<stop offset="95%" stopColor="#2563eb" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
|
<XAxis
|
|
dataKey="formattedDate"
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
angle={-45}
|
|
textAnchor="end"
|
|
height={60}
|
|
minTickGap={30}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tickFormatter={(value) => hideNumbers ? '***' : `£${(value / 1000).toFixed(0)}k`}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: "transparent", stroke: "hsl(var(--muted-foreground))", strokeDasharray: "3 3" }} />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="revenue"
|
|
stroke="#2563eb"
|
|
fillOpacity={1}
|
|
fill="url(#colorRevenue)"
|
|
strokeWidth={2}
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Summary stats */}
|
|
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{maskValue(formatGBP(totalRevenue))}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">Total Revenue</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{maskValue(totalOrders.toString())}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">Total Orders</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-purple-600">
|
|
{maskValue(formatGBP(averageRevenue))}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">Avg Daily Revenue</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|