Christmas decorations and theme logic have been disabled throughout the app, including the isDecember utility, layout, and related imports. Layout now shows a skeleton UI while mounting to prevent layout shift. Minor improvements to RevenueChart tooltip colors and ChatDetail request headers for better consistency.
229 lines
7.5 KiB
TypeScript
229 lines
7.5 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, BarChart, Bar } 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%">
|
|
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="formattedDate"
|
|
tick={{ fontSize: 12 }}
|
|
angle={-45}
|
|
textAnchor="end"
|
|
height={60}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12 }}
|
|
tickFormatter={(value) => hideNumbers ? '***' : `£${(value / 1000).toFixed(0)}k`}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Bar
|
|
dataKey="revenue"
|
|
fill="#2563eb"
|
|
stroke="#1d4ed8"
|
|
strokeWidth={1}
|
|
radius={[2, 2, 0, 0]}
|
|
/>
|
|
</BarChart>
|
|
</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>
|
|
);
|
|
}
|