Files
ember-market-frontend/components/analytics/RevenueChart.tsx
g fe01f31538
Some checks failed
Build Frontend / build (push) Failing after 7s
Refactor UI imports and update component paths
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
2026-01-13 05:02:13 +00:00

244 lines
8.3 KiB
TypeScript

"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import { TrendingUp, DollarSign } from "lucide-react";
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
import { formatGBP } from "@/lib/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>
);
}