Fix preorder/local/hold

This commit is contained in:
2024-12-25 14:43:27 -05:00
parent 751a0b1f39
commit 108f4c0b9d
2 changed files with 100 additions and 174 deletions

View File

@@ -1512,21 +1512,21 @@ export class EventsService {
const { metric, daily = false } = params; const { metric, daily = false } = params;
console.log('[EventsService] Request params:', params); console.log('[EventsService] Request params:', params);
// Get period dates using the same logic as calculatePeriodStats // Get period dates using TimeManager to respect 1 AM day start
let periodStart, periodEnd, prevPeriodStart, prevPeriodEnd; let periodStart, periodEnd, prevPeriodStart, prevPeriodEnd;
if (params.startDate && params.endDate) { if (params.startDate && params.endDate) {
periodStart = this.timeManager.toDateTime(params.startDate); periodStart = this.timeManager.getDayStart(this.timeManager.toDateTime(params.startDate));
periodEnd = this.timeManager.toDateTime(params.endDate); periodEnd = this.timeManager.getDayEnd(this.timeManager.toDateTime(params.endDate));
const duration = periodEnd.diff(periodStart); const duration = periodEnd.diff(periodStart);
prevPeriodStart = periodStart.minus(duration); prevPeriodStart = this.timeManager.getDayStart(periodStart.minus(duration));
prevPeriodEnd = periodStart.minus({ milliseconds: 1 }); prevPeriodEnd = this.timeManager.getDayEnd(periodStart.minus({ milliseconds: 1 }));
} else if (params.timeRange) { } else if (params.timeRange) {
const range = this.timeManager.getDateRange(params.timeRange); const range = this.timeManager.getDateRange(params.timeRange);
periodStart = range.start; periodStart = range.start;
periodEnd = range.end; periodEnd = range.end;
const duration = periodEnd.diff(periodStart); const prevRange = this.timeManager.getPreviousPeriod(params.timeRange);
prevPeriodStart = periodStart.minus(duration); prevPeriodStart = prevRange.start;
prevPeriodEnd = periodStart.minus({ milliseconds: 1 }); prevPeriodEnd = prevRange.end;
} }
// Load both current and previous period data // Load both current and previous period data
@@ -1549,36 +1549,7 @@ export class EventsService {
const currentEvents = this._transformEvents(currentResponse.data || []); const currentEvents = this._transformEvents(currentResponse.data || []);
const prevEvents = this._transformEvents(prevResponse.data || []); const prevEvents = this._transformEvents(prevResponse.data || []);
// Filter events based on metric type // Initialize daily stats map with all dates in range using TimeManager's day start
const filterEvents = (events) => {
switch (metric) {
case 'pre_orders':
return events.filter(event =>
Boolean(event.event_properties?.HasPreorder ||
event.event_properties?.has_preorder ||
event.event_properties?.preorder)
);
case 'local_pickup':
return events.filter(event =>
Boolean(event.event_properties?.LocalPickup ||
event.event_properties?.local_pickup ||
event.event_properties?.pickup)
);
case 'on_hold':
return events.filter(event =>
Boolean(event.event_properties?.IsOnHold ||
event.event_properties?.is_on_hold ||
event.event_properties?.on_hold)
);
default:
return events;
}
};
const filteredCurrentEvents = filterEvents(currentEvents);
const filteredPrevEvents = filterEvents(prevEvents);
// Initialize daily stats map with all dates in range
const dailyStats = new Map(); const dailyStats = new Map();
let currentDate = periodStart; let currentDate = periodStart;
while (currentDate <= periodEnd) { while (currentDate <= periodEnd) {
@@ -1589,8 +1560,6 @@ export class EventsService {
revenue: 0, revenue: 0,
orders: 0, orders: 0,
itemCount: 0, itemCount: 0,
averageOrderValue: 0,
averageItemsPerOrder: 0,
count: 0, count: 0,
value: 0, value: 0,
percentage: 0, percentage: 0,
@@ -1598,98 +1567,73 @@ export class EventsService {
prevRevenue: 0, prevRevenue: 0,
prevOrders: 0, prevOrders: 0,
prevItemCount: 0, prevItemCount: 0,
prevAvgOrderValue: 0, prevValue: 0,
orderValueRange: { prevCount: 0,
largest: 0, prevPercentage: 0,
smallest: 0, averageOrderValue: 0,
largestOrderId: null, averageItemsPerOrder: 0,
smallestOrderId: null, prevAvgOrderValue: 0
distribution: {
under25: { count: 0, total: 0 },
under50: { count: 0, total: 0 },
under100: { count: 0, total: 0 },
under200: { count: 0, total: 0 },
over200: { count: 0, total: 0 }
}
}
}); });
currentDate = currentDate.plus({ days: 1 }); currentDate = this.timeManager.getDayStart(currentDate.plus({ days: 1 }));
} }
// Get total orders for the period (needed for percentages) // First pass: Count total orders per day using TimeManager's day boundaries
const totalOrders = currentEvents.length; for (const event of currentEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue;
// Process current period events // Get the day start for this event's time to ensure proper day assignment
const dayStart = this.timeManager.getDayStart(datetime);
const dateKey = dayStart.toFormat('yyyy-MM-dd');
if (!dailyStats.has(dateKey)) continue;
const dayStats = dailyStats.get(dateKey);
dayStats.orders++;
dailyStats.set(dateKey, dayStats);
}
// Second pass: Process filtered orders
const filterEvents = (events) => {
switch (metric) {
case 'pre_orders':
return events.filter(event => Boolean(event.event_properties?.HasPreorder));
case 'local_pickup':
return events.filter(event => Boolean(event.event_properties?.LocalPickup));
case 'on_hold':
return events.filter(event => Boolean(event.event_properties?.IsOnHold));
default:
return events;
}
};
const filteredCurrentEvents = filterEvents(currentEvents);
// Process current period filtered events using TimeManager's day boundaries
for (const event of filteredCurrentEvents) { for (const event of filteredCurrentEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime); const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue; if (!datetime) continue;
const dateKey = datetime.toFormat('yyyy-MM-dd'); // Get the day start for this event's time
const dayStart = this.timeManager.getDayStart(datetime);
const dateKey = dayStart.toFormat('yyyy-MM-dd');
if (!dailyStats.has(dateKey)) continue; if (!dailyStats.has(dateKey)) continue;
const dayStats = dailyStats.get(dateKey); const dayStats = dailyStats.get(dateKey);
const props = event.event_properties || {}; const props = event.event_properties || {};
const totalAmount = Number(props.TotalAmount || 0); const totalAmount = Number(props.TotalAmount || 0);
const items = props.Items || []; const items = props.Items || [];
const orderId = props.OrderId;
dayStats.revenue += totalAmount;
dayStats.orders++;
dayStats.itemCount += items.length;
dayStats.value += totalAmount;
dayStats.count++; dayStats.count++;
dayStats.totalOrders = totalOrders; dayStats.value += totalAmount;
dayStats.percentage = (dayStats.count / totalOrders) * 100; dayStats.itemCount += items.length;
dayStats.averageOrderValue = dayStats.revenue / dayStats.orders; dayStats.percentage = (dayStats.count / dayStats.orders) * 100;
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders; dayStats.averageOrderValue = dayStats.value / dayStats.count;
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.count;
// Track daily order value range dailyStats.set(dateKey, dayStats);
if (totalAmount > dayStats.orderValueRange.largest) {
dayStats.orderValueRange.largest = totalAmount;
dayStats.orderValueRange.largestOrderId = orderId;
}
if (totalAmount > 0) {
if (dayStats.orderValueRange.smallest === 0 || totalAmount < dayStats.orderValueRange.smallest) {
dayStats.orderValueRange.smallest = totalAmount;
dayStats.orderValueRange.smallestOrderId = orderId;
} }
// Track order value distribution with more granular ranges // Initialize and process previous period stats using TimeManager's day boundaries
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;
}
}
}
// Process previous period events
const prevDailyStats = new Map(); const prevDailyStats = new Map();
let prevDate = prevPeriodStart; let prevDate = prevPeriodStart;
while (prevDate <= prevPeriodEnd) { while (prevDate <= prevPeriodEnd) {
@@ -1697,37 +1641,47 @@ export class EventsService {
prevDailyStats.set(dateKey, { prevDailyStats.set(dateKey, {
date: prevDate.toISO(), date: prevDate.toISO(),
timestamp: dateKey, timestamp: dateKey,
revenue: 0,
orders: 0, orders: 0,
itemCount: 0, count: 0,
value: 0, value: 0,
count: 0 percentage: 0
}); });
prevDate = prevDate.plus({ days: 1 }); prevDate = this.timeManager.getDayStart(prevDate.plus({ days: 1 }));
} }
// Get total orders for previous period // First pass for previous period: Count total orders
const prevTotalOrders = prevEvents.length; for (const event of prevEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue;
// Process previous period events const dayStart = this.timeManager.getDayStart(datetime);
const dateKey = dayStart.toFormat('yyyy-MM-dd');
if (!prevDailyStats.has(dateKey)) continue;
const dayStats = prevDailyStats.get(dateKey);
dayStats.orders++;
prevDailyStats.set(dateKey, dayStats);
}
// Second pass for previous period: Process filtered orders
const filteredPrevEvents = filterEvents(prevEvents);
for (const event of filteredPrevEvents) { for (const event of filteredPrevEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime); const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue; if (!datetime) continue;
const dateKey = datetime.toFormat('yyyy-MM-dd'); const dayStart = this.timeManager.getDayStart(datetime);
const dateKey = dayStart.toFormat('yyyy-MM-dd');
if (!prevDailyStats.has(dateKey)) continue; if (!prevDailyStats.has(dateKey)) continue;
const dayStats = prevDailyStats.get(dateKey); const dayStats = prevDailyStats.get(dateKey);
const props = event.event_properties || {}; const props = event.event_properties || {};
const totalAmount = Number(props.TotalAmount || 0); const totalAmount = Number(props.TotalAmount || 0);
const items = props.Items || [];
dayStats.revenue += totalAmount;
dayStats.orders++;
dayStats.itemCount += items.length;
dayStats.value += totalAmount;
dayStats.count++; dayStats.count++;
dayStats.percentage = (dayStats.count / prevTotalOrders) * 100; dayStats.value += totalAmount;
dayStats.percentage = (dayStats.count / dayStats.orders) * 100;
prevDailyStats.set(dateKey, dayStats);
} }
// Map previous period data to current period days // Map previous period data to current period days
@@ -1742,13 +1696,11 @@ export class EventsService {
if (prevDayStats && currentDayStats) { if (prevDayStats && currentDayStats) {
const dayStats = dailyStats.get(currentDayStats.timestamp); const dayStats = dailyStats.get(currentDayStats.timestamp);
if (dayStats) { if (dayStats) {
dayStats.prevRevenue = prevDayStats.revenue;
dayStats.prevOrders = prevDayStats.orders;
dayStats.prevItemCount = prevDayStats.itemCount;
dayStats.prevValue = prevDayStats.value; dayStats.prevValue = prevDayStats.value;
dayStats.prevCount = prevDayStats.count; dayStats.prevCount = prevDayStats.count;
dayStats.prevOrders = prevDayStats.orders;
dayStats.prevPercentage = prevDayStats.percentage; dayStats.prevPercentage = prevDayStats.percentage;
dayStats.prevAvgOrderValue = prevDayStats.orders > 0 ? prevDayStats.revenue / prevDayStats.orders : 0; dayStats.prevAvgOrderValue = prevDayStats.count > 0 ? prevDayStats.value / prevDayStats.count : 0;
dailyStats.set(currentDayStats.timestamp, dayStats); dailyStats.set(currentDayStats.timestamp, dayStats);
} }
} }
@@ -1759,29 +1711,19 @@ export class EventsService {
.sort((a, b) => a.date.localeCompare(b.date)) .sort((a, b) => a.date.localeCompare(b.date))
.map(day => ({ .map(day => ({
...day, ...day,
revenue: Number(day.revenue || 0), revenue: Number(day.value || 0),
orders: Number(day.orders || 0), orders: Number(day.orders || 0),
itemCount: Number(day.itemCount || 0), itemCount: Number(day.itemCount || 0),
averageOrderValue: Number(day.averageOrderValue || 0),
averageItemsPerOrder: Number(day.averageItemsPerOrder || 0),
count: Number(day.count || 0), count: Number(day.count || 0),
value: Number(day.value || 0), value: Number(day.value || 0),
percentage: Number(day.percentage || 0), percentage: Number(day.percentage || 0),
totalOrders: Number(day.totalOrders || 0), averageOrderValue: Number(day.averageOrderValue || 0),
prevRevenue: Number(day.prevRevenue || 0), averageItemsPerOrder: Number(day.averageItemsPerOrder || 0),
prevOrders: Number(day.prevOrders || 0),
prevItemCount: Number(day.prevItemCount || 0),
prevValue: Number(day.prevValue || 0), prevValue: Number(day.prevValue || 0),
prevCount: Number(day.prevCount || 0), prevCount: Number(day.prevCount || 0),
prevOrders: Number(day.prevOrders || 0),
prevPercentage: Number(day.prevPercentage || 0), prevPercentage: Number(day.prevPercentage || 0),
prevAvgOrderValue: Number(day.prevAvgOrderValue || 0), prevAvgOrderValue: Number(day.prevAvgOrderValue || 0)
orderValueRange: {
largest: Number(day.orderValueRange?.largest || 0),
smallest: Number(day.orderValueRange?.smallest || 0),
largestOrderId: day.orderValueRange?.largestOrderId || null,
smallestOrderId: day.orderValueRange?.smallestOrderId || null
},
orderValueDistribution: day.orderValueDistribution || []
})); }));
return stats; return stats;

View File

@@ -532,7 +532,6 @@ const ShippingDetails = ({ data }) => {
const OrderTypeDetails = ({ data, type }) => { const OrderTypeDetails = ({ data, type }) => {
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>;
// Access the correct daily data using the 'type' prop
const timeSeriesData = data.map(day => ({ const timeSeriesData = data.map(day => ({
timestamp: day.timestamp, timestamp: day.timestamp,
count: day.count, count: day.count,
@@ -540,31 +539,16 @@ const OrderTypeDetails = ({ data, type }) => {
percentage: day.percentage percentage: day.percentage
})); }));
const typeLabels = { const typeColors = {
'pre_orders': 'Pre-Orders', 'pre_orders': 'hsl(47.9 95.8% 53.1%)', // Yellow for pre-orders
'local_pickup': 'Local Pickup', 'local_pickup': 'hsl(192.2 70.1% 51.4%)', // Cyan for local pickup
'on_hold': 'On Hold' 'on_hold': 'hsl(346.8 77.2% 49.8%)' // Red for on hold
}; };
const color = typeColors[type];
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="grid grid-cols-2 gap-4">
<StatCard
title={`Total ${typeLabels[type]}`}
value={data[0]?.totalOrders?.toLocaleString() || 0}
description={`${data[0]?.percentage?.toFixed(1) || 0}% of all orders`}
colorClass="text-blue-600 dark:text-blue-400"
icon={ShoppingCart}
/>
<StatCard
title="Total Value"
value={formatCurrency(data[0]?.value || 0)}
description={`Avg: ${formatCurrency((data[0]?.value || 0) / (data[0]?.count || 1))}`}
colorClass="text-green-600 dark:text-green-400"
icon={DollarSign}
/>
</div>
<div> <div>
<h3 className="text-lg font-medium mb-4">Daily Order Count</h3> <h3 className="text-lg font-medium mb-4">Daily Order Count</h3>
<TimeSeriesChart <TimeSeriesChart
@@ -572,7 +556,7 @@ const OrderTypeDetails = ({ data, type }) => {
dataKey="count" dataKey="count"
name="Orders" name="Orders"
type="bar" type="bar"
color="hsl(221.2 83.2% 53.3%)" color={color}
/> />
</div> </div>
@@ -582,7 +566,7 @@ const OrderTypeDetails = ({ data, type }) => {
data={timeSeriesData} data={timeSeriesData}
dataKey="value" dataKey="value"
name="Value" name="Value"
color="hsl(142.1 76.2% 36.3%)" color={color}
valueFormatter={(value) => formatCurrency(value)} valueFormatter={(value) => formatCurrency(value)}
/> />
</div> </div>
@@ -593,7 +577,7 @@ const OrderTypeDetails = ({ data, type }) => {
data={timeSeriesData} data={timeSeriesData}
dataKey="percentage" dataKey="percentage"
name="Percentage" name="Percentage"
color="hsl(262.1 83.3% 57.8%)" color={color}
valueFormatter={(value) => `${value.toFixed(1)}%`} valueFormatter={(value) => `${value.toFixed(1)}%`}
/> />
</div> </div>
@@ -1622,7 +1606,7 @@ const StatCards = ({
<StatCard <StatCard
title="Pre-Orders" title="Pre-Orders"
value={stats?.orderTypes?.preOrders?.percentage?.toFixed(1) || '0'} value={((stats?.orderTypes?.preOrders?.count / stats?.orderCount) * 100)?.toFixed(1) || '0'}
valueSuffix="%" valueSuffix="%"
description={`${stats?.orderTypes?.preOrders?.count || 0} orders`} description={`${stats?.orderTypes?.preOrders?.count || 0} orders`}
colorClass="text-yellow-600 dark:text-yellow-400" colorClass="text-yellow-600 dark:text-yellow-400"
@@ -1634,7 +1618,7 @@ const StatCards = ({
<StatCard <StatCard
title="Local Pickup" title="Local Pickup"
value={stats?.orderTypes?.localPickup?.percentage?.toFixed(1) || '0'} value={((stats?.orderTypes?.localPickup?.count / stats?.orderCount) * 100)?.toFixed(1) || '0'}
valueSuffix="%" valueSuffix="%"
description={`${stats?.orderTypes?.localPickup?.count || 0} orders`} description={`${stats?.orderTypes?.localPickup?.count || 0} orders`}
colorClass="text-cyan-600 dark:text-cyan-400" colorClass="text-cyan-600 dark:text-cyan-400"
@@ -1646,7 +1630,7 @@ const StatCards = ({
<StatCard <StatCard
title="On Hold" title="On Hold"
value={stats?.orderTypes?.heldItems?.percentage?.toFixed(1) || '0'} value={((stats?.orderTypes?.heldItems?.count / stats?.orderCount) * 100)?.toFixed(1) || '0'}
valueSuffix="%" valueSuffix="%"
description={`${stats?.orderTypes?.heldItems?.count || 0} orders`} description={`${stats?.orderTypes?.heldItems?.count || 0} orders`}
colorClass="text-red-600 dark:text-red-400" colorClass="text-red-600 dark:text-red-400"