Add value distribution to order range dialog
This commit is contained in:
@@ -1654,22 +1654,37 @@ export class EventsService {
|
|||||||
dayStats.orderValueRange.smallestOrderId = orderId;
|
dayStats.orderValueRange.smallestOrderId = orderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track order value distribution
|
// Track order value distribution with more granular ranges
|
||||||
if (totalAmount < 25) {
|
const ranges = [
|
||||||
dayStats.orderValueRange.distribution.under25.count++;
|
{ min: 0, max: 25 },
|
||||||
dayStats.orderValueRange.distribution.under25.total += totalAmount;
|
{ min: 25, max: 50 },
|
||||||
} else if (totalAmount < 50) {
|
{ min: 50, max: 75 },
|
||||||
dayStats.orderValueRange.distribution.under50.count++;
|
{ min: 75, max: 100 },
|
||||||
dayStats.orderValueRange.distribution.under50.total += totalAmount;
|
{ min: 100, max: 150 },
|
||||||
} else if (totalAmount < 100) {
|
{ min: 150, max: 200 },
|
||||||
dayStats.orderValueRange.distribution.under100.count++;
|
{ min: 200, max: 300 },
|
||||||
dayStats.orderValueRange.distribution.under100.total += totalAmount;
|
{ min: 300, max: 500 },
|
||||||
} else if (totalAmount < 200) {
|
{ min: 500, max: Infinity }
|
||||||
dayStats.orderValueRange.distribution.under200.count++;
|
];
|
||||||
dayStats.orderValueRange.distribution.under200.total += totalAmount;
|
|
||||||
} else {
|
// Initialize distribution if not exists
|
||||||
dayStats.orderValueRange.distribution.over200.count++;
|
if (!dayStats.orderValueDistribution) {
|
||||||
dayStats.orderValueRange.distribution.over200.total += totalAmount;
|
dayStats.orderValueDistribution = ranges.map(range => ({
|
||||||
|
min: range.min,
|
||||||
|
max: range.max === Infinity ? 'Infinity' : range.max,
|
||||||
|
count: 0,
|
||||||
|
total: 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the appropriate range and update counts
|
||||||
|
const rangeIndex = ranges.findIndex(range =>
|
||||||
|
totalAmount >= range.min && totalAmount < range.max
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rangeIndex !== -1) {
|
||||||
|
dayStats.orderValueDistribution[rangeIndex].count++;
|
||||||
|
dayStats.orderValueDistribution[rangeIndex].total += totalAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1764,30 +1779,9 @@ export class EventsService {
|
|||||||
largest: Number(day.orderValueRange?.largest || 0),
|
largest: Number(day.orderValueRange?.largest || 0),
|
||||||
smallest: Number(day.orderValueRange?.smallest || 0),
|
smallest: Number(day.orderValueRange?.smallest || 0),
|
||||||
largestOrderId: day.orderValueRange?.largestOrderId || null,
|
largestOrderId: day.orderValueRange?.largestOrderId || null,
|
||||||
smallestOrderId: day.orderValueRange?.smallestOrderId || null,
|
smallestOrderId: day.orderValueRange?.smallestOrderId || null
|
||||||
distribution: {
|
|
||||||
under25: {
|
|
||||||
count: Number(day.orderValueRange?.distribution?.under25?.count || 0),
|
|
||||||
total: Number(day.orderValueRange?.distribution?.under25?.total || 0)
|
|
||||||
},
|
},
|
||||||
under50: {
|
orderValueDistribution: day.orderValueDistribution || []
|
||||||
count: Number(day.orderValueRange?.distribution?.under50?.count || 0),
|
|
||||||
total: Number(day.orderValueRange?.distribution?.under50?.total || 0)
|
|
||||||
},
|
|
||||||
under100: {
|
|
||||||
count: Number(day.orderValueRange?.distribution?.under100?.count || 0),
|
|
||||||
total: Number(day.orderValueRange?.distribution?.under100?.total || 0)
|
|
||||||
},
|
|
||||||
under200: {
|
|
||||||
count: Number(day.orderValueRange?.distribution?.under200?.count || 0),
|
|
||||||
total: Number(day.orderValueRange?.distribution?.under200?.total || 0)
|
|
||||||
},
|
|
||||||
over200: {
|
|
||||||
count: Number(day.orderValueRange?.distribution?.over200?.count || 0),
|
|
||||||
total: Number(day.orderValueRange?.distribution?.over200?.total || 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const TimeSeriesChart = ({
|
|||||||
return (
|
return (
|
||||||
<div className="h-[400px] w-full">
|
<div className="h-[400px] w-full">
|
||||||
<ResponsiveContainer width="100%" height={height}>
|
<ResponsiveContainer width="100%" height={height}>
|
||||||
<ChartComponent data={data} margin={{ top: 10, right: 30, left: 20, bottom: 5 }}>
|
<ChartComponent data={data} margin={{ top: 10, right: 20, left: -20, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="timestamp"
|
dataKey="timestamp"
|
||||||
@@ -740,6 +740,31 @@ const RefundDetails = ({ data }) => {
|
|||||||
const OrderRangeDetails = ({ data }) => {
|
const OrderRangeDetails = ({ data }) => {
|
||||||
if (!data?.length) return <div className="text-muted-foreground">No data available for the selected time range.</div>;
|
if (!data?.length) return <div className="text-muted-foreground">No data available for the selected time range.</div>;
|
||||||
|
|
||||||
|
// Get the data from the entire period
|
||||||
|
const allData = data.reduce((acc, day) => {
|
||||||
|
// Initialize distribution data structure if not exists
|
||||||
|
if (!acc.orderValueDistribution) {
|
||||||
|
acc.orderValueDistribution = day.orderValueDistribution?.map(range => ({
|
||||||
|
...range,
|
||||||
|
count: 0,
|
||||||
|
total: 0
|
||||||
|
})) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate distribution data
|
||||||
|
day.orderValueDistribution?.forEach((range, index) => {
|
||||||
|
if (acc.orderValueDistribution[index]) {
|
||||||
|
acc.orderValueDistribution[index].count += range.count;
|
||||||
|
acc.orderValueDistribution[index].total += range.total;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track total orders for percentage calculation
|
||||||
|
acc.totalOrders = (acc.totalOrders || 0) + (day.orders || 0);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
const timeSeriesData = data.map(day => ({
|
const timeSeriesData = data.map(day => ({
|
||||||
timestamp: day.timestamp,
|
timestamp: day.timestamp,
|
||||||
largest: day.orderValueRange?.largest || 0,
|
largest: day.orderValueRange?.largest || 0,
|
||||||
@@ -747,6 +772,17 @@ const OrderRangeDetails = ({ data }) => {
|
|||||||
average: day.averageOrderValue || 0
|
average: day.averageOrderValue || 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Transform distribution data using aggregated values
|
||||||
|
const formattedDistributionData = allData.orderValueDistribution?.map(range => {
|
||||||
|
const totalRevenue = allData.orderValueDistribution.reduce((sum, r) => sum + (r.total || 0), 0);
|
||||||
|
return {
|
||||||
|
range: range.max === 'Infinity' ? `$${range.min}+` : `$${range.min}-${range.max}`,
|
||||||
|
count: range.count,
|
||||||
|
total: range.total,
|
||||||
|
percentage: ((range.count / (allData.totalOrders || 1)) * 100).toFixed(1),
|
||||||
|
revenuePercentage: ((range.total / (totalRevenue || 1)) * 100).toFixed(1)
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@@ -772,6 +808,73 @@ const OrderRangeDetails = ({ data }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{formattedDistributionData.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-4">Order Value Distribution</h3>
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="">Range</TableHead>
|
||||||
|
<TableHead className="text-right">Orders</TableHead>
|
||||||
|
<TableHead className="text-right">Total Revenue</TableHead>
|
||||||
|
<TableHead className="text-right">% of Orders</TableHead>
|
||||||
|
<TableHead className="text-right">% of Revenue</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{formattedDistributionData.map((range, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell className="font-medium whitespace-nowrap">{range.range}</TableCell>
|
||||||
|
<TableCell className="text-right whitespace-nowrap">{range.count.toLocaleString()}</TableCell>
|
||||||
|
<TableCell className="text-right whitespace-nowrap">{formatCurrency(range.total)}</TableCell>
|
||||||
|
<TableCell className="text-right whitespace-nowrap">{range.percentage}%</TableCell>
|
||||||
|
<TableCell className="text-right whitespace-nowrap">{range.revenuePercentage}%</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={formattedDistributionData} margin={{ top: 10, right: 20, left: -20, bottom: 40 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="range"
|
||||||
|
angle={-45}
|
||||||
|
textAnchor="end"
|
||||||
|
height={80}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
className="text-xs"
|
||||||
|
tickFormatter={(value) => `${value}%`}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
content={({ active, payload }) => {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
const data = payload[0].payload;
|
||||||
|
return (
|
||||||
|
<div className="bg-background border rounded-lg shadow-lg p-3">
|
||||||
|
<p className="font-medium">{data.range}</p>
|
||||||
|
<p className="text-sm">Orders: {data.count.toLocaleString()}</p>
|
||||||
|
<p className="text-sm">Revenue: {formatCurrency(data.total)}</p>
|
||||||
|
<p className="text-sm">% of Orders: {data.percentage}%</p>
|
||||||
|
<p className="text-sm">% of Revenue: {data.revenuePercentage}%</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="percentage"
|
||||||
|
name="Percentage of Orders"
|
||||||
|
fill="hsl(262.1 83.3% 57.8%)"
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user