diff --git a/dashboard-server/klaviyo-server/routes/events.routes.js b/dashboard-server/klaviyo-server/routes/events.routes.js
index f710719..9e25f06 100644
--- a/dashboard-server/klaviyo-server/routes/events.routes.js
+++ b/dashboard-server/klaviyo-server/routes/events.routes.js
@@ -149,6 +149,57 @@ export function createEventsRouter(apiKey, apiRevision) {
}
});
+ // Add new route for smart revenue projection
+ router.get('/projection', async (req, res) => {
+ try {
+ const { timeRange, startDate, endDate } = req.query;
+ console.log('[Events Route] Projection request:', {
+ timeRange,
+ startDate,
+ endDate
+ });
+
+ let range;
+ if (startDate && endDate) {
+ range = timeManager.getCustomRange(startDate, endDate);
+ } else if (timeRange) {
+ range = timeManager.getDateRange(timeRange);
+ } else {
+ return res.status(400).json({ error: 'Must provide either timeRange or startDate and endDate' });
+ }
+
+ if (!range) {
+ return res.status(400).json({ error: 'Invalid time range' });
+ }
+
+ const params = {
+ timeRange,
+ startDate: range.start.toISO(),
+ endDate: range.end.toISO()
+ };
+
+ // Try to get from cache first with a short TTL
+ const cacheKey = redisService._getCacheKey('projection', params);
+ const cachedData = await redisService.get(cacheKey);
+
+ if (cachedData) {
+ console.log('[Events Route] Cache hit for projection');
+ return res.json(cachedData);
+ }
+
+ console.log('[Events Route] Calculating smart projection with params:', params);
+ const projection = await eventsService.calculateSmartProjection(params);
+
+ // Cache the results with a short TTL (5 minutes)
+ await redisService.set(cacheKey, projection, 300);
+
+ res.json(projection);
+ } catch (error) {
+ console.error("[Events Route] Error calculating projection:", error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
// Add new route for detailed stats
router.get('/stats/details', async (req, res) => {
try {
diff --git a/dashboard-server/klaviyo-server/services/events.service.js b/dashboard-server/klaviyo-server/services/events.service.js
index 551f805..19142d5 100644
--- a/dashboard-server/klaviyo-server/services/events.service.js
+++ b/dashboard-server/klaviyo-server/services/events.service.js
@@ -2022,4 +2022,160 @@ export class EventsService {
throw error;
}
}
+
+ async calculateSmartProjection(params = {}) {
+ try {
+ const { timeRange, startDate, endDate } = params;
+
+ // Get current period dates
+ let periodStart, periodEnd;
+ if (startDate && endDate) {
+ periodStart = this.timeManager.getDayStart(this.timeManager.toDateTime(startDate));
+ periodEnd = this.timeManager.getDayEnd(this.timeManager.toDateTime(endDate));
+ } else if (timeRange) {
+ const range = this.timeManager.getDateRange(timeRange);
+ periodStart = range.start;
+ periodEnd = range.end;
+ }
+
+ // Get the same day of week from the last 4 weeks for pattern matching
+ const historicalPeriods = [];
+ let historicalStart = periodStart.minus({ weeks: 4 });
+
+ for (let i = 0; i < 4; i++) {
+ historicalPeriods.push({
+ start: historicalStart.plus({ weeks: i }),
+ end: historicalStart.plus({ weeks: i + 1 }).minus({ milliseconds: 1 })
+ });
+ }
+
+ // Fetch current period data
+ const currentEvents = await this.getEvents({
+ startDate: periodStart.toISO(),
+ endDate: periodEnd.toISO(),
+ metricId: METRIC_IDS.PLACED_ORDER
+ });
+
+ // Fetch historical data for pattern matching
+ const historicalPromises = historicalPeriods.map(period =>
+ this.getEvents({
+ startDate: period.start.toISO(),
+ endDate: period.end.toISO(),
+ metricId: METRIC_IDS.PLACED_ORDER
+ })
+ );
+
+ const historicalResults = await Promise.all(historicalPromises);
+
+ // Process current period data
+ const currentData = this._transformEvents(currentEvents.data || []);
+ const currentRevenue = currentData.reduce((sum, event) => {
+ const props = event.event_properties || {};
+ return sum + Number(props.TotalAmount || 0);
+ }, 0);
+
+ // Build hourly patterns from historical data
+ const hourlyPatterns = Array(24).fill(0).map(() => ({
+ count: 0,
+ revenue: 0,
+ percentage: 0
+ }));
+
+ let totalHistoricalRevenue = 0;
+ let totalHistoricalOrders = 0;
+
+ historicalResults.forEach(result => {
+ const events = this._transformEvents(result.data || []);
+ events.forEach(event => {
+ const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
+ if (!datetime) return;
+
+ const hour = datetime.hour;
+ const props = event.event_properties || {};
+ const amount = Number(props.TotalAmount || 0);
+
+ hourlyPatterns[hour].count++;
+ hourlyPatterns[hour].revenue += amount;
+ totalHistoricalRevenue += amount;
+ totalHistoricalOrders++;
+ });
+ });
+
+ // Calculate percentages
+ hourlyPatterns.forEach(pattern => {
+ pattern.percentage = totalHistoricalRevenue > 0 ?
+ (pattern.revenue / totalHistoricalRevenue) * 100 : 0;
+ });
+
+ // Get current hour in the period's timezone
+ const now = this.timeManager.getNow();
+ const currentHour = now.hour;
+ const currentMinute = now.minute;
+
+ // Calculate how much of the current hour has passed (0-1)
+ const hourProgress = currentMinute / 60;
+
+ // Calculate how much of the expected daily revenue we've seen so far
+ let expectedPercentageSeen = 0;
+ for (let i = 0; i < currentHour; i++) {
+ expectedPercentageSeen += hourlyPatterns[i].percentage;
+ }
+ // Add partial current hour
+ expectedPercentageSeen += hourlyPatterns[currentHour].percentage * hourProgress;
+
+ // Calculate projection based on patterns
+ let projectedRevenue = 0;
+ if (expectedPercentageSeen > 0) {
+ projectedRevenue = (currentRevenue / (expectedPercentageSeen / 100));
+ }
+
+ // Calculate confidence score (0-1) based on:
+ // 1. How much historical data we have
+ // 2. How consistent the patterns are
+ // 3. How far through the period we are
+ const patternConsistency = this._calculatePatternConsistency(hourlyPatterns);
+ const periodProgress = Math.min(100, Math.max(0, (now.diff(periodStart).milliseconds / periodEnd.diff(periodStart).milliseconds) * 100));
+ const historicalDataAmount = Math.min(totalHistoricalOrders / 1000, 1); // Normalize to 0-1, considering 1000+ orders as maximum confidence
+
+ const confidence = (
+ (patternConsistency * 0.4) +
+ (periodProgress / 100 * 0.4) +
+ (historicalDataAmount * 0.2)
+ );
+
+ // Return both the simple and pattern-based projections with metadata
+ return {
+ currentRevenue,
+ projectedRevenue,
+ confidence,
+ metadata: {
+ periodProgress,
+ patternConsistency,
+ historicalOrders: totalHistoricalOrders,
+ hourlyPatterns,
+ expectedPercentageSeen,
+ currentHour,
+ currentMinute
+ }
+ };
+ } catch (error) {
+ console.error('[EventsService] Error calculating smart projection:', error);
+ throw error;
+ }
+ }
+
+ _calculatePatternConsistency(hourlyPatterns) {
+ // Calculate the standard deviation of the percentage distribution
+ const mean = hourlyPatterns.reduce((sum, pattern) => sum + pattern.percentage, 0) / 24;
+ const variance = hourlyPatterns.reduce((sum, pattern) => {
+ const diff = pattern.percentage - mean;
+ return sum + (diff * diff);
+ }, 0) / 24;
+ const stdDev = Math.sqrt(variance);
+
+ // Normalize to a 0-1 scale where lower standard deviation means higher consistency
+ // Using a sigmoid function to normalize
+ const normalizedConsistency = 1 / (1 + Math.exp(stdDev / 10));
+ return normalizedConsistency;
+ }
}
diff --git a/dashboard/src/components/dashboard/StatCards.jsx b/dashboard/src/components/dashboard/StatCards.jsx
index 52b3f2e..679cf04 100644
--- a/dashboard/src/components/dashboard/StatCards.jsx
+++ b/dashboard/src/components/dashboard/StatCards.jsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect, useCallback, Suspense, memo } from 'react';
-import axios from 'axios';
+import React, { useState, useEffect, useCallback, Suspense, memo } from "react";
+import axios from "axios";
import {
Card,
CardContent,
@@ -28,14 +28,15 @@ import {
XAxis,
YAxis,
CartesianGrid,
- Tooltip,
ResponsiveContainer,
Legend,
PieChart,
Pie,
- Cell
+ Cell,
} from "recharts";
-import {
+// Import Tooltip from recharts with alias to avoid naming conflict
+import { Tooltip as RechartsTooltip } from "recharts";
+import {
DollarSign,
ShoppingCart,
Package,
@@ -54,7 +55,7 @@ import {
CircleDollarSign,
MapPin,
Info,
- Loader2
+ Loader2,
} from "lucide-react";
import { DateTime } from "luxon";
import { TIME_RANGES } from "@/lib/constants";
@@ -70,6 +71,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
+import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
const formatCurrency = (value, minimumFractionDigits = 0) => {
if (!value || isNaN(value)) return "$0";
@@ -82,14 +84,14 @@ const formatCurrency = (value, minimumFractionDigits = 0) => {
};
const formatPercentage = (value) => {
- if (typeof value !== 'number') return '0%';
+ if (typeof value !== "number") return "0%";
return `${Math.round(value)}%`;
};
const formatHour = (hour) => {
const date = new Date();
date.setHours(hour, 0, 0);
- return date.toLocaleString('en-US', { hour: 'numeric', hour12: true });
+ return date.toLocaleString("en-US", { hour: "numeric", hour12: true });
};
// Reusable chart components
@@ -100,7 +102,7 @@ const TimeSeriesChart = ({
color = "hsl(var(--primary))",
type = "line",
valueFormatter = (value) => value,
- height = 400
+ height = 400,
}) => {
const ChartComponent = type === "line" ? LineChart : BarChart;
const DataComponent = type === "line" ? Line : Bar;
@@ -113,23 +115,25 @@ const TimeSeriesChart = ({
return (
-
+
DateTime.fromISO(value).toFormat('LLL d')}
+ tickFormatter={(value) => DateTime.fromISO(value).toFormat("LLL d")}
className="text-xs"
/>
-
-
+ {
if (!active || !payload?.length) return null;
return (
-
{DateTime.fromISO(label).toFormat('LLL d')}
+
+ {DateTime.fromISO(label).toFormat("LLL d")}
+
{payload.map((entry, i) => (
{entry.name}: {valueFormatter(entry.value)}
@@ -158,12 +162,7 @@ const TimeSeriesChart = ({
);
};
-const DetailDialog = ({
- open,
- onOpenChange,
- title,
- children
-}) => (
+const DetailDialog = ({ open, onOpenChange, title, children }) => (
@@ -176,15 +175,24 @@ const DetailDialog = ({
// Detail view components
const RevenueDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
-
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
+
// Ensure we have daily data points and they're properly formatted
- const chartData = data.map(day => ({
- timestamp: DateTime.fromISO(day.timestamp).toFormat('yyyy-MM-dd'),
- revenue: parseFloat(day.revenue || 0),
- orders: parseInt(day.orders || 0),
- date: DateTime.fromISO(day.timestamp).toFormat('LLL d')
- })).sort((a, b) => DateTime.fromISO(a.timestamp) - DateTime.fromISO(b.timestamp));
+ const chartData = data
+ .map((day) => ({
+ timestamp: DateTime.fromISO(day.timestamp).toFormat("yyyy-MM-dd"),
+ revenue: parseFloat(day.revenue || 0),
+ orders: parseInt(day.orders || 0),
+ date: DateTime.fromISO(day.timestamp).toFormat("LLL d"),
+ }))
+ .sort(
+ (a, b) => DateTime.fromISO(a.timestamp) - DateTime.fromISO(b.timestamp)
+ );
return (
{
};
const OrdersDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
return (
<>
@@ -215,7 +228,7 @@ const OrdersDetails = ({ data }) => {
({
hour: formatHour(hour),
- orders: count
+ orders: count,
}))}
dataKey="orders"
name="Orders"
@@ -238,7 +251,7 @@ const BarList = ({ data, valueFormatter = (v) => v }) => (
{valueFormatter(item.value)}
-
@@ -255,7 +268,9 @@ const StatGrid = ({ stats }) => (
{stat.label}
{stat.value}
{stat.description && (
-
{stat.description}
+
+ {stat.description}
+
)}
))}
@@ -264,7 +279,12 @@ const StatGrid = ({ stats }) => (
// Add detail view components
const AverageOrderDetails = ({ data, orderCount }) => {
- if (!data?.length) return
No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
return (
{
};
const CancellationsDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
- const cancelData = data[0]?.canceledOrders || { total: 0, count: 0, reasons: {}, items: [] };
- const timeSeriesData = data.map(day => ({
+ const cancelData = data[0]?.canceledOrders || {
+ total: 0,
+ count: 0,
+ reasons: {},
+ items: [],
+ };
+ const timeSeriesData = data.map((day) => ({
timestamp: day.timestamp,
total: day.canceledOrders?.total || 0,
- count: day.canceledOrders?.count || 0
+ count: day.canceledOrders?.count || 0,
}));
const reasonData = Object.entries(cancelData.reasons || {})
.map(([reason, count]) => ({
reason,
- count
+ count,
}))
.sort((a, b) => b.count - a.count);
@@ -333,7 +363,9 @@ const CancellationsDetails = ({ data }) => {
{reasonData.map((item, index) => (
{item.reason}
- {item.count.toLocaleString()}
+
+ {item.count.toLocaleString()}
+
))}
@@ -346,7 +378,12 @@ const CancellationsDetails = ({ data }) => {
};
const BrandsCategoriesDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
const stats = data[0];
const brandsList = stats?.brands?.list || [];
@@ -372,8 +409,12 @@ const BrandsCategoriesDetails = ({ data }) => {
{brandsList.map((brand) => (
{brand.name}
- {brand.count?.toLocaleString()}
- ${brand.revenue?.toFixed(2)}
+
+ {brand.count?.toLocaleString()}
+
+
+ ${brand.revenue?.toFixed(2)}
+
))}
@@ -399,8 +440,12 @@ const BrandsCategoriesDetails = ({ data }) => {
{categoriesList.map((category) => (
{category.name}
- {category.count?.toLocaleString()}
- ${category.revenue?.toFixed(2)}
+
+ {category.count?.toLocaleString()}
+
+
+ ${category.revenue?.toFixed(2)}
+
))}
@@ -412,7 +457,12 @@ const BrandsCategoriesDetails = ({ data }) => {
};
const ShippingDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
const shippedCount = data[0]?.shipping?.shippedCount || 0;
const locations = data[0]?.shipping?.locations || {};
@@ -420,14 +470,14 @@ const ShippingDetails = ({ data }) => {
// Shipping method name mappings
const shippingMethodNames = {
- 'usps_ground_advantage': 'USPS Ground Advantage',
- 'usps_priority': 'USPS Priority',
- 'Unknown': 'Digital',
- 'fedex_ieconomy': 'FedEx Intl Economy',
- 'fedex_homedelivery': 'FedEx Ground',
- 'fedex_ground': 'FedEx Ground',
- 'fedex_iground': 'FedEx Intl Ground',
- 'fedex_2day': 'FedEx 2 Day'
+ usps_ground_advantage: "USPS Ground Advantage",
+ usps_priority: "USPS Priority",
+ Unknown: "Digital",
+ fedex_ieconomy: "FedEx Intl Economy",
+ fedex_homedelivery: "FedEx Ground",
+ fedex_ground: "FedEx Ground",
+ fedex_iground: "FedEx Intl Ground",
+ fedex_2day: "FedEx 2 Day",
};
return (
@@ -450,8 +500,12 @@ const ShippingDetails = ({ data }) => {
{methodStats.map((method) => (
- {shippingMethodNames[method.name] || method.name}
- {method.value.toLocaleString()}
+
+ {shippingMethodNames[method.name] || method.name}
+
+
+ {method.value.toLocaleString()}
+
{((method.value / shippedCount) * 100).toFixed(1)}%
@@ -480,9 +534,15 @@ const ShippingDetails = ({ data }) => {
{locations.byCountry?.map((country) => (
- {country.country}
- {country.count.toLocaleString()}
- {country.percentage.toFixed(1)}%
+
+ {country.country}
+
+
+ {country.count.toLocaleString()}
+
+
+ {country.percentage.toFixed(1)}%
+
))}
@@ -509,8 +569,12 @@ const ShippingDetails = ({ data }) => {
{locations.byState?.map((state) => (
{state.state}
- {state.count.toLocaleString()}
- {state.percentage.toFixed(1)}%
+
+ {state.count.toLocaleString()}
+
+
+ {state.percentage.toFixed(1)}%
+
))}
@@ -522,19 +586,24 @@ const ShippingDetails = ({ data }) => {
};
const OrderTypeDetails = ({ data, type }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
- const timeSeriesData = data.map(day => ({
+ const timeSeriesData = data.map((day) => ({
timestamp: day.timestamp,
count: day.count,
value: day.value,
- percentage: day.percentage
+ percentage: day.percentage,
}));
const typeColors = {
- 'pre_orders': 'hsl(47.9 95.8% 53.1%)', // Yellow for pre-orders
- 'local_pickup': 'hsl(192.2 70.1% 51.4%)', // Cyan for local pickup
- 'on_hold': 'hsl(346.8 77.2% 49.8%)' // Red for on hold
+ pre_orders: "hsl(47.9 95.8% 53.1%)", // Yellow for pre-orders
+ local_pickup: "hsl(192.2 70.1% 51.4%)", // Cyan for local pickup
+ on_hold: "hsl(346.8 77.2% 49.8%)", // Red for on hold
};
const color = typeColors[type];
@@ -578,34 +647,49 @@ const OrderTypeDetails = ({ data, type }) => {
};
const PeakHourDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
- const hourlyData = data[0]?.hourlyOrders?.map((count, hour) => ({
- timestamp: hour, // Use raw hour number for x-axis
- orders: count
- })) || [];
+ const hourlyData =
+ data[0]?.hourlyOrders?.map((count, hour) => ({
+ timestamp: hour, // Use raw hour number for x-axis
+ orders: count,
+ })) || [];
return (
-
+
{
const date = new Date();
date.setHours(hour, 0, 0);
- return date.toLocaleString('en-US', { hour: 'numeric', hour12: true });
+ return date.toLocaleString("en-US", {
+ hour: "numeric",
+ hour12: true,
+ });
}}
className="text-xs"
/>
- {
if (!active || !payload?.length) return null;
const date = new Date();
date.setHours(label, 0, 0);
- const time = date.toLocaleString('en-US', { hour: 'numeric', hour12: true });
+ const time = date.toLocaleString("en-US", {
+ hour: "numeric",
+ hour12: true,
+ });
return (
{time}
@@ -614,11 +698,7 @@ const PeakHourDetails = ({ data }) => {
);
}}
/>
-
+
@@ -626,19 +706,24 @@ const PeakHourDetails = ({ data }) => {
};
const RefundDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ if (!data?.length)
+ return (
+
+ No data available for the selected time range.
+
+ );
const refundData = data[0]?.refunds || { total: 0, count: 0, reasons: {} };
- const timeSeriesData = data.map(day => ({
+ const timeSeriesData = data.map((day) => ({
timestamp: day.timestamp,
total: day.refunds?.total || 0,
- count: day.refunds?.count || 0
+ count: day.refunds?.count || 0,
}));
const reasonData = Object.entries(refundData.reasons || {})
.map(([reason, count]) => ({
reason,
- count
+ count,
}))
.sort((a, b) => b.count - a.count);
@@ -681,7 +766,9 @@ const RefundDetails = ({ data }) => {
{reasonData.map((item, index) => (
{item.reason}
- {item.count.toLocaleString()}
+
+ {item.count.toLocaleString()}
+
))}
@@ -694,17 +781,23 @@ const RefundDetails = ({ data }) => {
};
const OrderRangeDetails = ({ data }) => {
- if (!data?.length) return No data available for the selected time range.
;
+ 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
- })) || [];
+ acc.orderValueDistribution =
+ day.orderValueDistribution?.map((range) => ({
+ ...range,
+ count: 0,
+ total: 0,
+ })) || [];
}
// Aggregate distribution data
@@ -721,24 +814,35 @@ const OrderRangeDetails = ({ data }) => {
return acc;
}, {});
- const timeSeriesData = data.map(day => ({
+ const timeSeriesData = data.map((day) => ({
timestamp: day.timestamp,
largest: day.orderValueRange?.largest || 0,
smallest: day.orderValueRange?.smallest || 0,
- 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)
- };
- }) || [];
+ 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 (
@@ -781,11 +885,21 @@ const OrderRangeDetails = ({ data }) => {
{formattedDistributionData.map((range, index) => (
- {range.range}
- {range.count.toLocaleString()}
- {formatCurrency(range.total)}
- {range.percentage}%
- {range.revenuePercentage}%
+
+ {range.range}
+
+
+ {range.count.toLocaleString()}
+
+
+ {formatCurrency(range.total)}
+
+
+ {range.percentage}%
+
+
+ {range.revenuePercentage}%
+
))}
@@ -793,7 +907,10 @@ const OrderRangeDetails = ({ data }) => {
-
+
{
className="text-xs"
tickFormatter={(value) => `${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}%
+
+ Orders: {data.count.toLocaleString()}
+
+
+ Revenue: {formatCurrency(data.total)}
+
+
+ % of Orders: {data.percentage}%
+
+
+ % of Revenue: {data.revenuePercentage}%
+
);
}}
@@ -835,11 +960,11 @@ const OrderRangeDetails = ({ data }) => {
);
};
-const StatCard = ({
- title,
- value,
- description,
- trend,
+const StatCard = ({
+ title,
+ value,
+ description,
+ trend,
trendValue,
valuePrefix = "",
valueSuffix = "",
@@ -853,10 +978,14 @@ const StatCard = ({
info,
onDetailsClick,
isLoading = false,
- progress
+ progress,
}) => (
-
@@ -883,21 +1012,34 @@ const StatCard = ({
- {valuePrefix}{value}{valueSuffix}
+ {valuePrefix}
+ {value}
+ {valueSuffix}
{description && (
{description}
{trend && (
-
- {trend === 'up' ? : }
- {trendPrefix}{trendValue}{trendSuffix}
+
+ {trend === "up" ? (
+
+ ) : (
+
+ )}
+ {trendPrefix}
+ {trendValue}
+ {trendSuffix}
)}
)}
-
)}
@@ -911,12 +1053,12 @@ const useDataCache = () => {
const getCacheKey = (timeRange, metric) => `${timeRange}_${metric}`;
const setCacheData = (timeRange, metric, data) => {
- setCache(prev => ({
+ setCache((prev) => ({
...prev,
[getCacheKey(timeRange, metric)]: {
data,
- timestamp: Date.now()
- }
+ timestamp: Date.now(),
+ },
}));
};
@@ -1018,12 +1160,12 @@ const SkeletonTable = ({ rows = 5 }) => (
);
-const StatCards = ({
- timeRange: initialTimeRange = 'today',
+const StatCards = ({
+ timeRange: initialTimeRange = "today",
startDate,
endDate,
title = "Sales Dashboard",
- description = ""
+ description = "",
}) => {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
@@ -1035,184 +1177,229 @@ const StatCards = ({
const [detailDataLoading, setDetailDataLoading] = useState({});
const [detailData, setDetailData] = useState({});
const [isInitialLoad, setIsInitialLoad] = useState(true);
+ const [projection, setProjection] = useState(null);
+ const [projectionLoading, setProjectionLoading] = useState(false);
const { setCacheData, getCacheData, clearCache } = useDataCache();
// Function to determine if we should use last30days for trend charts
- const shouldUseLast30Days = useCallback((metric) => {
- if (['brands_categories', 'shipping'].includes(metric)) {
- return false;
- }
- const shortPeriods = ['today', 'yesterday', 'last7days', 'thisWeek', 'lastWeek'];
- return shortPeriods.includes(timeRange);
- }, [timeRange]);
+ const shouldUseLast30Days = useCallback(
+ (metric) => {
+ if (["brands_categories", "shipping"].includes(metric)) {
+ return false;
+ }
+ const shortPeriods = [
+ "today",
+ "yesterday",
+ "last7days",
+ "thisWeek",
+ "lastWeek",
+ ];
+ return shortPeriods.includes(timeRange);
+ },
+ [timeRange]
+ );
// Function to fetch detail data for a specific metric
- const fetchDetailData = useCallback(async (metric, orderType) => {
- const detailTimeRange = shouldUseLast30Days(metric) ? 'last30days' : timeRange;
- const cachedData = getCacheData(detailTimeRange, metric);
-
- if (cachedData) {
- console.log(`Using cached data for ${metric}`);
- setDetailData(prev => ({ ...prev, [metric]: cachedData }));
- return cachedData;
- }
+ const fetchDetailData = useCallback(
+ async (metric, orderType) => {
+ const detailTimeRange = shouldUseLast30Days(metric)
+ ? "last30days"
+ : timeRange;
+ const cachedData = getCacheData(detailTimeRange, metric);
- console.log(`Fetching detail data for ${metric}`);
- setDetailDataLoading(prev => ({ ...prev, [metric]: true }));
-
- try {
- const params = {
- ...(timeRange === 'custom'
- ? { startDate, endDate }
- : { timeRange: detailTimeRange }),
- metric,
- daily: true
- };
-
- // For metrics that need the full stats
- if (['shipping', 'brands_categories'].includes(metric)) {
- const response = await axios.get('/api/klaviyo/events/stats', { params });
- const data = [response.data.stats];
- setCacheData(detailTimeRange, metric, data);
- setDetailData(prev => ({ ...prev, [metric]: data }));
- setError(null);
- return data;
+ if (cachedData) {
+ console.log(`Using cached data for ${metric}`);
+ setDetailData((prev) => ({ ...prev, [metric]: cachedData }));
+ return cachedData;
}
- // For order types (pre_orders, local_pickup, on_hold)
- if (['pre_orders', 'local_pickup', 'on_hold'].includes(metric)) {
- const response = await axios.get('/api/klaviyo/events/stats/details', {
- params: {
- ...params,
- orderType: orderType
- }
+ console.log(`Fetching detail data for ${metric}`);
+ setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
+
+ try {
+ const params = {
+ ...(timeRange === "custom"
+ ? { startDate, endDate }
+ : { timeRange: detailTimeRange }),
+ metric,
+ daily: true,
+ };
+
+ // For metrics that need the full stats
+ if (["shipping", "brands_categories"].includes(metric)) {
+ const response = await axios.get("/api/klaviyo/events/stats", {
+ params,
+ });
+ const data = [response.data.stats];
+ setCacheData(detailTimeRange, metric, data);
+ setDetailData((prev) => ({ ...prev, [metric]: data }));
+ setError(null);
+ return data;
+ }
+
+ // For order types (pre_orders, local_pickup, on_hold)
+ if (["pre_orders", "local_pickup", "on_hold"].includes(metric)) {
+ const response = await axios.get(
+ "/api/klaviyo/events/stats/details",
+ {
+ params: {
+ ...params,
+ orderType: orderType,
+ },
+ }
+ );
+ const data = response.data.stats;
+ setCacheData(detailTimeRange, metric, data);
+ setDetailData((prev) => ({ ...prev, [metric]: data }));
+ setError(null);
+ return data;
+ }
+
+ // For refunds and cancellations
+ if (["refunds", "cancellations"].includes(metric)) {
+ const response = await axios.get(
+ "/api/klaviyo/events/stats/details",
+ {
+ params: {
+ ...params,
+ eventType:
+ metric === "refunds" ? "PAYMENT_REFUNDED" : "CANCELED_ORDER",
+ },
+ }
+ );
+ const data = response.data.stats;
+
+ // Transform the data to match the expected format
+ const transformedData = data.map((day) => ({
+ ...day,
+ timestamp: day.timestamp,
+ refunds:
+ metric === "refunds"
+ ? {
+ total: day.refunds?.total || 0,
+ count: day.refunds?.count || 0,
+ reasons: day.refunds?.reasons || {},
+ }
+ : undefined,
+ canceledOrders:
+ metric === "cancellations"
+ ? {
+ total: day.canceledOrders?.total || 0,
+ count: day.canceledOrders?.count || 0,
+ reasons: day.canceledOrders?.reasons || {},
+ }
+ : undefined,
+ }));
+
+ setCacheData(detailTimeRange, metric, transformedData);
+ setDetailData((prev) => ({ ...prev, [metric]: transformedData }));
+ setError(null);
+ return transformedData;
+ }
+
+ // For order range
+ if (metric === "order_range") {
+ const response = await axios.get(
+ "/api/klaviyo/events/stats/details",
+ {
+ params: {
+ ...params,
+ eventType: "PLACED_ORDER",
+ },
+ }
+ );
+ const data = response.data.stats;
+ console.log("Fetched order range data:", data);
+ setCacheData(detailTimeRange, metric, data);
+ setDetailData((prev) => ({ ...prev, [metric]: data }));
+ setError(null);
+ return data;
+ }
+
+ // For all other metrics
+ const response = await axios.get("/api/klaviyo/events/stats/details", {
+ params,
});
const data = response.data.stats;
setCacheData(detailTimeRange, metric, data);
- setDetailData(prev => ({ ...prev, [metric]: data }));
+ setDetailData((prev) => ({ ...prev, [metric]: data }));
setError(null);
return data;
+ } catch (error) {
+ console.error(`Error fetching detail data for ${metric}:`, error);
+ setError(error.response?.data?.error || error.message);
+ return null;
+ } finally {
+ setDetailDataLoading((prev) => ({ ...prev, [metric]: false }));
}
-
- // For refunds and cancellations
- if (['refunds', 'cancellations'].includes(metric)) {
- const response = await axios.get('/api/klaviyo/events/stats/details', {
- params: {
- ...params,
- eventType: metric === 'refunds' ? 'PAYMENT_REFUNDED' : 'CANCELED_ORDER'
- }
- });
- const data = response.data.stats;
-
- // Transform the data to match the expected format
- const transformedData = data.map(day => ({
- ...day,
- timestamp: day.timestamp,
- refunds: metric === 'refunds' ? {
- total: day.refunds?.total || 0,
- count: day.refunds?.count || 0,
- reasons: day.refunds?.reasons || {}
- } : undefined,
- canceledOrders: metric === 'cancellations' ? {
- total: day.canceledOrders?.total || 0,
- count: day.canceledOrders?.count || 0,
- reasons: day.canceledOrders?.reasons || {}
- } : undefined
- }));
-
- setCacheData(detailTimeRange, metric, transformedData);
- setDetailData(prev => ({ ...prev, [metric]: transformedData }));
- setError(null);
- return transformedData;
- }
-
- // For order range
- if (metric === 'order_range') {
- const response = await axios.get('/api/klaviyo/events/stats/details', {
- params: {
- ...params,
- eventType: 'PLACED_ORDER'
- }
- });
- const data = response.data.stats;
- console.log('Fetched order range data:', data);
- setCacheData(detailTimeRange, metric, data);
- setDetailData(prev => ({ ...prev, [metric]: data }));
- setError(null);
- return data;
- }
-
- // For all other metrics
- const response = await axios.get('/api/klaviyo/events/stats/details', { params });
- const data = response.data.stats;
- setCacheData(detailTimeRange, metric, data);
- setDetailData(prev => ({ ...prev, [metric]: data }));
- setError(null);
- return data;
- } catch (error) {
- console.error(`Error fetching detail data for ${metric}:`, error);
- setError(error.response?.data?.error || error.message);
- return null;
- } finally {
- setDetailDataLoading(prev => ({ ...prev, [metric]: false }));
- }
- }, [timeRange, startDate, endDate, shouldUseLast30Days, setCacheData, getCacheData]);
+ },
+ [
+ timeRange,
+ startDate,
+ endDate,
+ shouldUseLast30Days,
+ setCacheData,
+ getCacheData,
+ ]
+ );
// Corrected preloadDetailData function
const preloadDetailData = useCallback(() => {
const metrics = [
- 'revenue',
- 'orders',
- 'average_order',
- 'refunds',
- 'cancellations',
- 'order_range',
- 'pre_orders',
- 'local_pickup',
- 'on_hold'
+ "revenue",
+ "orders",
+ "average_order",
+ "refunds",
+ "cancellations",
+ "order_range",
+ "pre_orders",
+ "local_pickup",
+ "on_hold",
];
return Promise.all(
- metrics.map(metric => fetchDetailData(metric, metric))
- ).catch(error => {
- console.error('Error during detail data preload:', error);
+ metrics.map((metric) => fetchDetailData(metric, metric))
+ ).catch((error) => {
+ console.error("Error during detail data preload:", error);
});
}, [fetchDetailData]);
// Move trend calculation functions inside the component
const calculateTrend = useCallback((current, previous) => {
if (!current || !previous) return null;
- const trend = current >= previous ? 'up' : 'down';
+ const trend = current >= previous ? "up" : "down";
const diff = Math.abs(current - previous);
- const percentage = ((diff / previous) * 100);
-
- return {
- trend,
+ const percentage = (diff / previous) * 100;
+
+ return {
+ trend,
value: percentage,
current,
- previous
+ previous,
};
}, []);
const calculateRevenueTrend = useCallback(() => {
- if (!stats?.prevPeriodRevenue && stats?.prevPeriodRevenue !== 0) return null;
-
+ if (!stats?.prevPeriodRevenue && stats?.prevPeriodRevenue !== 0)
+ return null;
+
// For incomplete periods, compare projected revenue to previous period
// For complete periods, compare actual revenue to previous period
- const currentRevenue = stats.periodProgress < 100 ? stats.projectedRevenue : stats.revenue;
+ const currentRevenue =
+ stats.periodProgress < 100 ? stats.projectedRevenue : stats.revenue;
const prevRevenue = stats.prevPeriodRevenue;
-
+
if (!currentRevenue || !prevRevenue) return null;
-
- const trend = currentRevenue >= prevRevenue ? 'up' : 'down';
+
+ const trend = currentRevenue >= prevRevenue ? "up" : "down";
const diff = Math.abs(currentRevenue - prevRevenue);
- const percentage = ((diff / prevRevenue) * 100);
-
- return {
- trend,
+ const percentage = (diff / prevRevenue) * 100;
+
+ return {
+ trend,
value: percentage,
current: currentRevenue,
- previous: prevRevenue
+ previous: prevRevenue,
};
}, [stats]);
@@ -1229,29 +1416,30 @@ const StatCards = ({
// Initial load effect
useEffect(() => {
let isMounted = true;
-
+
const loadData = async () => {
try {
setLoading(true);
setStats(null);
- const params = timeRange === 'custom'
- ? { startDate, endDate }
- : { timeRange };
+ const params =
+ timeRange === "custom" ? { startDate, endDate } : { timeRange };
+
+ const response = await axios.get("/api/klaviyo/events/stats", {
+ params,
+ });
- const response = await axios.get('/api/klaviyo/events/stats', { params });
-
if (!isMounted) return;
setDateRange(response.data.timeRange);
setStats(response.data.stats);
- setLastUpdate(DateTime.now().setZone('America/New_York'));
+ setLastUpdate(DateTime.now().setZone("America/New_York"));
setError(null);
// Start preloading detail data
preloadDetailData();
} catch (error) {
- console.error('Error loading data:', error);
+ console.error("Error loading data:", error);
if (isMounted) {
setError(error.message);
}
@@ -1264,22 +1452,64 @@ const StatCards = ({
};
loadData();
- return () => { isMounted = false; };
+ return () => {
+ isMounted = false;
+ };
}, [timeRange, startDate, endDate]);
+ // Load smart projection separately
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadProjection = async () => {
+ if (!stats?.periodProgress || stats.periodProgress >= 100) return;
+
+ try {
+ setProjectionLoading(true);
+ const params =
+ timeRange === "custom" ? { startDate, endDate } : { timeRange };
+
+ const response = await axios.get("/api/klaviyo/events/projection", {
+ params,
+ });
+
+ if (!isMounted) return;
+ setProjection(response.data);
+ } catch (error) {
+ console.error("Error loading projection:", error);
+ } finally {
+ if (isMounted) {
+ setProjectionLoading(false);
+ }
+ }
+ };
+
+ loadProjection();
+ return () => {
+ isMounted = false;
+ };
+ }, [timeRange, startDate, endDate, stats?.periodProgress]);
+
// Auto-refresh for 'today' view
useEffect(() => {
- if (timeRange !== 'today') return;
+ if (timeRange !== "today") return;
const interval = setInterval(async () => {
try {
- const response = await axios.get('/api/klaviyo/events/stats', {
- params: { timeRange: 'today' }
- });
- setStats(response.data.stats);
- setLastUpdate(DateTime.now().setZone('America/New_York'));
+ const [statsResponse, projectionResponse] = await Promise.all([
+ axios.get("/api/klaviyo/events/stats", {
+ params: { timeRange: "today" },
+ }),
+ axios.get("/api/klaviyo/events/projection", {
+ params: { timeRange: "today" },
+ }),
+ ]);
+
+ setStats(statsResponse.data.stats);
+ setProjection(projectionResponse.data);
+ setLastUpdate(DateTime.now().setZone("America/New_York"));
} catch (error) {
- console.error('Error auto-refreshing stats:', error);
+ console.error("Error auto-refreshing stats:", error);
}
}, 60000);
@@ -1288,10 +1518,17 @@ const StatCards = ({
// Modified AsyncDetailView component
const AsyncDetailView = memo(({ metric, type, orderCount }) => {
- const detailTimeRange = shouldUseLast30Days(metric) ? 'last30days' : timeRange;
- const cachedData = detailData[metric] || getCacheData(detailTimeRange, metric);
+ const detailTimeRange = shouldUseLast30Days(metric)
+ ? "last30days"
+ : timeRange;
+ const cachedData =
+ detailData[metric] || getCacheData(detailTimeRange, metric);
const isLoading = detailDataLoading[metric];
- const isOrderTypeMetric = ['pre_orders', 'local_pickup', 'on_hold'].includes(metric);
+ const isOrderTypeMetric = [
+ "pre_orders",
+ "local_pickup",
+ "on_hold",
+ ].includes(metric);
useEffect(() => {
let isMounted = true;
@@ -1299,33 +1536,38 @@ const StatCards = ({
const loadData = async () => {
if (!cachedData && !isLoading) {
// Pass type only for order type metrics
- const data = await fetchDetailData(metric, isOrderTypeMetric ? metric : undefined);
+ const data = await fetchDetailData(
+ metric,
+ isOrderTypeMetric ? metric : undefined
+ );
if (!isMounted) return;
// The state updates are handled in fetchDetailData
}
};
loadData();
- return () => { isMounted = false; };
+ return () => {
+ isMounted = false;
+ };
}, [metric, timeRange, isOrderTypeMetric]); // Depend on isOrderTypeMetric
if (isLoading || (!cachedData && !error)) {
switch (metric) {
- case 'revenue':
- case 'orders':
- case 'average_order':
+ case "revenue":
+ case "orders":
+ case "average_order":
return ;
- case 'refunds':
- case 'cancellations':
- case 'order_range':
- case 'pre_orders':
- case 'local_pickup':
- case 'on_hold':
+ case "refunds":
+ case "cancellations":
+ case "order_range":
+ case "pre_orders":
+ case "local_pickup":
+ case "on_hold":
return ;
- case 'brands_categories':
- case 'shipping':
+ case "brands_categories":
+ case "shipping":
return ;
- case 'peak_hour':
+ case "peak_hour":
return ;
default:
return Loading...
;
@@ -1337,82 +1579,113 @@ const StatCards = ({
Error
-
- Failed to load stats: {error}
-
+ Failed to load stats: {error}
);
}
- if (!cachedData) return No data available for the selected time range.
;
+ if (!cachedData)
+ return (
+
+ No data available for the selected time range.
+
+ );
switch (metric) {
- case 'revenue':
+ case "revenue":
return ;
- case 'orders':
+ case "orders":
return ;
- case 'average_order':
- return ;
- case 'refunds':
+ case "average_order":
+ return (
+
+ );
+ case "refunds":
return ;
- case 'cancellations':
+ case "cancellations":
return ;
- case 'order_range':
+ case "order_range":
return ;
- case 'pre_orders':
- case 'local_pickup':
- case 'on_hold':
+ case "pre_orders":
+ case "local_pickup":
+ case "on_hold":
return ;
default:
- return Invalid metric selected.
;
+ return (
+ Invalid metric selected.
+ );
}
});
- AsyncDetailView.displayName = 'AsyncDetailView';
+ AsyncDetailView.displayName = "AsyncDetailView";
// Modified getDetailComponent to use memoized components
const getDetailComponent = useCallback(() => {
if (!selectedMetric || !stats) {
- return No data available for the selected time range.
;
+ return (
+
+ No data available for the selected time range.
+
+ );
}
const data = detailData[selectedMetric];
const isLoading = detailDataLoading[selectedMetric];
- const isOrderTypeMetric = ['pre_orders', 'local_pickup', 'on_hold'].includes(selectedMetric);
+ const isOrderTypeMetric = [
+ "pre_orders",
+ "local_pickup",
+ "on_hold",
+ ].includes(selectedMetric);
if (isLoading) {
return ;
}
switch (selectedMetric) {
- case 'revenue':
- case 'best_revenue_day':
+ case "revenue":
+ case "best_revenue_day":
return ;
- case 'orders':
+ case "orders":
return ;
- case 'average_order':
- return ;
- case 'refunds':
+ case "average_order":
+ return (
+
+ );
+ case "refunds":
return ;
- case 'cancellations':
+ case "cancellations":
return ;
- case 'order_range':
+ case "order_range":
return ;
- case 'pre_orders':
- case 'local_pickup':
- case 'on_hold':
- return ;
- case 'brands_categories':
+ case "pre_orders":
+ case "local_pickup":
+ case "on_hold":
+ return (
+
+ );
+ case "brands_categories":
return ;
- case 'shipping':
+ case "shipping":
return ;
- case 'peak_hour':
- if (!['today', 'yesterday'].includes(timeRange)) {
- return Peak hour details are only available for single-day periods.
;
+ case "peak_hour":
+ if (!["today", "yesterday"].includes(timeRange)) {
+ return (
+
+ Peak hour details are only available for single-day periods.
+
+ );
}
return ;
default:
- return Invalid metric selected.
;
+ return (
+ Invalid metric selected.
+ );
}
}, [selectedMetric, stats, timeRange, detailData, detailDataLoading]);
@@ -1451,9 +1724,13 @@ const StatCards = ({
- {title}
+
+ {title}
+
{description && (
- {description}
+
+ {description}
+
)}
@@ -1482,9 +1759,7 @@ const StatCards = ({
Error
-
- Failed to load stats: {error}
-
+ Failed to load stats: {error}
@@ -1496,7 +1771,7 @@ const StatCards = ({
const revenueTrend = calculateRevenueTrend();
const orderTrend = calculateOrderTrend();
const aovTrend = calculateAOVTrend();
- const isSingleDay = ['today', 'yesterday'].includes(timeRange);
+ const isSingleDay = ["today", "yesterday"].includes(timeRange);
return (
@@ -1504,13 +1779,41 @@ const StatCards = ({
-
{title}
+
+ {title}
+
{lastUpdate && !loading && (
-
Last updated {lastUpdate.toFormat("h:mm a")}
+
+ Last updated {lastUpdate.toFormat("h:mm a")}
+ {projection?.confidence > 0 && !projectionLoading && (
+
+
+
+
+ (
+
+ {Math.round(projection.confidence * 100)}%
+
+ )
+
+
+
+ Confidence level of revenue projection based on historical data patterns
+
+
+
+ )}
+
)}
+
-
@@ -1533,44 +1836,92 @@ const StatCards = ({
title="Total Revenue"
value={formatCurrency(stats?.revenue || 0)}
description={
- stats?.periodProgress < 100
- ? Proj: Projected: {formatCurrency(stats.projectedRevenue)}
- : `Previous: ${formatCurrency(stats.prevPeriodRevenue)}`
+
+
+ {stats?.periodProgress < 100 ? (
+
+
Proj:
+
Projected:
+ {projectionLoading ? (
+
+
+
+ ) : (
+ formatCurrency(
+ projection?.projectedRevenue || stats.projectedRevenue
+ )
+ )}
+
+ ) : (
+
+ Prev:
+ Previous:
+ {formatCurrency(stats.prevPeriodRevenue || 0)}
+
+ )}
+
+
+ }
+ progress={
+ stats?.periodProgress < 100 ? stats.periodProgress : undefined
+ }
+ trend={projectionLoading && stats?.periodProgress < 100 ? undefined : revenueTrend?.trend}
+ trendValue={
+ projectionLoading && stats?.periodProgress < 100 ? (
+
+
+
+
+ ) : revenueTrend?.value ? (
+
+
+
+ {formatPercentage(revenueTrend.value)}
+
+
+ Previous Period: {formatCurrency(stats.prevPeriodRevenue || 0)}
+
+
+
+ ) : null
}
- progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined}
- trend={revenueTrend?.trend}
- trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null}
colorClass="text-green-600 dark:text-green-400"
icon={DollarSign}
iconColor="text-green-500"
- onDetailsClick={() => setSelectedMetric('revenue')}
+ onDetailsClick={() => setSelectedMetric("revenue")}
isLoading={loading || !stats}
/>
-
+
setSelectedMetric('orders')}
+ onDetailsClick={() => setSelectedMetric("orders")}
isLoading={loading || !stats}
/>
-
+
setSelectedMetric('average_order')}
+ onDetailsClick={() => setSelectedMetric("average_order")}
isLoading={loading || !stats}
/>
@@ -1581,7 +1932,7 @@ const StatCards = ({
colorClass="text-indigo-600 dark:text-indigo-400"
icon={Tags}
iconColor="text-indigo-500"
- onDetailsClick={() => setSelectedMetric('brands_categories')}
+ onDetailsClick={() => setSelectedMetric("brands_categories")}
isLoading={loading || !stats}
/>
@@ -1592,63 +1943,91 @@ const StatCards = ({
colorClass="text-teal-600 dark:text-teal-400"
icon={Package}
iconColor="text-teal-500"
- onDetailsClick={() => setSelectedMetric('shipping')}
+ onDetailsClick={() => setSelectedMetric("shipping")}
isLoading={loading || !stats}
/>
setSelectedMetric('pre_orders')}
+ onDetailsClick={() => setSelectedMetric("pre_orders")}
isLoading={loading || !stats}
/>
setSelectedMetric('local_pickup')}
+ onDetailsClick={() => setSelectedMetric("local_pickup")}
isLoading={loading || !stats}
/>
setSelectedMetric('on_hold')}
+ onDetailsClick={() => setSelectedMetric("on_hold")}
isLoading={loading || !stats}
/>
{isSingleDay ? (
setSelectedMetric('peak_hour')}
+ onDetailsClick={() => setSelectedMetric("peak_hour")}
isLoading={loading || !stats}
/>
) : (
setSelectedMetric('refunds')}
+ onDetailsClick={() => setSelectedMetric("refunds")}
isLoading={loading || !stats}
/>
setSelectedMetric('cancellations')}
+ onDetailsClick={() => setSelectedMetric("cancellations")}
isLoading={loading || !stats}
/>
setSelectedMetric('order_range')}
+ onDetailsClick={() => setSelectedMetric("order_range")}
isLoading={loading || !stats}
/>
@@ -1696,7 +2077,14 @@ const StatCards = ({
setSelectedMetric(null)}
- title={selectedMetric ? `${selectedMetric.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} Details` : ''}
+ title={
+ selectedMetric
+ ? `${selectedMetric
+ .split("_")
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
+ .join(" ")} Details`
+ : ""
+ }
>
{getDetailComponent()}
@@ -1705,4 +2093,4 @@ const StatCards = ({
);
};
-export default StatCards;
+export default StatCards;