From 90a003fccdfd44dcfdeeb2a343bfd4abb8f4b4f3 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 23 Dec 2024 23:21:54 -0500 Subject: [PATCH] Add value distribution to order range dialog --- .../klaviyo-server/services/events.service.js | 74 ++++++------ .../src/components/dashboard/StatCards.jsx | 105 +++++++++++++++++- 2 files changed, 138 insertions(+), 41 deletions(-) diff --git a/dashboard-server/klaviyo-server/services/events.service.js b/dashboard-server/klaviyo-server/services/events.service.js index 18e13a2..9e7d166 100644 --- a/dashboard-server/klaviyo-server/services/events.service.js +++ b/dashboard-server/klaviyo-server/services/events.service.js @@ -1654,22 +1654,37 @@ export class EventsService { dayStats.orderValueRange.smallestOrderId = orderId; } - // Track order value distribution - if (totalAmount < 25) { - dayStats.orderValueRange.distribution.under25.count++; - dayStats.orderValueRange.distribution.under25.total += totalAmount; - } else if (totalAmount < 50) { - dayStats.orderValueRange.distribution.under50.count++; - dayStats.orderValueRange.distribution.under50.total += totalAmount; - } else if (totalAmount < 100) { - dayStats.orderValueRange.distribution.under100.count++; - dayStats.orderValueRange.distribution.under100.total += totalAmount; - } else if (totalAmount < 200) { - dayStats.orderValueRange.distribution.under200.count++; - dayStats.orderValueRange.distribution.under200.total += totalAmount; - } else { - dayStats.orderValueRange.distribution.over200.count++; - dayStats.orderValueRange.distribution.over200.total += totalAmount; + // Track order value distribution with more granular ranges + const ranges = [ + { min: 0, max: 25 }, + { min: 25, max: 50 }, + { min: 50, max: 75 }, + { min: 75, max: 100 }, + { min: 100, max: 150 }, + { min: 150, max: 200 }, + { min: 200, max: 300 }, + { min: 300, max: 500 }, + { min: 500, max: Infinity } + ]; + + // Initialize distribution if not exists + if (!dayStats.orderValueDistribution) { + 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), smallest: Number(day.orderValueRange?.smallest || 0), largestOrderId: day.orderValueRange?.largestOrderId || 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: { - 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) - } - } - } + smallestOrderId: day.orderValueRange?.smallestOrderId || null + }, + orderValueDistribution: day.orderValueDistribution || [] })); return stats; diff --git a/dashboard/src/components/dashboard/StatCards.jsx b/dashboard/src/components/dashboard/StatCards.jsx index b0a6d6a..6eb65ed 100644 --- a/dashboard/src/components/dashboard/StatCards.jsx +++ b/dashboard/src/components/dashboard/StatCards.jsx @@ -113,7 +113,7 @@ const TimeSeriesChart = ({ return (
- + { const OrderRangeDetails = ({ data }) => { if (!data?.length) return
No data available for the selected time range.
; + // 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 => ({ timestamp: day.timestamp, largest: day.orderValueRange?.largest || 0, @@ -747,6 +772,17 @@ const OrderRangeDetails = ({ data }) => { 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 (
@@ -772,6 +808,73 @@ const OrderRangeDetails = ({ data }) => { />
+ {formattedDistributionData.length > 0 && ( +
+

Order Value Distribution

+
+ + + + Range + Orders + Total Revenue + % of Orders + % of Revenue + + + + {formattedDistributionData.map((range, index) => ( + + {range.range} + {range.count.toLocaleString()} + {formatCurrency(range.total)} + {range.percentage}% + {range.revenuePercentage}% + + ))} + +
+
+
+ + + + + `${value}%`} + /> + { + if (!active || !payload?.length) return null; + const data = payload[0].payload; + return ( +
+

{data.range}

+

Orders: {data.count.toLocaleString()}

+

Revenue: {formatCurrency(data.total)}

+

% of Orders: {data.percentage}%

+

% of Revenue: {data.revenuePercentage}%

+
+ ); + }} + /> + +
+
+
+
+ )}
); };