Fix time ranges
This commit is contained in:
@@ -1417,503 +1417,45 @@ export class EventsService {
|
||||
const { metric, daily = false } = params;
|
||||
|
||||
// Get period dates
|
||||
let periodStart, periodEnd;
|
||||
let periodStart, periodEnd, prevPeriodStart, prevPeriodEnd;
|
||||
if (params.startDate && params.endDate) {
|
||||
periodStart = this.timeManager.toDateTime(params.startDate);
|
||||
periodEnd = this.timeManager.toDateTime(params.endDate);
|
||||
const duration = periodEnd.diff(periodStart);
|
||||
prevPeriodEnd = periodStart.minus({ milliseconds: 1 });
|
||||
prevPeriodStart = prevPeriodEnd.minus(duration);
|
||||
} else if (params.timeRange) {
|
||||
const range = this.timeManager.getDateRange(params.timeRange);
|
||||
// Normalize time range to use 'last' prefix instead of 'previous'
|
||||
const normalizedTimeRange = params.timeRange.replace('previous', 'last');
|
||||
const range = this.timeManager.getDateRange(normalizedTimeRange);
|
||||
const prevRange = this.timeManager.getPreviousPeriod(normalizedTimeRange);
|
||||
if (!range || !prevRange) {
|
||||
throw new Error('Invalid time range specified');
|
||||
}
|
||||
periodStart = range.start;
|
||||
periodEnd = range.end;
|
||||
prevPeriodStart = prevRange.start;
|
||||
prevPeriodEnd = prevRange.end;
|
||||
}
|
||||
|
||||
// Load data based on metric type
|
||||
let mainData, orderData;
|
||||
switch (metric) {
|
||||
case 'revenue':
|
||||
case 'orders':
|
||||
case 'average_order':
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
});
|
||||
break;
|
||||
case 'refunds':
|
||||
// Get refund events
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.PAYMENT_REFUNDED
|
||||
});
|
||||
|
||||
const refundEvents = this._transformEvents(mainData.data);
|
||||
const dailyRefundStats = new Map();
|
||||
let currentDate = periodStart;
|
||||
|
||||
// Initialize daily stats for the entire date range
|
||||
while (currentDate <= periodEnd) {
|
||||
const dateKey = currentDate.toFormat('yyyy-MM-dd');
|
||||
dailyRefundStats.set(dateKey, {
|
||||
date: currentDate.toISO(),
|
||||
timestamp: dateKey,
|
||||
total: 0,
|
||||
count: 0,
|
||||
reasons: {},
|
||||
items: []
|
||||
});
|
||||
currentDate = currentDate.plus({ days: 1 });
|
||||
}
|
||||
|
||||
// Process refund events
|
||||
refundEvents.forEach(event => {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) return;
|
||||
|
||||
const dateKey = datetime.toFormat('yyyy-MM-dd');
|
||||
if (!dailyRefundStats.has(dateKey)) return;
|
||||
|
||||
const stats = dailyRefundStats.get(dateKey);
|
||||
const props = event.event_properties || {};
|
||||
const amount = Number(props.PaymentAmount || 0);
|
||||
const reason = props.CancelReason || props.OrderMessage || 'No reason provided';
|
||||
|
||||
stats.total += amount;
|
||||
stats.count++;
|
||||
stats.reasons[reason] = (stats.reasons[reason] || 0) + 1;
|
||||
stats.items.push({
|
||||
orderId: props.OrderId,
|
||||
amount,
|
||||
reason,
|
||||
datetime: datetime.toISO()
|
||||
});
|
||||
|
||||
// Add these properties to match what RefundDetails component expects
|
||||
stats.refunds = {
|
||||
total: stats.total,
|
||||
count: stats.count,
|
||||
reasons: stats.reasons
|
||||
};
|
||||
});
|
||||
|
||||
// Format daily stats
|
||||
const formattedRefundStats = Array.from(dailyRefundStats.values())
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Calculate total stats
|
||||
const totalRefunds = refundEvents.reduce((sum, event) => {
|
||||
const props = event.event_properties || {};
|
||||
return sum + Number(props.PaymentAmount || 0);
|
||||
}, 0);
|
||||
|
||||
const totalReasons = refundEvents.reduce((acc, event) => {
|
||||
const props = event.event_properties || {};
|
||||
const reason = props.CancelReason || props.OrderMessage || 'No reason provided';
|
||||
acc[reason] = (acc[reason] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Store totals in both meta and first day
|
||||
const totalStats = {
|
||||
meta: {
|
||||
refunds: {
|
||||
total: totalRefunds,
|
||||
count: refundEvents.length,
|
||||
reasons: totalReasons
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add totals to first day for the details view
|
||||
if (formattedRefundStats.length > 0) {
|
||||
formattedRefundStats[0] = {
|
||||
...formattedRefundStats[0],
|
||||
refunds: {
|
||||
...formattedRefundStats[0].refunds,
|
||||
total: totalRefunds,
|
||||
count: refundEvents.length,
|
||||
reasons: totalReasons
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Return array with meta property
|
||||
return Object.assign(formattedRefundStats, totalStats);
|
||||
case 'shipping':
|
||||
// Get shipped orders
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.SHIPPED_ORDER
|
||||
});
|
||||
|
||||
// Transform shipped orders data with proper error handling
|
||||
if (mainData?.data) {
|
||||
try {
|
||||
const transformedShipped = this._transformEvents(mainData.data).filter(event => {
|
||||
const props = event.event_properties || {};
|
||||
// Log each event's shipping properties
|
||||
console.log('[EventsService] Shipping properties for event:', {
|
||||
id: event.id,
|
||||
orderId: props.OrderId,
|
||||
shipMethod: props.ShipMethod,
|
||||
shippingMethod: props.ShippingMethod,
|
||||
state: props.ShippingState,
|
||||
country: props.ShippingCountry,
|
||||
isValid: !!(props.OrderId && (props.ShipMethod || props.ShippingMethod))
|
||||
});
|
||||
// Ensure we have basic shipping data
|
||||
return props.OrderId && (props.ShipMethod || props.ShippingMethod);
|
||||
});
|
||||
|
||||
const shippedCount = transformedShipped.length;
|
||||
console.log('[EventsService] Processing shipping data:', {
|
||||
totalEvents: mainData.data.length,
|
||||
validEvents: shippedCount
|
||||
});
|
||||
|
||||
// Track shipping methods and locations with error handling
|
||||
const methodMap = new Map();
|
||||
const stateMap = new Map();
|
||||
const countryMap = new Map();
|
||||
|
||||
transformedShipped.forEach(shipped => {
|
||||
const props = shipped.event_properties || {};
|
||||
|
||||
// Track shipping methods with normalization
|
||||
const method = (props.ShipMethod || props.ShippingMethod || 'Unknown').trim();
|
||||
methodMap.set(method, (methodMap.get(method) || 0) + 1);
|
||||
|
||||
// Track locations with proper validation
|
||||
const state = props.ShippingState?.trim();
|
||||
const country = props.ShippingCountry?.trim();
|
||||
|
||||
// For international orders, use country as the "state"
|
||||
const locationKey = state || country;
|
||||
if (locationKey) {
|
||||
if (!stateMap.has(locationKey)) {
|
||||
stateMap.set(locationKey, { count: 0, country: country || 'Unknown' });
|
||||
}
|
||||
stateMap.get(locationKey).count++;
|
||||
|
||||
// Track by country
|
||||
if (country) {
|
||||
if (!countryMap.has(country)) {
|
||||
countryMap.set(country, { count: 0, states: new Set() });
|
||||
}
|
||||
const countryStats = countryMap.get(country);
|
||||
countryStats.count++;
|
||||
countryStats.states.add(locationKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Format the data for the frontend with validation
|
||||
const methodStats = Array.from(methodMap.entries())
|
||||
.map(([method, count]) => ({
|
||||
name: method,
|
||||
value: count,
|
||||
percentage: shippedCount > 0 ? (count / shippedCount) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
mainData.meta = {
|
||||
...mainData.meta,
|
||||
shipping: {
|
||||
shippedCount,
|
||||
methods: Object.fromEntries(methodMap),
|
||||
methodStats,
|
||||
locations: {
|
||||
total: stateMap.size,
|
||||
byState: Array.from(stateMap.entries())
|
||||
.map(([location, data]) => ({
|
||||
state: location,
|
||||
country: data.country,
|
||||
count: data.count,
|
||||
percentage: shippedCount > 0 ? (data.count / shippedCount) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count),
|
||||
byCountry: Array.from(countryMap.entries())
|
||||
.map(([country, data]) => ({
|
||||
country,
|
||||
count: data.count,
|
||||
states: Array.from(data.states),
|
||||
percentage: shippedCount > 0 ? (data.count / shippedCount) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log shipping stats for debugging
|
||||
console.log('[EventsService] Shipping stats:', {
|
||||
shippedCount,
|
||||
methodCount: methodStats.length,
|
||||
stateCount: stateMap.size,
|
||||
countryCount: countryMap.size,
|
||||
methods: methodStats,
|
||||
states: mainData.meta.shipping.locations.byState.length,
|
||||
countries: mainData.meta.shipping.locations.byCountry.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[EventsService] Error processing shipping data:', error);
|
||||
// Provide empty but valid data structure on error
|
||||
mainData.meta = {
|
||||
...mainData.meta,
|
||||
shipping: {
|
||||
shippedCount: 0,
|
||||
methods: {},
|
||||
methodStats: [],
|
||||
locations: {
|
||||
total: 0,
|
||||
byState: [],
|
||||
byCountry: []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'order_range':
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
});
|
||||
|
||||
const orderEvents = this._transformEvents(mainData.data);
|
||||
const dailyOrderStats = new Map();
|
||||
let orderCurrentDate = periodStart;
|
||||
|
||||
// Initialize daily stats for the entire date range
|
||||
while (orderCurrentDate <= periodEnd) {
|
||||
const dateKey = orderCurrentDate.toFormat('yyyy-MM-dd');
|
||||
dailyOrderStats.set(dateKey, {
|
||||
date: orderCurrentDate.toISO(),
|
||||
timestamp: dateKey,
|
||||
orderValueRange: {
|
||||
largest: { value: 0, orderId: null },
|
||||
smallest: { value: Infinity, orderId: null },
|
||||
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 }
|
||||
}
|
||||
}
|
||||
});
|
||||
orderCurrentDate = orderCurrentDate.plus({ days: 1 });
|
||||
}
|
||||
|
||||
// Process order events
|
||||
orderEvents.forEach(event => {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) return;
|
||||
|
||||
const dateKey = datetime.toFormat('yyyy-MM-dd');
|
||||
if (!dailyOrderStats.has(dateKey)) return;
|
||||
|
||||
const stats = dailyOrderStats.get(dateKey);
|
||||
const props = event.event_properties || {};
|
||||
const amount = Number(props.TotalAmount || 0);
|
||||
const orderId = props.OrderId;
|
||||
|
||||
// Update largest/smallest
|
||||
if (amount > stats.orderValueRange.largest.value) {
|
||||
stats.orderValueRange.largest = { value: amount, orderId };
|
||||
}
|
||||
if (amount < stats.orderValueRange.smallest.value) {
|
||||
stats.orderValueRange.smallest = { value: amount, orderId };
|
||||
}
|
||||
|
||||
// Update distribution
|
||||
if (amount <= 25) {
|
||||
stats.orderValueRange.distribution.under25.count++;
|
||||
stats.orderValueRange.distribution.under25.total += amount;
|
||||
} else if (amount <= 50) {
|
||||
stats.orderValueRange.distribution.under50.count++;
|
||||
stats.orderValueRange.distribution.under50.total += amount;
|
||||
} else if (amount <= 100) {
|
||||
stats.orderValueRange.distribution.under100.count++;
|
||||
stats.orderValueRange.distribution.under100.total += amount;
|
||||
} else if (amount <= 200) {
|
||||
stats.orderValueRange.distribution.under200.count++;
|
||||
stats.orderValueRange.distribution.under200.total += amount;
|
||||
} else {
|
||||
stats.orderValueRange.distribution.over200.count++;
|
||||
stats.orderValueRange.distribution.over200.total += amount;
|
||||
}
|
||||
});
|
||||
|
||||
// Format daily stats
|
||||
const formattedOrderStats = Array.from(dailyOrderStats.values())
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Calculate period totals
|
||||
const periodStats = {
|
||||
largest: { value: 0, orderId: null },
|
||||
smallest: { value: Infinity, orderId: null },
|
||||
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 }
|
||||
}
|
||||
};
|
||||
|
||||
// Aggregate all daily stats
|
||||
orderEvents.forEach(event => {
|
||||
const props = event.event_properties || {};
|
||||
const amount = Number(props.TotalAmount || 0);
|
||||
const orderId = props.OrderId;
|
||||
|
||||
if (amount > periodStats.largest.value) {
|
||||
periodStats.largest = { value: amount, orderId };
|
||||
}
|
||||
if (amount < periodStats.smallest.value) {
|
||||
periodStats.smallest = { value: amount, orderId };
|
||||
}
|
||||
|
||||
if (amount <= 25) {
|
||||
periodStats.distribution.under25.count++;
|
||||
periodStats.distribution.under25.total += amount;
|
||||
} else if (amount <= 50) {
|
||||
periodStats.distribution.under50.count++;
|
||||
periodStats.distribution.under50.total += amount;
|
||||
} else if (amount <= 100) {
|
||||
periodStats.distribution.under100.count++;
|
||||
periodStats.distribution.under100.total += amount;
|
||||
} else if (amount <= 200) {
|
||||
periodStats.distribution.under200.count++;
|
||||
periodStats.distribution.under200.total += amount;
|
||||
} else {
|
||||
periodStats.distribution.over200.count++;
|
||||
periodStats.distribution.over200.total += amount;
|
||||
}
|
||||
});
|
||||
|
||||
// Add period totals to first day and meta
|
||||
if (formattedOrderStats.length > 0) {
|
||||
formattedOrderStats[0].orderValueRange = {
|
||||
...formattedOrderStats[0].orderValueRange,
|
||||
...periodStats
|
||||
};
|
||||
}
|
||||
|
||||
return Object.assign(formattedOrderStats, { meta: { orderValueRange: periodStats } });
|
||||
case 'refunds':
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.PAYMENT_REFUNDED
|
||||
});
|
||||
break;
|
||||
case 'cancellations':
|
||||
// Get cancellation events
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.CANCELED_ORDER
|
||||
});
|
||||
|
||||
const cancelEvents = this._transformEvents(mainData.data);
|
||||
const dailyCancelStats = new Map();
|
||||
let cancelCurrentDate = periodStart;
|
||||
|
||||
// Initialize daily stats for the entire date range
|
||||
while (cancelCurrentDate <= periodEnd) {
|
||||
const dateKey = cancelCurrentDate.toFormat('yyyy-MM-dd');
|
||||
dailyCancelStats.set(dateKey, {
|
||||
date: cancelCurrentDate.toISO(),
|
||||
timestamp: dateKey,
|
||||
total: 0,
|
||||
count: 0,
|
||||
reasons: {},
|
||||
items: []
|
||||
});
|
||||
cancelCurrentDate = cancelCurrentDate.plus({ days: 1 });
|
||||
}
|
||||
|
||||
// Process cancellation events
|
||||
cancelEvents.forEach(event => {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) return;
|
||||
|
||||
const dateKey = datetime.toFormat('yyyy-MM-dd');
|
||||
if (!dailyCancelStats.has(dateKey)) return;
|
||||
|
||||
const stats = dailyCancelStats.get(dateKey);
|
||||
const props = event.event_properties || {};
|
||||
const amount = Number(props.TotalAmount || 0);
|
||||
const reason = props.CancelReason || props.OrderMessage || 'No reason provided';
|
||||
|
||||
stats.total += amount;
|
||||
stats.count++;
|
||||
stats.reasons[reason] = (stats.reasons[reason] || 0) + 1;
|
||||
stats.items.push({
|
||||
orderId: props.OrderId,
|
||||
amount,
|
||||
reason,
|
||||
datetime: datetime.toISO()
|
||||
});
|
||||
|
||||
// Add these properties to match what CancellationsDetails component expects
|
||||
stats.canceledOrders = {
|
||||
total: stats.total,
|
||||
count: stats.count,
|
||||
reasons: stats.reasons
|
||||
};
|
||||
});
|
||||
|
||||
// Format daily stats
|
||||
const formattedCancelStats = Array.from(dailyCancelStats.values())
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Calculate total stats
|
||||
const cancelTotalAmount = cancelEvents.reduce((sum, event) => {
|
||||
const props = event.event_properties || {};
|
||||
return sum + Number(props.TotalAmount || 0);
|
||||
}, 0);
|
||||
|
||||
const cancelReasonTotals = cancelEvents.reduce((acc, event) => {
|
||||
const props = event.event_properties || {};
|
||||
const reason = props.CancelReason || props.OrderMessage || 'No reason provided';
|
||||
acc[reason] = (acc[reason] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Store totals in both meta and first day
|
||||
const cancelTotalStats = {
|
||||
meta: {
|
||||
canceledOrders: {
|
||||
total: cancelTotalAmount,
|
||||
count: cancelEvents.length,
|
||||
reasons: cancelReasonTotals
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add totals to first day for the details view
|
||||
if (formattedCancelStats.length > 0) {
|
||||
formattedCancelStats[0] = {
|
||||
...formattedCancelStats[0],
|
||||
canceledOrders: {
|
||||
...formattedCancelStats[0].canceledOrders,
|
||||
total: cancelTotalAmount,
|
||||
count: cancelEvents.length,
|
||||
reasons: cancelReasonTotals
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Return array with meta property
|
||||
return Object.assign(formattedCancelStats, cancelTotalStats);
|
||||
default:
|
||||
mainData = await this.getEvents({
|
||||
...params,
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
});
|
||||
}
|
||||
// Load both current and previous period data
|
||||
const [currentResponse, prevResponse] = await Promise.all([
|
||||
this.getEvents({
|
||||
...params,
|
||||
startDate: periodStart.toISO(),
|
||||
endDate: periodEnd.toISO(),
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
}),
|
||||
this.getEvents({
|
||||
metricId: METRIC_IDS.PLACED_ORDER,
|
||||
startDate: prevPeriodStart.toISO(),
|
||||
endDate: prevPeriodEnd.toISO()
|
||||
})
|
||||
]);
|
||||
|
||||
// Transform events
|
||||
const transformedData = this._transformEvents(mainData.data);
|
||||
const currentEvents = this._transformEvents(currentResponse.data || []);
|
||||
const prevEvents = this._transformEvents(prevResponse.data || []);
|
||||
|
||||
// Initialize daily stats map with all dates in range
|
||||
const dailyStats = new Map();
|
||||
@@ -1928,19 +1470,15 @@ export class EventsService {
|
||||
itemCount: 0,
|
||||
averageOrderValue: 0,
|
||||
averageItemsPerOrder: 0,
|
||||
refunds: { total: 0, count: 0, reasons: {} },
|
||||
shipping: { count: 0, locations: [], methods: {} },
|
||||
orderTypes: {
|
||||
preOrders: { count: 0, value: 0, percentage: 0 },
|
||||
localPickup: { count: 0, value: 0, percentage: 0 },
|
||||
heldItems: { count: 0, value: 0, percentage: 0 }
|
||||
}
|
||||
prevRevenue: 0,
|
||||
prevOrders: 0,
|
||||
prevAvgOrderValue: 0
|
||||
});
|
||||
currentDate = currentDate.plus({ days: 1 });
|
||||
}
|
||||
|
||||
// Process events into daily stats
|
||||
for (const event of transformedData) {
|
||||
// Process current period events
|
||||
for (const event of currentEvents) {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) continue;
|
||||
|
||||
@@ -1949,106 +1487,42 @@ export class EventsService {
|
||||
|
||||
const dayStats = dailyStats.get(dateKey);
|
||||
const props = event.event_properties || {};
|
||||
const totalAmount = Number(props.TotalAmount || 0);
|
||||
const items = props.Items || [];
|
||||
|
||||
switch (metric) {
|
||||
case 'revenue':
|
||||
case 'orders':
|
||||
case 'average_order':
|
||||
case 'order_range':
|
||||
dayStats.revenue += Number(props.TotalAmount || 0);
|
||||
dayStats.orders++;
|
||||
dayStats.itemCount += (props.Items || []).length;
|
||||
// Calculate daily averages immediately
|
||||
dayStats.averageOrderValue = dayStats.revenue / dayStats.orders;
|
||||
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
|
||||
break;
|
||||
case 'shipping':
|
||||
const shippingMethod = props.ShipMethod || props.ShippingMethod || 'Unknown';
|
||||
dayStats.shipping.count++;
|
||||
dayStats.shipping.methods[shippingMethod] = (dayStats.shipping.methods[shippingMethod] || 0) + 1;
|
||||
|
||||
if (props.ShippingState) {
|
||||
dayStats.shipping.locations.push({
|
||||
state: props.ShippingState,
|
||||
country: props.ShippingCountry || 'Unknown',
|
||||
count: 1
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'pre_orders':
|
||||
case 'local_pickup':
|
||||
case 'on_hold':
|
||||
const typeKey = metric === 'pre_orders' ? 'preOrders' :
|
||||
metric === 'local_pickup' ? 'localPickup' : 'heldItems';
|
||||
const isPreOrder = metric === 'pre_orders' && props.HasPreorder;
|
||||
const isLocalPickup = metric === 'local_pickup' && props.LocalPickup;
|
||||
const isOnHold = metric === 'on_hold' && props.IsOnHold;
|
||||
|
||||
if (isPreOrder || isLocalPickup || isOnHold) {
|
||||
const amount = Number(props.TotalAmount || 0);
|
||||
dayStats.orderTypes[typeKey].count++;
|
||||
dayStats.orderTypes[typeKey].value += amount;
|
||||
dayStats.orderTypes[typeKey].percentage = (dayStats.orderTypes[typeKey].count / dayStats.orders) * 100;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
dailyStats.set(dateKey, dayStats);
|
||||
dayStats.revenue += totalAmount;
|
||||
dayStats.orders++;
|
||||
dayStats.itemCount += items.length;
|
||||
dayStats.averageOrderValue = dayStats.revenue / dayStats.orders;
|
||||
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
|
||||
}
|
||||
|
||||
// Calculate period totals separately after processing daily stats
|
||||
const totalOrders = transformedData.length;
|
||||
const totalRevenue = transformedData.reduce((sum, event) => {
|
||||
// Process previous period events and map them to corresponding current period dates
|
||||
const daysDiff = periodEnd.diff(periodStart, 'days').days;
|
||||
for (const event of prevEvents) {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) continue;
|
||||
|
||||
// Calculate the corresponding date in the current period
|
||||
const daysFromStart = datetime.diff(prevPeriodStart, 'days').days;
|
||||
const correspondingDate = periodStart.plus({ days: daysFromStart });
|
||||
const dateKey = correspondingDate.toFormat('yyyy-MM-dd');
|
||||
|
||||
if (!dailyStats.has(dateKey)) continue;
|
||||
|
||||
const dayStats = dailyStats.get(dateKey);
|
||||
const props = event.event_properties || {};
|
||||
return sum + Number(props.TotalAmount || 0);
|
||||
}, 0);
|
||||
const totalItems = transformedData.reduce((sum, event) => {
|
||||
const props = event.event_properties || {};
|
||||
return sum + (props.Items || []).length;
|
||||
}, 0);
|
||||
const totalAmount = Number(props.TotalAmount || 0);
|
||||
|
||||
dayStats.prevRevenue += totalAmount;
|
||||
dayStats.prevOrders++;
|
||||
dayStats.prevAvgOrderValue = dayStats.prevRevenue / dayStats.prevOrders;
|
||||
}
|
||||
|
||||
// Convert to array and sort by date
|
||||
const stats = Array.from(dailyStats.values())
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// If not requesting daily data, aggregate the stats
|
||||
if (!daily) {
|
||||
const aggregated = {
|
||||
totalOrders,
|
||||
totalRevenue,
|
||||
totalItems,
|
||||
averageOrderValue: totalOrders > 0 ? totalRevenue / totalOrders : 0,
|
||||
averageItemsPerOrder: totalOrders > 0 ? totalItems / totalOrders : 0,
|
||||
orderCount: totalOrders
|
||||
};
|
||||
|
||||
// Add meta data for specific metrics
|
||||
if (metric === 'shipping' && mainData.meta?.shipping) {
|
||||
aggregated.shipping = mainData.meta.shipping;
|
||||
} else if (['pre_orders', 'local_pickup', 'on_hold'].includes(metric) && mainData.meta?.orderType) {
|
||||
const typeKey = metric === 'pre_orders' ? 'preOrders' :
|
||||
metric === 'local_pickup' ? 'localPickup' : 'heldItems';
|
||||
aggregated.orderTypes = {
|
||||
[typeKey]: mainData.meta.orderType
|
||||
};
|
||||
}
|
||||
|
||||
// For average_order metric, return array with daily stats included
|
||||
if (metric === 'average_order') {
|
||||
return [{
|
||||
orderCount: totalOrders,
|
||||
averageOrderValue: totalOrders > 0 ? totalRevenue / totalOrders : 0,
|
||||
averageItemsPerOrder: totalOrders > 0 ? totalItems / totalOrders : 0,
|
||||
totalRevenue: totalRevenue,
|
||||
totalItems: totalItems,
|
||||
orders: totalOrders,
|
||||
dailyStats: stats // Include the daily stats
|
||||
}];
|
||||
}
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[EventsService] Error calculating detailed stats:', error);
|
||||
|
||||
@@ -310,71 +310,80 @@ export class TimeManager {
|
||||
end: this.getDayEnd(twoDaysAgo)
|
||||
};
|
||||
}
|
||||
case 'last7days': {
|
||||
// If current period is days 0-6 (e.g., 12/11 1am - 12/17 12:59am)
|
||||
// Previous should be days 7-13 (e.g., 12/4 1am - 12/11 12:59am)
|
||||
case 'last7days':
|
||||
case 'previous7days': {
|
||||
const dayStart = this.getDayStart(now);
|
||||
const currentStart = dayStart.minus({ days: 6 }); // Start of current period
|
||||
const prevStart = currentStart.minus({ days: 7 }); // Start 7 days before current start
|
||||
const currentStart = dayStart.minus({ days: 6 });
|
||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||
const prevStart = prevEnd.minus({ days: 6 });
|
||||
return {
|
||||
start: prevStart,
|
||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'last30days': {
|
||||
// If current period is days 0-29, previous should be days 30-59
|
||||
case 'last30days':
|
||||
case 'previous30days': {
|
||||
const dayStart = this.getDayStart(now);
|
||||
const currentStart = dayStart.minus({ days: 29 }); // Start of current period
|
||||
const prevStart = currentStart.minus({ days: 30 }); // Start 30 days before current start
|
||||
const currentStart = dayStart.minus({ days: 29 });
|
||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||
const prevStart = prevEnd.minus({ days: 29 });
|
||||
return {
|
||||
start: prevStart,
|
||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'last90days': {
|
||||
// If current period is days 0-89, previous should be days 90-179
|
||||
case 'last90days':
|
||||
case 'previous90days': {
|
||||
const dayStart = this.getDayStart(now);
|
||||
const currentStart = dayStart.minus({ days: 89 }); // Start of current period
|
||||
const prevStart = currentStart.minus({ days: 90 }); // Start 90 days before current start
|
||||
const currentStart = dayStart.minus({ days: 89 });
|
||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||
const prevStart = prevEnd.minus({ days: 89 });
|
||||
return {
|
||||
start: prevStart,
|
||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'thisWeek': {
|
||||
const lastWeek = now.minus({ weeks: 1 });
|
||||
const weekStart = this.getWeekStart(lastWeek);
|
||||
const weekEnd = weekStart.plus({ days: 6 });
|
||||
const weekStart = this.getWeekStart(now);
|
||||
const prevEnd = weekStart.minus({ milliseconds: 1 });
|
||||
const prevStart = this.getWeekStart(prevEnd);
|
||||
return {
|
||||
start: weekStart,
|
||||
end: this.getDayEnd(weekEnd)
|
||||
start: prevStart,
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'lastWeek': {
|
||||
const twoWeeksAgo = now.minus({ weeks: 2 });
|
||||
const weekStart = this.getWeekStart(twoWeeksAgo);
|
||||
const weekEnd = weekStart.plus({ days: 6 });
|
||||
const lastWeekStart = this.getWeekStart(now.minus({ weeks: 1 }));
|
||||
const prevEnd = lastWeekStart.minus({ milliseconds: 1 });
|
||||
const prevStart = this.getWeekStart(prevEnd);
|
||||
return {
|
||||
start: weekStart,
|
||||
end: this.getDayEnd(weekEnd)
|
||||
start: prevStart,
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'thisMonth': {
|
||||
const lastMonth = now.minus({ months: 1 });
|
||||
const monthStart = lastMonth.startOf('month').set({ hour: this.dayStartHour });
|
||||
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
||||
const monthStart = now.startOf('month').set({ hour: this.dayStartHour });
|
||||
const prevEnd = monthStart.minus({ milliseconds: 1 });
|
||||
const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour });
|
||||
return {
|
||||
start: monthStart,
|
||||
end: this.getDayEnd(monthEnd)
|
||||
start: prevStart,
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'lastMonth': {
|
||||
const twoMonthsAgo = now.minus({ months: 2 });
|
||||
const monthStart = twoMonthsAgo.startOf('month').set({ hour: this.dayStartHour });
|
||||
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
||||
const lastMonthStart = now.minus({ months: 1 }).startOf('month').set({ hour: this.dayStartHour });
|
||||
const prevEnd = lastMonthStart.minus({ milliseconds: 1 });
|
||||
const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour });
|
||||
return {
|
||||
start: monthStart,
|
||||
end: this.getDayEnd(monthEnd)
|
||||
start: prevStart,
|
||||
end: prevEnd
|
||||
};
|
||||
}
|
||||
case 'twoDaysAgo': {
|
||||
const twoDaysAgo = now.minus({ days: 2 });
|
||||
return {
|
||||
start: this.getDayStart(twoDaysAgo),
|
||||
end: this.getDayEnd(twoDaysAgo)
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -91,10 +91,10 @@ const formatPercent = (value, total) => {
|
||||
const getPreviousPeriod = (timeRange) => {
|
||||
switch (timeRange) {
|
||||
case 'today': return 'yesterday';
|
||||
case 'yesterday': return 'last2days';
|
||||
case 'last7days': return 'previous7days';
|
||||
case 'last30days': return 'previous30days';
|
||||
case 'last90days': return 'previous90days';
|
||||
case 'yesterday': return 'twoDaysAgo';
|
||||
case 'last7days': return 'last7days';
|
||||
case 'last30days': return 'last30days';
|
||||
case 'last90days': return 'last90days';
|
||||
default: return timeRange;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,9 +60,9 @@ const PREVIOUS_PERIOD_MAP = {
|
||||
today: 'yesterday',
|
||||
thisWeek: 'lastWeek',
|
||||
thisMonth: 'lastMonth',
|
||||
last7days: 'previous7days',
|
||||
last30days: 'previous30days',
|
||||
last90days: 'previous90days',
|
||||
last7days: 'last7days',
|
||||
last30days: 'last30days',
|
||||
last90days: 'last90days',
|
||||
yesterday: 'twoDaysAgo'
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user