Files
ember-market-frontend/components/analytics/RevenueChart.tsx
g a0605e47de
All checks were successful
Build Frontend / build (push) Successful in 1m19s
Improve chart visuals and add null safety in analytics
Refactored GrowthAnalyticsChart to use Area for 'orders' with gradient fill and improved dot handling. Enhanced PredictionsChart with consistent null checks for predictions data, improved tooltip rendering, and adjusted chart margins and axis styles. Updated RevenueChart to add activeDot styling for better interactivity.
2026-01-12 04:52:40 +00:00

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 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>
);
}