Revamp growth analytics to show all-time cumulative data

Refactors GrowthAnalyticsChart to display all-time growth since first sale, removes period selection, and introduces tabbed charts for daily, monthly, and customer segment analytics. Updates the GrowthAnalytics interface and service to return cumulative and segmented data, and simplifies API usage to always fetch all-time analytics. Improves customer segment breakdown and chart visualizations.
This commit is contained in:
g
2026-01-07 12:58:52 +00:00
parent 3e27a4b1f2
commit f17d623570
2 changed files with 500 additions and 383 deletions

View File

@@ -8,23 +8,17 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { import {
TrendingUp,
TrendingDown,
Users, Users,
ShoppingCart, ShoppingCart,
DollarSign, DollarSign,
Package, Package,
RefreshCw, RefreshCw,
Calendar,
TrendingUp,
} from "lucide-react"; } from "lucide-react";
import { import {
getGrowthAnalyticsWithStore, getGrowthAnalyticsWithStore,
@@ -43,6 +37,8 @@ import {
PieChart, PieChart,
Pie, Pie,
Cell, Cell,
BarChart,
Legend,
} from "recharts"; } from "recharts";
interface GrowthAnalyticsChartProps { interface GrowthAnalyticsChartProps {
@@ -56,19 +52,25 @@ const SEGMENT_COLORS = {
vip: "#8b5cf6", vip: "#8b5cf6",
}; };
const SEGMENT_LABELS = {
new: "New (1 order)",
returning: "Returning (2-3 orders)",
loyal: "Loyal (4+ orders or £300+)",
vip: "VIP (10+ orders or £1000+)",
};
export default function GrowthAnalyticsChart({ export default function GrowthAnalyticsChart({
hideNumbers = false, hideNumbers = false,
}: GrowthAnalyticsChartProps) { }: GrowthAnalyticsChartProps) {
const [data, setData] = useState<GrowthAnalytics | null>(null); const [data, setData] = useState<GrowthAnalytics | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [period, setPeriod] = useState("30");
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getGrowthAnalyticsWithStore(period); const response = await getGrowthAnalyticsWithStore();
setData(response); setData(response);
} catch (err) { } catch (err) {
console.error("Error fetching growth data:", err); console.error("Error fetching growth data:", err);
@@ -85,7 +87,7 @@ export default function GrowthAnalyticsChart({
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [period]); }, []);
const handleRefresh = () => { const handleRefresh = () => {
setRefreshing(true); setRefreshing(true);
@@ -106,59 +108,22 @@ export default function GrowthAnalyticsChart({
return value.toLocaleString(); return value.toLocaleString();
}; };
const TrendIndicator = ({ const formatDate = (dateStr: string) => {
value, const date = new Date(dateStr);
suffix = "%", return date.toLocaleDateString("en-GB", {
}: { day: "numeric",
value: number; month: "short",
suffix?: string; year: "numeric",
}) => { });
if (hideNumbers) return <span className="text-muted-foreground">***</span>;
const isPositive = value > 0;
const isNeutral = value === 0;
return (
<div
className={`flex items-center text-sm font-medium ${
isNeutral
? "text-muted-foreground"
: isPositive
? "text-green-600"
: "text-red-600"
}`}
>
{isPositive ? (
<TrendingUp className="h-4 w-4 mr-1" />
) : isNeutral ? null : (
<TrendingDown className="h-4 w-4 mr-1" />
)}
{isPositive ? "+" : ""}
{value.toFixed(1)}
{suffix}
</div>
);
}; };
const CustomTooltip = ({ active, payload }: any) => { const getDaysSinceLaunch = () => {
if (active && payload?.length) { if (!data?.launchDate) return 0;
const item = payload[0].payload; const launch = new Date(data.launchDate);
return ( const now = new Date();
<div className="bg-background border border-border p-3 rounded-lg shadow-lg"> return Math.floor(
<p className="font-medium mb-2">{item.date}</p> (now.getTime() - launch.getTime()) / (1000 * 60 * 60 * 24),
<p className="text-sm text-blue-600">
Orders: {hideNumbers ? "***" : item.orders.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers: {hideNumbers ? "***" : item.uniqueCustomers}
</p>
</div>
); );
}
return null;
}; };
if (loading && !data) { if (loading && !data) {
@@ -174,52 +139,91 @@ export default function GrowthAnalyticsChart({
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
No growth data available No growth data available. Complete your first sale to see analytics.
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }
const { summary, customerInsights, timeSeries, topGrowingProducts } = data; // Prepare chart data
const recentDaily = data.daily.slice(-30); // Last 30 days for daily chart
// Prepare pie chart data // Prepare segment pie chart data
const segmentData = [ const segmentData = Object.entries(data.customers.segments)
{ .filter(([_, value]) => value > 0)
name: "New", .map(([key, value]) => ({
value: customerInsights.newCustomers, name: key.charAt(0).toUpperCase() + key.slice(1),
color: SEGMENT_COLORS.new, value,
}, color: SEGMENT_COLORS[key as keyof typeof SEGMENT_COLORS],
{ label: SEGMENT_LABELS[key as keyof typeof SEGMENT_LABELS],
name: "Returning", }));
value: customerInsights.returningCustomers,
color: SEGMENT_COLORS.returning, const CustomTooltip = ({ active, payload }: any) => {
}, if (active && payload?.length) {
]; const item = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{item.date || item.month}</p>
<p className="text-sm text-blue-600">
Orders: {hideNumbers ? "***" : item.orders?.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers: {hideNumbers ? "***" : item.customers}
</p>
{item.avgOrderValue && (
<p className="text-sm text-orange-600">
Avg Order: {hideNumbers ? "£***" : formatGBP(item.avgOrderValue)}
</p>
)}
</div>
);
}
return null;
};
const MonthlyTooltip = ({ active, payload }: any) => {
if (active && payload?.length) {
const item = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{item.month}</p>
<p className="text-sm text-blue-600">
Orders: {hideNumbers ? "***" : item.orders?.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {hideNumbers ? "£***" : formatGBP(item.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers: {hideNumbers ? "***" : item.customers}
</p>
<p className="text-sm text-cyan-600">
New Customers: {hideNumbers ? "***" : item.newCustomers}
</p>
</div>
);
}
return null;
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="text-lg font-semibold">Store Growth</h3> <h3 className="text-lg font-semibold flex items-center gap-2">
<p className="text-sm text-muted-foreground"> <TrendingUp className="h-5 w-5" />
{data.period.start} to {data.period.end} ({data.period.granularity}) Growth Since First Sale
</h3>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Calendar className="h-3 w-3" />
Started {formatDate(data.launchDate)} ({getDaysSinceLaunch()} days
ago)
</p> </p>
</div> </div>
<div className="flex items-center gap-2">
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
<SelectItem value="365">Last year</SelectItem>
<SelectItem value="all">All time</SelectItem>
</SelectContent>
</Select>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -231,122 +235,124 @@ export default function GrowthAnalyticsChart({
/> />
</Button> </Button>
</div> </div>
</div>
{/* Summary Cards */} {/* Cumulative Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
{/* Orders */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Orders</CardTitle> <CardTitle className="text-sm font-medium">
Total Orders
</CardTitle>
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{formatNumber(summary.currentPeriod.orders)} {formatNumber(data.cumulative.orders)}
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-xs text-muted-foreground">
vs {formatNumber(summary.previousPeriod.orders)} prev
</span>
<TrendIndicator value={summary.growthRates.orders} />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Revenue */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Revenue</CardTitle> <CardTitle className="text-sm font-medium">
Total Revenue
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-green-600"> <div className="text-2xl font-bold text-green-600">
{formatCurrency(summary.currentPeriod.revenue)} {formatCurrency(data.cumulative.revenue)}
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-xs text-muted-foreground">
vs {formatCurrency(summary.previousPeriod.revenue)} prev
</span>
<TrendIndicator value={summary.growthRates.revenue} />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Avg Order Value */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Avg Order</CardTitle> <CardTitle className="text-sm font-medium">
<Package className="h-4 w-4 text-muted-foreground" /> Total Customers
</div> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatCurrency(summary.currentPeriod.avgOrderValue)}
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-xs text-muted-foreground">
vs {formatCurrency(summary.previousPeriod.avgOrderValue)} prev
</span>
<TrendIndicator value={summary.growthRates.avgOrderValue} />
</div>
</CardContent>
</Card>
{/* Customers */}
<Card>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Customers</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" /> <Users className="h-4 w-4 text-muted-foreground" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{formatNumber(summary.currentPeriod.customers)} {formatNumber(data.cumulative.customers)}
</div> </div>
<div className="flex items-center justify-between mt-1"> </CardContent>
<span className="text-xs text-muted-foreground"> </Card>
vs {formatNumber(summary.previousPeriod.customers)} prev
</span> <Card>
<TrendIndicator value={summary.growthRates.customers} /> <CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Products</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(data.cumulative.products)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Avg Order</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatCurrency(data.cumulative.avgOrderValue)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Orders & Revenue Chart */} {/* Tabbed Charts */}
<Tabs defaultValue="daily" className="space-y-4">
<TabsList>
<TabsTrigger value="daily">Daily (Last 30 Days)</TabsTrigger>
<TabsTrigger value="monthly">Monthly Growth</TabsTrigger>
<TabsTrigger value="customers">Customer Segments</TabsTrigger>
</TabsList>
{/* Daily Chart */}
<TabsContent value="daily">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Orders & Revenue Trend</CardTitle> <CardTitle>Daily Orders & Revenue</CardTitle>
<CardDescription> <CardDescription>Last 30 days of activity</CardDescription>
Performance over the selected time period
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loading || refreshing ? ( {loading || refreshing ? (
<div className="flex items-center justify-center h-80"> <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 className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div> </div>
) : timeSeries.length > 0 ? ( ) : recentDaily.length > 0 ? (
<div className="h-80"> <div className="h-80">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<ComposedChart <ComposedChart
data={timeSeries} data={recentDaily}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
> >
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis <XAxis
dataKey="date" dataKey="date"
tick={{ fontSize: 12 }} tick={{ fontSize: 11 }}
angle={-45} angle={-45}
textAnchor="end" textAnchor="end"
height={60} height={60}
tickFormatter={(v) => {
const d = new Date(v);
return `${d.getDate()}/${d.getMonth() + 1}`;
}}
/> />
<YAxis <YAxis
yAxisId="left" yAxisId="left"
@@ -362,6 +368,7 @@ export default function GrowthAnalyticsChart({
} }
/> />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Legend />
<Bar <Bar
yAxisId="left" yAxisId="left"
dataKey="orders" dataKey="orders"
@@ -369,6 +376,91 @@ export default function GrowthAnalyticsChart({
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
name="Orders" name="Orders"
/> />
<Line
yAxisId="right"
type="monotone"
dataKey="revenue"
stroke="#10b981"
strokeWidth={2}
dot={false}
name="Revenue"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-80 text-muted-foreground">
No daily data available yet
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Monthly Chart */}
<TabsContent value="monthly">
<Card>
<CardHeader>
<CardTitle>Monthly Growth</CardTitle>
<CardDescription>
Orders, revenue, and new customers by month
</CardDescription>
</CardHeader>
<CardContent>
{loading || refreshing ? (
<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>
) : data.monthly.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={data.monthly}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="month"
tick={{ fontSize: 12 }}
tickFormatter={(v) => {
const [year, month] = v.split("-");
const date = new Date(
parseInt(year),
parseInt(month) - 1,
);
return date.toLocaleDateString("en-GB", {
month: "short",
year: "2-digit",
});
}}
/>
<YAxis
yAxisId="left"
tick={{ fontSize: 12 }}
tickFormatter={(v) => (hideNumbers ? "***" : v)}
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
tickFormatter={(v) =>
hideNumbers ? "***" : `£${(v / 1000).toFixed(0)}k`
}
/>
<Tooltip content={<MonthlyTooltip />} />
<Legend />
<Bar
yAxisId="left"
dataKey="orders"
fill="#3b82f6"
name="Orders"
/>
<Bar
yAxisId="left"
dataKey="newCustomers"
fill="#06b6d4"
name="New Customers"
/>
<Line <Line
yAxisId="right" yAxisId="right"
type="monotone" type="monotone"
@@ -383,115 +475,150 @@ export default function GrowthAnalyticsChart({
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center h-80 text-muted-foreground"> <div className="flex items-center justify-center h-80 text-muted-foreground">
No data available for the selected period No monthly data available yet
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</TabsContent>
{/* Customer Breakdown & Top Products */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Customer Segments */} {/* Customer Segments */}
<TabsContent value="customers">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Pie Chart */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Customer Breakdown</CardTitle> <CardTitle>Customer Segments</CardTitle>
<CardDescription>New vs returning customers</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-muted/50 p-3 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">
{formatNumber(customerInsights.newCustomers)}
</div>
<div className="text-xs text-muted-foreground">New</div>
</div>
<div className="bg-muted/50 p-3 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">
{formatNumber(customerInsights.returningCustomers)}
</div>
<div className="text-xs text-muted-foreground">Returning</div>
</div>
<div className="bg-muted/50 p-3 rounded-lg text-center">
<div className="text-2xl font-bold">
{formatNumber(customerInsights.totalCustomers)}
</div>
<div className="text-xs text-muted-foreground">Total</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted/50 p-3 rounded-lg text-center">
<div className="text-lg font-bold">
{hideNumbers
? "***"
: customerInsights.avgOrdersPerCustomer.toFixed(1)}
</div>
<div className="text-xs text-muted-foreground">
Avg Orders/Customer
</div>
</div>
<div className="bg-muted/50 p-3 rounded-lg text-center">
<div className="text-lg font-bold">
{formatCurrency(customerInsights.avgSpentPerCustomer)}
</div>
<div className="text-xs text-muted-foreground">
Avg Spent/Customer
</div>
</div>
</div>
</CardContent>
</Card>
{/* Top Growing Products */}
<Card>
<CardHeader>
<CardTitle>Top Growing Products</CardTitle>
<CardDescription> <CardDescription>
Highest revenue growth vs previous period Breakdown by purchase behavior
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{topGrowingProducts.length > 0 ? ( {segmentData.length > 0 ? (
<div className="space-y-2"> <div className="h-64">
{topGrowingProducts.slice(0, 5).map((product, index) => ( <ResponsiveContainer width="100%" height="100%">
<div <PieChart>
key={product.productId} <Pie
className="flex items-center justify-between p-2 bg-muted/30 rounded-lg" data={segmentData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
label={({ name, percent }) =>
hideNumbers
? name
: `${name} ${(percent * 100).toFixed(0)}%`
}
> >
<div className="flex items-center gap-2"> {segmentData.map((entry, index) => (
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-semibold"> <Cell key={`cell-${index}`} fill={entry.color} />
{index + 1}
</div>
<div>
<div className="text-sm font-medium truncate max-w-[150px]">
{product.productName}
</div>
<div className="text-xs text-muted-foreground">
{formatCurrency(product.currentPeriodRevenue)}
</div>
</div>
</div>
<div
className={`text-sm font-semibold ${
product.revenueGrowth >= 0
? "text-green-600"
: "text-red-600"
}`}
>
{hideNumbers
? "***"
: `${product.revenueGrowth >= 0 ? "+" : ""}${product.revenueGrowth.toFixed(0)}%`}
</div>
</div>
))} ))}
</Pie>
<Tooltip
formatter={(value: number) =>
hideNumbers ? "***" : value
}
/>
</PieChart>
</ResponsiveContainer>
</div> </div>
) : ( ) : (
<div className="text-center text-muted-foreground py-8"> <div className="flex items-center justify-center h-64 text-muted-foreground">
No product growth data available No customer data yet
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Segment Details */}
<Card>
<CardHeader>
<CardTitle>Segment Details</CardTitle>
<CardDescription>Customer value by segment</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{Object.entries(data.customers.segments).map(
([segment, count]) => {
const details =
data.customers.segmentDetails[segment] || {};
const percentage =
data.customers.segmentPercentages[
segment as keyof typeof data.customers.segmentPercentages
] || 0;
return (
<div
key={segment}
className="flex items-center justify-between p-3 bg-muted/30 rounded-lg"
>
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
style={{
backgroundColor:
SEGMENT_COLORS[
segment as keyof typeof SEGMENT_COLORS
],
}}
/>
<div>
<div className="font-medium capitalize">
{segment}
</div>
<div className="text-xs text-muted-foreground">
{SEGMENT_LABELS[
segment as keyof typeof SEGMENT_LABELS
] || segment}
</div>
</div>
</div>
<div className="text-right">
<div className="font-semibold">
{hideNumbers ? "***" : count} customers
</div>
<div className="text-xs text-muted-foreground">
{hideNumbers ? "***" : `${percentage}%`} |{" "}
{hideNumbers
? "£***"
: formatGBP(details.totalRevenue || 0)}{" "}
revenue
</div>
</div> </div>
</div> </div>
); );
},
)}
</div>
{/* Summary Stats */}
<div className="grid grid-cols-2 gap-4 mt-6 pt-4 border-t">
<div className="text-center">
<div className="text-2xl font-bold">
{formatNumber(data.customers.total)}
</div>
<div className="text-xs text-muted-foreground">
Total Customers
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{formatCurrency(
data.cumulative.revenue / (data.customers.total || 1),
)}
</div>
<div className="text-xs text-muted-foreground">
Avg Revenue/Customer
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
);
} }

View File

@@ -87,58 +87,53 @@ export interface OrderAnalytics {
} }
export interface GrowthAnalytics { export interface GrowthAnalytics {
period: { launchDate: string;
start: string; generatedAt: string;
end: string; daily: Array<{
days: number;
granularity: "daily" | "weekly" | "monthly";
};
summary: {
currentPeriod: {
revenue: number;
orders: number;
avgOrderValue: number;
customers: number;
};
previousPeriod: {
revenue: number;
orders: number;
avgOrderValue: number;
customers: number;
};
growthRates: {
revenue: number;
orders: number;
avgOrderValue: number;
customers: number;
};
};
customerInsights: {
newCustomers: number;
returningCustomers: number;
totalCustomers: number;
newCustomerRate: number;
avgOrdersPerCustomer: number;
avgSpentPerCustomer: number;
};
timeSeries: Array<{
date: string; date: string;
revenue: number;
orders: number; orders: number;
revenue: number;
customers: number;
avgOrderValue: number; avgOrderValue: number;
uniqueCustomers: number;
cumulativeRevenue: number;
cumulativeOrders: number;
}>; }>;
topGrowingProducts: Array<{ monthly: Array<{
productId: string; month: string;
productName: string; orders: number;
currentPeriodRevenue: number; revenue: number;
previousPeriodRevenue: number; customers: number;
revenueGrowth: number; avgOrderValue: number;
currentPeriodQuantity: number; newCustomers: number;
previousPeriodQuantity: number;
}>; }>;
customers: {
total: number;
segments: {
new: number;
returning: number;
loyal: number;
vip: number;
};
segmentDetails: {
[key: string]: {
count: number;
totalRevenue: number;
avgOrderCount: number;
avgSpent: number;
};
};
segmentPercentages: {
new: number;
returning: number;
loyal: number;
vip: number;
};
};
cumulative: {
orders: number;
revenue: number;
customers: number;
products: number;
avgOrderValue: number;
};
} }
// Analytics Service Functions // Analytics Service Functions
@@ -223,21 +218,18 @@ export const getOrderAnalytics = async (
}; };
/** /**
* Get growth analytics data * Get growth analytics data (since first order)
* @param period Time period: "7", "30", "90", "365", or "all" (default: "30")
* @param granularity Data granularity: "daily", "weekly", "monthly" (auto-selected if not specified)
* @param storeId Optional storeId for staff users * @param storeId Optional storeId for staff users
*/ */
export const getGrowthAnalytics = async ( export const getGrowthAnalytics = async (
period: string = "30",
granularity?: string,
storeId?: string, storeId?: string,
): Promise<GrowthAnalytics> => { ): Promise<GrowthAnalytics> => {
const params = new URLSearchParams({ period }); const params = new URLSearchParams();
if (granularity) params.append("granularity", granularity);
if (storeId) params.append("storeId", storeId); if (storeId) params.append("storeId", storeId);
const url = `/analytics/growth?${params.toString()}`; const url = params.toString()
? `/analytics/growth?${params.toString()}`
: "/analytics/growth";
return clientFetch<GrowthAnalytics>(url); return clientFetch<GrowthAnalytics>(url);
}; };
@@ -292,12 +284,10 @@ export const getOrderAnalyticsWithStore = async (
return getOrderAnalytics(period, storeId); return getOrderAnalytics(period, storeId);
}; };
export const getGrowthAnalyticsWithStore = async ( export const getGrowthAnalyticsWithStore =
period: string = "30", async (): Promise<GrowthAnalytics> => {
granularity?: string,
): Promise<GrowthAnalytics> => {
const storeId = getStoreIdForUser(); const storeId = getStoreIdForUser();
return getGrowthAnalytics(period, granularity, storeId); return getGrowthAnalytics(storeId);
}; };
export function formatGBP(value: number) { export function formatGBP(value: number) {