Fix time ranges
This commit is contained in:
@@ -1417,503 +1417,45 @@ export class EventsService {
|
|||||||
const { metric, daily = false } = params;
|
const { metric, daily = false } = params;
|
||||||
|
|
||||||
// Get period dates
|
// Get period dates
|
||||||
let periodStart, periodEnd;
|
let periodStart, periodEnd, prevPeriodStart, prevPeriodEnd;
|
||||||
if (params.startDate && params.endDate) {
|
if (params.startDate && params.endDate) {
|
||||||
periodStart = this.timeManager.toDateTime(params.startDate);
|
periodStart = this.timeManager.toDateTime(params.startDate);
|
||||||
periodEnd = this.timeManager.toDateTime(params.endDate);
|
periodEnd = this.timeManager.toDateTime(params.endDate);
|
||||||
|
const duration = periodEnd.diff(periodStart);
|
||||||
|
prevPeriodEnd = periodStart.minus({ milliseconds: 1 });
|
||||||
|
prevPeriodStart = prevPeriodEnd.minus(duration);
|
||||||
} else if (params.timeRange) {
|
} 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;
|
periodStart = range.start;
|
||||||
periodEnd = range.end;
|
periodEnd = range.end;
|
||||||
|
prevPeriodStart = prevRange.start;
|
||||||
|
prevPeriodEnd = prevRange.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load data based on metric type
|
// Load both current and previous period data
|
||||||
let mainData, orderData;
|
const [currentResponse, prevResponse] = await Promise.all([
|
||||||
switch (metric) {
|
this.getEvents({
|
||||||
case 'revenue':
|
...params,
|
||||||
case 'orders':
|
startDate: periodStart.toISO(),
|
||||||
case 'average_order':
|
endDate: periodEnd.toISO(),
|
||||||
mainData = await this.getEvents({
|
metricId: METRIC_IDS.PLACED_ORDER
|
||||||
...params,
|
}),
|
||||||
metricId: METRIC_IDS.PLACED_ORDER
|
this.getEvents({
|
||||||
});
|
metricId: METRIC_IDS.PLACED_ORDER,
|
||||||
break;
|
startDate: prevPeriodStart.toISO(),
|
||||||
case 'refunds':
|
endDate: prevPeriodEnd.toISO()
|
||||||
// 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform events
|
// 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
|
// Initialize daily stats map with all dates in range
|
||||||
const dailyStats = new Map();
|
const dailyStats = new Map();
|
||||||
@@ -1928,19 +1470,15 @@ export class EventsService {
|
|||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
averageOrderValue: 0,
|
averageOrderValue: 0,
|
||||||
averageItemsPerOrder: 0,
|
averageItemsPerOrder: 0,
|
||||||
refunds: { total: 0, count: 0, reasons: {} },
|
prevRevenue: 0,
|
||||||
shipping: { count: 0, locations: [], methods: {} },
|
prevOrders: 0,
|
||||||
orderTypes: {
|
prevAvgOrderValue: 0
|
||||||
preOrders: { count: 0, value: 0, percentage: 0 },
|
|
||||||
localPickup: { count: 0, value: 0, percentage: 0 },
|
|
||||||
heldItems: { count: 0, value: 0, percentage: 0 }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
currentDate = currentDate.plus({ days: 1 });
|
currentDate = currentDate.plus({ days: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process events into daily stats
|
// Process current period events
|
||||||
for (const event of transformedData) {
|
for (const event of currentEvents) {
|
||||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||||
if (!datetime) continue;
|
if (!datetime) continue;
|
||||||
|
|
||||||
@@ -1949,106 +1487,42 @@ export class EventsService {
|
|||||||
|
|
||||||
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 items = props.Items || [];
|
||||||
|
|
||||||
switch (metric) {
|
dayStats.revenue += totalAmount;
|
||||||
case 'revenue':
|
dayStats.orders++;
|
||||||
case 'orders':
|
dayStats.itemCount += items.length;
|
||||||
case 'average_order':
|
dayStats.averageOrderValue = dayStats.revenue / dayStats.orders;
|
||||||
case 'order_range':
|
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate period totals separately after processing daily stats
|
// Process previous period events and map them to corresponding current period dates
|
||||||
const totalOrders = transformedData.length;
|
const daysDiff = periodEnd.diff(periodStart, 'days').days;
|
||||||
const totalRevenue = transformedData.reduce((sum, event) => {
|
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 || {};
|
const props = event.event_properties || {};
|
||||||
return sum + Number(props.TotalAmount || 0);
|
const totalAmount = Number(props.TotalAmount || 0);
|
||||||
}, 0);
|
|
||||||
const totalItems = transformedData.reduce((sum, event) => {
|
dayStats.prevRevenue += totalAmount;
|
||||||
const props = event.event_properties || {};
|
dayStats.prevOrders++;
|
||||||
return sum + (props.Items || []).length;
|
dayStats.prevAvgOrderValue = dayStats.prevRevenue / dayStats.prevOrders;
|
||||||
}, 0);
|
}
|
||||||
|
|
||||||
// Convert to array and sort by date
|
// Convert to array and sort by date
|
||||||
const stats = Array.from(dailyStats.values())
|
const stats = Array.from(dailyStats.values())
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.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;
|
return stats;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[EventsService] Error calculating detailed stats:', error);
|
console.error('[EventsService] Error calculating detailed stats:', error);
|
||||||
|
|||||||
@@ -310,71 +310,80 @@ export class TimeManager {
|
|||||||
end: this.getDayEnd(twoDaysAgo)
|
end: this.getDayEnd(twoDaysAgo)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last7days': {
|
case 'last7days':
|
||||||
// If current period is days 0-6 (e.g., 12/11 1am - 12/17 12:59am)
|
case 'previous7days': {
|
||||||
// Previous should be days 7-13 (e.g., 12/4 1am - 12/11 12:59am)
|
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 6 }); // Start of current period
|
const currentStart = dayStart.minus({ days: 6 });
|
||||||
const prevStart = currentStart.minus({ days: 7 }); // Start 7 days before current start
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
|
const prevStart = prevEnd.minus({ days: 6 });
|
||||||
return {
|
return {
|
||||||
start: prevStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last30days': {
|
case 'last30days':
|
||||||
// If current period is days 0-29, previous should be days 30-59
|
case 'previous30days': {
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 29 }); // Start of current period
|
const currentStart = dayStart.minus({ days: 29 });
|
||||||
const prevStart = currentStart.minus({ days: 30 }); // Start 30 days before current start
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
|
const prevStart = prevEnd.minus({ days: 29 });
|
||||||
return {
|
return {
|
||||||
start: prevStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last90days': {
|
case 'last90days':
|
||||||
// If current period is days 0-89, previous should be days 90-179
|
case 'previous90days': {
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 89 }); // Start of current period
|
const currentStart = dayStart.minus({ days: 89 });
|
||||||
const prevStart = currentStart.minus({ days: 90 }); // Start 90 days before current start
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
|
const prevStart = prevEnd.minus({ days: 89 });
|
||||||
return {
|
return {
|
||||||
start: prevStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(currentStart.minus({ days: 1 })) // End right before current start
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'thisWeek': {
|
case 'thisWeek': {
|
||||||
const lastWeek = now.minus({ weeks: 1 });
|
const weekStart = this.getWeekStart(now);
|
||||||
const weekStart = this.getWeekStart(lastWeek);
|
const prevEnd = weekStart.minus({ milliseconds: 1 });
|
||||||
const weekEnd = weekStart.plus({ days: 6 });
|
const prevStart = this.getWeekStart(prevEnd);
|
||||||
return {
|
return {
|
||||||
start: weekStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(weekEnd)
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'lastWeek': {
|
case 'lastWeek': {
|
||||||
const twoWeeksAgo = now.minus({ weeks: 2 });
|
const lastWeekStart = this.getWeekStart(now.minus({ weeks: 1 }));
|
||||||
const weekStart = this.getWeekStart(twoWeeksAgo);
|
const prevEnd = lastWeekStart.minus({ milliseconds: 1 });
|
||||||
const weekEnd = weekStart.plus({ days: 6 });
|
const prevStart = this.getWeekStart(prevEnd);
|
||||||
return {
|
return {
|
||||||
start: weekStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(weekEnd)
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'thisMonth': {
|
case 'thisMonth': {
|
||||||
const lastMonth = now.minus({ months: 1 });
|
const monthStart = now.startOf('month').set({ hour: this.dayStartHour });
|
||||||
const monthStart = lastMonth.startOf('month').set({ hour: this.dayStartHour });
|
const prevEnd = monthStart.minus({ milliseconds: 1 });
|
||||||
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour });
|
||||||
return {
|
return {
|
||||||
start: monthStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(monthEnd)
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'lastMonth': {
|
case 'lastMonth': {
|
||||||
const twoMonthsAgo = now.minus({ months: 2 });
|
const lastMonthStart = now.minus({ months: 1 }).startOf('month').set({ hour: this.dayStartHour });
|
||||||
const monthStart = twoMonthsAgo.startOf('month').set({ hour: this.dayStartHour });
|
const prevEnd = lastMonthStart.minus({ milliseconds: 1 });
|
||||||
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour });
|
||||||
return {
|
return {
|
||||||
start: monthStart,
|
start: prevStart,
|
||||||
end: this.getDayEnd(monthEnd)
|
end: prevEnd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'twoDaysAgo': {
|
||||||
|
const twoDaysAgo = now.minus({ days: 2 });
|
||||||
|
return {
|
||||||
|
start: this.getDayStart(twoDaysAgo),
|
||||||
|
end: this.getDayEnd(twoDaysAgo)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ const formatPercent = (value, total) => {
|
|||||||
const getPreviousPeriod = (timeRange) => {
|
const getPreviousPeriod = (timeRange) => {
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case 'today': return 'yesterday';
|
case 'today': return 'yesterday';
|
||||||
case 'yesterday': return 'last2days';
|
case 'yesterday': return 'twoDaysAgo';
|
||||||
case 'last7days': return 'previous7days';
|
case 'last7days': return 'last7days';
|
||||||
case 'last30days': return 'previous30days';
|
case 'last30days': return 'last30days';
|
||||||
case 'last90days': return 'previous90days';
|
case 'last90days': return 'last90days';
|
||||||
default: return timeRange;
|
default: return timeRange;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ const PREVIOUS_PERIOD_MAP = {
|
|||||||
today: 'yesterday',
|
today: 'yesterday',
|
||||||
thisWeek: 'lastWeek',
|
thisWeek: 'lastWeek',
|
||||||
thisMonth: 'lastMonth',
|
thisMonth: 'lastMonth',
|
||||||
last7days: 'previous7days',
|
last7days: 'last7days',
|
||||||
last30days: 'previous30days',
|
last30days: 'last30days',
|
||||||
last90days: 'previous90days',
|
last90days: 'last90days',
|
||||||
yesterday: 'twoDaysAgo'
|
yesterday: 'twoDaysAgo'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user