Files
ember-market-frontend/components/analytics/GrowthAnalyticsChart.tsx
g a05787a091
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Revamp analytics dashboard UI and charts
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.
2026-01-12 05:44:54 +00:00

355 lines
13 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { RefreshCw } from "lucide-react";
import {
getGrowthAnalyticsWithStore,
type GrowthAnalytics,
} from "@/lib/services/analytics-service";
import {
ComposedChart,
Bar,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
} from "recharts";
interface GrowthAnalyticsChartProps {
hideNumbers?: boolean;
}
export default function GrowthAnalyticsChart({
hideNumbers = false,
}: GrowthAnalyticsChartProps) {
const [growthData, setGrowthData] = useState<GrowthAnalytics | null>(null);
const [growthLoading, setGrowthLoading] = useState(true);
const { toast } = useToast();
const fetchGrowthData = async () => {
try {
setGrowthLoading(true);
const response = await getGrowthAnalyticsWithStore();
setGrowthData(response);
} catch (err) {
console.error("Error fetching growth data:", err);
toast({
title: "Error",
description: "Failed to load growth analytics data.",
variant: "destructive",
});
} finally {
setGrowthLoading(false);
}
};
useEffect(() => {
fetchGrowthData();
}, []);
const handleGrowthRefresh = () => {
fetchGrowthData();
};
const formatCurrency = (value: number) => {
if (hideNumbers) return "£***";
return new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
maximumFractionDigits: 0,
}).format(value);
};
return (
<div className="space-y-6">
{/* Growth Header */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">Growth Since First Sale</h3>
<p className="text-sm text-muted-foreground">
{growthData?.launchDate
? `Tracking since ${new Date(growthData.launchDate).toLocaleDateString("en-GB", { month: "long", year: "numeric" })}`
: "Loading..."}
{growthData?.generatedAt && (
<span className="ml-2">
- Last updated:{" "}
{new Date(growthData.generatedAt).toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
)}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleGrowthRefresh}
disabled={growthLoading}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${growthLoading ? "animate-spin" : ""}`}
/>
Refresh
</Button>
</div>
{/* Cumulative Stats Cards */}
{growthData?.cumulative && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Orders
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.orders.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Revenue
</div>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(growthData.cumulative.revenue)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Customers
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.customers.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Products
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.products.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Avg Order Value
</div>
<div className="text-2xl font-bold">
{formatCurrency(growthData.cumulative.avgOrderValue)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Monthly Revenue & Orders Chart */}
<Card>
<CardHeader>
<CardTitle>Monthly Revenue & Orders</CardTitle>
<CardDescription>
Store performance by month since first sale
</CardDescription>
</CardHeader>
<CardContent>
{growthLoading ? (
<div className="flex items-center justify-center h-80">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
) : growthData?.monthly && growthData.monthly.length > 0 ? (
<div className="h-80">
<ResponsiveContainer key={growthData?.monthly?.length || 0} width="100%" height="100%">
<ComposedChart
data={growthData.monthly.map((m) => ({
...m,
formattedMonth: new Date(
m.month + "-01",
).toLocaleDateString("en-GB", {
month: "short",
year: "2-digit",
}),
}))}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
<linearGradient id="colorRevenueGrowth" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorOrdersGrowth" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.6} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis
dataKey="formattedMonth"
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
axisLine={false}
tickLine={false}
/>
<YAxis yAxisId="left" tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }} axisLine={false} tickLine={false} />
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
axisLine={false}
tickLine={false}
tickFormatter={(value) =>
hideNumbers ? "***" : `£${(value / 1000).toFixed(0)}k`
}
/>
<Tooltip
cursor={{ fill: "transparent", stroke: "hsl(var(--muted-foreground))", strokeDasharray: "3 3" }}
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{data.month}</p>
<p className="text-sm text-blue-600">
Orders:{" "}
{hideNumbers
? "***"
: data.orders.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {formatCurrency(data.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers:{" "}
{hideNumbers
? "***"
: data.customers.toLocaleString()}
</p>
{data.newCustomers !== undefined && (
<p className="text-sm text-cyan-600">
New Customers:{" "}
{hideNumbers ? "***" : data.newCustomers}
</p>
)}
</div>
);
}
return null;
}}
/>
<Area
yAxisId="left"
type="monotone"
dataKey="orders"
stroke="#3b82f6"
strokeWidth={2}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
name="Orders"
fill="url(#colorOrdersGrowth)"
/>
<Area
yAxisId="right"
type="monotone"
dataKey="revenue"
stroke="#10b981"
strokeWidth={3}
dot={false}
activeDot={{ r: 5, strokeWidth: 0 }}
name="Revenue"
fill="url(#colorRevenueGrowth)"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-80 text-muted-foreground">
No growth data available
</div>
)}
</CardContent>
</Card>
{/* Monthly Growth Table */}
{growthData?.monthly && growthData.monthly.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Monthly Breakdown</CardTitle>
<CardDescription>Detailed metrics by month</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left p-2 font-medium">Month</th>
<th className="text-right p-2 font-medium">Orders</th>
<th className="text-right p-2 font-medium">Revenue</th>
<th className="text-right p-2 font-medium">Customers</th>
<th className="text-right p-2 font-medium">Avg Order</th>
<th className="text-right p-2 font-medium">
New Customers
</th>
</tr>
</thead>
<tbody>
{growthData.monthly.map((month) => (
<tr
key={month.month}
className="border-b hover:bg-muted/50"
>
<td className="p-2 font-medium">
{new Date(month.month + "-01").toLocaleDateString(
"en-GB",
{ month: "long", year: "numeric" },
)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : month.orders.toLocaleString()}
</td>
<td className="text-right p-2 text-green-600">
{formatCurrency(month.revenue)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : month.customers.toLocaleString()}
</td>
<td className="text-right p-2">
{formatCurrency(month.avgOrderValue)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : (month.newCustomers ?? 0)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</div>
);
}