import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DollarSign,
ShoppingCart,
Package,
Clock,
Map,
Tags,
Star,
XCircle,
TrendingUp,
AlertCircle,
Box,
RefreshCcw,
CircleDollarSign,
ArrowDown,
ArrowUp,
MapPin
} from "lucide-react";
import { DateTime } from "luxon";
import { TimeRangeSelect } from "@/components/dashboard/TimeRangeSelect";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
Area,
AreaChart,
ComposedChart,
} from "recharts";
const CHART_COLORS = [
"hsl(var(--primary))",
"hsl(var(--secondary))",
"#8b5cf6",
"#10b981",
"#f59e0b",
"#ef4444",
];
// Formatting utilities
const formatCurrency = (value, minimumFractionDigits = 0) => {
if (!value || isNaN(value)) return "$0";
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits,
maximumFractionDigits: minimumFractionDigits,
}).format(value);
};
const formatHour = (hour) => {
const hourNum = parseInt(hour);
if (hourNum === 0) return "12am";
if (hourNum === 12) return "12pm";
if (hourNum > 12) return `${hourNum - 12}pm`;
return `${hourNum}am`;
};
const normalizePaymentMethod = (method) => {
if (method.toLowerCase().includes("credit card")) return "Credit Card";
if (method.toLowerCase().includes("gift")) return "Gift Card";
return method;
};
const formatPercent = (value, total) => {
if (!total || !value) return "0%";
return `${((value / total) * 100).toFixed(1)}%`;
};
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';
default: return timeRange;
}
};
const formatShipMethod = (method) => {
if (!method) return "Standard Shipping";
return method
.replace("usps_", "USPS ")
.replace("ups_", "UPS ")
.replace("fedex_", "FedEx ")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
// Component building blocks
const DetailCard = ({ title, icon: Icon, children }) => (
{Icon && }
{title}
{children}
);
const SkeletonMetricCard = () => (
);
const SkeletonChart = ({ type = "line" }) => (
{type === "bar" ? (
{[...Array(24)].map((_, i) => (
))}
) : type === "range" ? (
{[...Array(5)].map((_, i) => (
))}
) : (
{[...Array(5)].map((_, i) => (
))}
)}
);
const SkeletonTable = ({ rows = 5 }) => (
{/* Header */}
{[...Array(3)].map((_, i) => (
))}
{/* Rows */}
{[...Array(rows)].map((_, i) => (
{[...Array(3)].map((_, j) => (
))}
))}
);
const SkeletonMetricGrid = () => (
{Array(12)
.fill(0)
.map((_, i) => (
))}
);
const StatRow = ({ label, value, change, emphasize }) => (
{label}
{value}
{change && (
0 ? "text-green-500" : "text-red-500"
}`}
>
{change > 0 ? "↑" : "↓"} {Math.abs(change)}%
)}
);
const DetailSection = ({ title, children }) => (
{title}
{children}
);
const formatChartDate = (value) => {
if (!value) return '';
try {
return DateTime.fromISO(value).setZone('America/New_York').toFormat('LLL d');
} catch (error) {
console.error("[KLAVIYO STATS] Date formatting error:", error);
return value;
}
};
export const RevenueDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
// Ensure we have valid daily data
if (!metrics?.revenue?.daily || !Array.isArray(metrics.revenue.daily)) {
console.error("[KLAVIYO STATS] Invalid daily revenue data:", metrics?.revenue);
return null;
}
// Sort daily data by date to ensure correct order
const revenueData = metrics.revenue.daily
.sort((a, b) => DateTime.fromISO(a.date).toMillis() - DateTime.fromISO(b.date).toMillis())
.map(day => ({
date: day.date,
revenue: parseFloat(day.value) || 0,
orders: parseInt(day.orders) || 0,
items: parseInt(day.items) || 0
}));
console.log("[KLAVIYO STATS] Processed revenue data:", {
totalDays: revenueData.length,
dates: revenueData.map(d => d.date),
totals: {
revenue: revenueData.reduce((sum, day) => sum + day.revenue, 0),
orders: revenueData.reduce((sum, day) => sum + day.orders, 0),
items: revenueData.reduce((sum, day) => sum + day.items, 0)
}
});
return (
formatCurrency(value, 0)}
width={80}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
[formatCurrency(value, 0), "Revenue"]}
/>
);
};
export const OrdersDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return (
);
}
const orderData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
orders: day.orders || 0,
})) || [];
const hourlyData =
metrics.hourly_distribution
?.map((count, hour) => ({
hour,
orders: count,
}))
.filter((data) => data.orders > 0) || [];
return (
[value.toLocaleString(), "Orders"]}
/>
{hourlyData.length > 0 && (
Hourly Distribution
[value.toLocaleString(), "Orders"]}
labelFormatter={formatHour}
/>
)}
);
};
export const AverageOrderDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const avgOrderData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
average: day.orders > 0 ? day.value / day.orders : 0,
})) || [];
return (
formatCurrency(value, 0)}
minDays={30}
/>
);
};
const DataTable = ({ title, data, isLoading }) => {
if (isLoading) {
return (
{Array(10).fill(0).map((_, i) => (
))}
);
}
return (
{title}
Name
Count
{data.map(([name, count]) => (
{name}
{count}
))}
);
};
export const BrandsAndCategoriesDetails = ({ metrics, isLoading }) => {
const brands = Object.entries(metrics?.orders?.brands || {})
.sort(([, a], [, b]) => b - a)
.slice(0, 20);
const categories = Object.entries(metrics?.orders?.categories || {})
.sort(([, a], [, b]) => b - a)
.slice(0, 20);
return (
);
};
export const ShippedOrdersDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return (
{Array(8)
.fill(0)
.map((_, i) => (
))}
);
}
const shippedData = metrics.revenue?.daily?.map((day) => ({
date: day.date,
shipped_orders: day.shipped_orders || 0,
})) || [];
const locations = Object.entries(metrics.orders?.shipping_states || {})
.sort(([, a], [, b]) => b - a)
.filter(([location]) => location);
return (
[value.toLocaleString(), "Orders"]}
/>
Top Shipping Locations
{locations.map(([location, count]) => (
{location}
{count.toLocaleString()}
))}
);
};
export const PreOrdersDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const preOrderData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
percentage:
day.orders > 0 ? ((day.pre_orders || 0) / day.orders) * 100 : 0,
})) || [];
return (
`${Math.round(value)}%`}
width={60}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
} />
);
};
export const LocalPickupDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const pickupData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
percentage:
day.orders > 0 ? ((day.local_pickup || 0) / day.orders) * 100 : 0,
})) || [];
return (
`${Math.round(value)}%`}
width={60}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
} />
);
};
export const OnHoldDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const onHoldData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
percentage:
day.orders > 0 ? ((day.status?.on_hold || 0) / day.orders) * 100 : 0,
})) || [];
return (
`${Math.round(value)}%`}
width={60}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
} />
);
};
export const ShippingDetails = ({ metrics }) => {
const locations = Object.entries(
metrics.orders?.shipping_locations || {}
).sort(([, a], [, b]) => b - a);
const shippedOrders = metrics.orders?.shipped_orders || 0;
const totalOrders = metrics.orders?.total || 0;
return (
{Object.entries(metrics.orders?.shipping_methods || {})
.sort(([, a], [, b]) => b - a)
.map(([method, count]) => (
{formatShipMethod(method)}
{count.toLocaleString()}
({((count / shippedOrders) * 100).toFixed(1)}%)
))}
{locations.map(([location, count]) => (
{location}
{count.toLocaleString()}
({((count / totalOrders) * 100).toFixed(1)}%)
))}
);
};
export const ProductsDetails = ({ metrics }) => {
const brands = Object.entries(metrics.orders?.brands || {}).sort(
([, a], [, b]) => b - a
);
const categories = Object.entries(metrics.orders?.categories || {}).sort(
([, a], [, b]) => b - a
);
return (
{brands.map(([brand, count], index) => (
{index + 1}.
{brand}
{count.toLocaleString()}
({((count / metrics.orders.items_total) * 100).toFixed(1)}%)
))}
{categories.map(([category, count], index) => (
{index + 1}.
{category}
{count.toLocaleString()}
({((count / metrics.orders.items_total) * 100).toFixed(1)}%)
))}
);
};
export const RefundsDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const refundData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
amount: day.refunds?.total || 0,
})) || [];
return (
formatCurrency(value, 0)}
minDays={30}
/>
);
};
export const PeakHourDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const hourlyData =
metrics.hourly_distribution
?.map((count, hour) => ({
hour,
orders: count,
}))
.filter((data) => data.orders > 0) || [];
return (
[value, "Orders"]}
labelFormatter={formatHour}
content={}
/>
);
};
export const CancellationsDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const cancelData =
metrics.revenue?.daily?.map((day) => ({
date: day.date,
amount: day.cancellations?.total || 0,
})) || [];
return (
formatCurrency(value, 0)}
minDays={30}
/>
);
};
export const OrderRangeDetails = ({ metrics, isLoading }) => {
if (isLoading) {
return ;
}
const rangeData = metrics.revenue?.daily
?.map((day) => {
if (!day.orders_list?.length) return null;
const validOrders = day.orders_list
.map((order) => parseFloat(order.TotalAmount))
.filter((amount) => amount > 0);
if (!validOrders.length) return null;
return {
date: day.date,
min: Math.min(...validOrders),
max: Math.max(...validOrders),
avg: validOrders.reduce((a, b) => a + b, 0) / validOrders.length,
};
})
.filter(Boolean);
return (
formatCurrency(value, 0)}
width={80}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
} />
);
};
const MetricCard = React.memo(
({ title, value, subtitle, icon: Icon, iconColor, onClick, secondaryValue }) => (
{title}
{value}
{subtitle}
{secondaryValue && (
{secondaryValue}
)}
)
);
MetricCard.displayName = "MetricCard";
const ChartTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
return (
{formatChartDate(label)}
{payload.map((entry, index) => (
{entry.name}:
{typeof entry.value === "number"
? entry.name.toLowerCase().includes("revenue")
? formatCurrency(entry.value)
: entry.value.toLocaleString()
: entry.value}
))}
);
};
const TimeSeriesChart = ({
data,
valueKey,
label,
type = "line",
valueFormatter = (v) => v,
}) => {
const ChartComponent =
type === "line" ? LineChart : type === "bar" ? BarChart : AreaChart;
return (
} />
{type === "line" && (
)}
{type === "bar" && (
)}
{type === "area" && (
)}
);
};
// Status components
const OrderStatusTags = React.memo(({ details }) => {
if (!details) return null;
const tags = [
{
condition: details.HasPreorder,
label: "Pre-order",
color: "bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300",
},
{
condition: details.LocalPickup,
label: "Local Pickup",
color:
"bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300",
},
{
condition: details.IsOnHold,
label: "On Hold",
color:
"bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300",
},
{
condition: details.HasDigitalGC,
label: "Gift Card",
color:
"bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300",
},
];
return (
{tags.map(
({ condition, label, color }, index) =>
condition && (
{label}
)
)}
);
});
OrderStatusTags.displayName = "OrderStatusTags";
const PromotionalInfo = React.memo(({ details }) => {
if (!details?.PromosUsedReg?.length && !details?.PointsDiscount) return null;
return (
Savings Applied
{details.PromosUsedReg?.map(([code, amount], index) => (
{code}
-{formatCurrency(amount)}
))}
{details.PointsDiscount > 0 && (
Points Discount
-{formatCurrency(details.PointsDiscount)}
)}
);
});
PromotionalInfo.displayName = "PromotionalInfo";
const ShippingInfo = React.memo(({ details }) => (
Shipping Address
{details.ShippingName}
{details.ShippingStreet1}
{details.ShippingStreet2 &&
{details.ShippingStreet2}
}
{details.ShippingCity}, {details.ShippingState} {details.ShippingZip}
{details.ShippingCountry !== "US" && (
{details.ShippingCountry}
)}
{details.TrackingNumber && (
Tracking Information
{formatShipMethod(details.ShipMethod)}
{details.TrackingNumber}
)}
));
ShippingInfo.displayName = "ShippingInfo";
const OrderSummary = React.memo(({ details }) => (
Subtotal
Items ({details.Items?.length || 0})
{formatCurrency(details.Subtotal)}
{details.PointsDiscount > 0 && (
Points Discount
-{formatCurrency(details.PointsDiscount)}
)}
{details.TotalDiscounts > 0 && (
Discounts
-{formatCurrency(details.TotalDiscounts)}
)}
Shipping
Shipping Cost
{formatCurrency(details.ShippingTotal)}
{details.RushFee > 0 && (
Rush Fee
{formatCurrency(details.RushFee)}
)}
{details.SalesTax > 0 && (
Sales Tax
{formatCurrency(details.SalesTax)}
)}
Total
{details.TotalSavings > 0 && (
You saved {formatCurrency(details.TotalSavings)}
)}
{formatCurrency(details.TotalAmount)}
{details.Payments?.length > 0 && (
Payment Details
{details.Payments.map(([method, amount], index) => (
{method}
{formatCurrency(amount)}
))}
)}
));
OrderSummary.displayName = "OrderSummary";
// Loading and empty states
const LoadingState = () => (
);
const ErrorState = ({ message }) => (
Error Loading Metrics
{message}
);
// Add data validation and normalization utilities
const validateMetricsData = (data) => {
if (!data || typeof data !== 'object') {
throw new Error('Invalid metrics data: expected object');
}
if (!data.revenue?.daily || !Array.isArray(data.revenue.daily)) {
throw new Error('Invalid metrics data: missing or invalid daily revenue data');
}
return true;
};
const normalizeMetricsData = (data) => {
// Ensure all required properties exist with default values
const normalized = {
revenue: {
total: parseFloat(data.revenue?.total) || 0,
daily: data.revenue?.daily?.map(day => ({
date: day.date,
value: parseFloat(day.value) || 0,
orders: parseInt(day.orders) || 0,
items: parseInt(day.items) || 0,
pre_orders: parseInt(day.pre_orders) || 0,
local_pickup: parseInt(day.local_pickup) || 0,
refunds: {
total: parseFloat(day.refunds?.total) || 0,
count: parseInt(day.refunds?.count) || 0
},
cancellations: {
total: parseFloat(day.cancellations?.total) || 0,
count: parseInt(day.cancellations?.count) || 0
},
status: {
on_hold: parseInt(day.status?.on_hold) || 0,
processing: parseInt(day.status?.processing) || 0,
completed: parseInt(day.status?.completed) || 0
},
hourly_orders: Array.isArray(day.hourly_orders) ?
day.hourly_orders.map(count => parseInt(count) || 0) :
Array(24).fill(0),
payment_methods: day.payment_methods || {},
categories: day.categories || {},
brands: day.brands || {},
shipping_states: day.shipping_states || {},
unique_customers: Array.isArray(day.unique_customers) ?
[...new Set(day.unique_customers)] : []
})) || []
},
orders: {
total: parseInt(data.orders?.total) || 0,
items_total: parseInt(data.orders?.items_total) || 0,
pre_orders: parseInt(data.orders?.pre_orders) || 0,
local_pickup: parseInt(data.orders?.local_pickup) || 0,
unique_customers: Array.isArray(data.orders?.unique_customers) ?
[...new Set(data.orders.unique_customers)] : [],
payment_methods: data.orders?.payment_methods || {},
categories: data.orders?.categories || {},
brands: data.orders?.brands || {},
shipping_states: data.orders?.shipping_states || {},
status: {
pre_order: parseInt(data.orders?.status?.pre_order) || 0,
ready: parseInt(data.orders?.status?.ready) || 0,
on_hold: parseInt(data.orders?.status?.on_hold) || 0
}
},
hourly_distribution: Array.isArray(data.hourly_distribution) ?
data.hourly_distribution.map(count => parseInt(count) || 0) :
Array(24).fill(0),
refunds: {
count: parseInt(data.refunds?.count) || 0,
total: parseFloat(data.refunds?.total) || 0
},
cancellations: {
count: parseInt(data.cancellations?.count) || 0,
total: parseFloat(data.cancellations?.total) || 0
}
};
// Sort daily data by date
normalized.revenue.daily.sort((a, b) => new Date(a.date) - new Date(b.date));
// Calculate any derived values
if (normalized.orders.total > 0) {
normalized.orders.average_order_value = normalized.revenue.total / normalized.orders.total;
normalized.orders.average_items = normalized.orders.items_total / normalized.orders.total;
}
return normalized;
};
// Main component
const KlaviyoStats = ({ className }) => {
const [timeRange, setTimeRange] = useState("today");
const [timeRangeChanging, setTimeRangeChanging] = useState(false);
const [metrics, setMetrics] = useState(null);
const [previousMetrics, setPreviousMetrics] = useState(null);
const [error, setError] = useState(null);
const [selectedMetric, setSelectedMetric] = useState(null);
const [lastUpdate, setLastUpdate] = useState(null);
// Extended data pre-loaded in background
const [extendedData, setExtendedData] = useState(null);
const [isLoadingExtraData, setIsLoadingExtraData] = useState(false);
const [isLoadingPrevious, setIsLoadingPrevious] = useState(false);
const handleTimeRangeChange = useCallback(async (newRange) => {
const validRanges = [
'today',
'yesterday',
'last2days',
'last7days',
'last30days',
'last90days',
'previous7days',
'previous30days',
'previous90days'
];
if (!validRanges.includes(newRange)) {
console.error(`Invalid time range: ${newRange}`);
return;
}
setTimeRangeChanging(true);
setTimeRange(newRange);
}, []);
const isSingleDay = timeRange === "today" || timeRange === "yesterday";
const processedMetrics = useMemo(() => {
if (!metrics) return null;
console.log("[KLAVIYO STATS] Processing metrics:", {
hasMetrics: !!metrics,
hasPreviousMetrics: !!previousMetrics,
revenue: metrics.revenue,
orders: metrics.orders,
refunds: metrics.refunds,
cancellations: metrics.cancellations,
timeRange
});
const getComparison = (current, previous) => {
if (!previous) return null;
const diff = current - previous;
return {
diff,
percent: previous ? (diff / previous) * 100 : 0,
increased: diff > 0,
};
};
// Ensure we have valid numbers for all metrics
const currentRevenue = parseFloat(metrics.revenue?.total) || 0;
const previousRevenue = parseFloat(previousMetrics?.revenue?.total) || 0;
const revenueComparison = getComparison(currentRevenue, previousRevenue);
const totalOrders = parseInt(metrics.orders?.total) || 0;
const totalItems = parseInt(metrics.orders?.items_total) || 0;
const shippedOrders = parseInt(metrics.orders?.shipped_orders) || 0;
const onHoldCount = parseInt(metrics.orders?.status?.on_hold) || 0;
const preOrderCount = parseInt(metrics.orders?.pre_orders) || 0;
const localPickupCount = parseInt(metrics.orders?.local_pickup) || 0;
const refundCount = parseInt(metrics.refunds?.count) || 0;
const refundTotal = parseFloat(metrics.refunds?.total) || 0;
const cancelCount = parseInt(metrics.cancellations?.count) || 0;
const cancelTotal = parseFloat(metrics.cancellations?.total) || 0;
const avgOrderValue = totalOrders > 0 ? currentRevenue / totalOrders : 0;
const avgItems = totalOrders > 0 ? totalItems / totalOrders : 0;
const processed = {
revenue: {
current: currentRevenue,
previous: previousRevenue,
comparison: revenueComparison,
},
orders: {
current: totalOrders,
items: totalItems,
avgItems: avgItems,
avgValue: avgOrderValue,
shipped_orders: shippedOrders,
locations: Object.keys(metrics.orders?.shipping_locations || {}).length,
preOrders: {
count: preOrderCount,
percent: totalOrders > 0 ? (preOrderCount / totalOrders) * 100 : 0,
},
localPickup: {
count: localPickupCount,
percent: totalOrders > 0 ? (localPickupCount / totalOrders) * 100 : 0,
},
onHold: {
count: onHoldCount,
percent: totalOrders > 0 ? (onHoldCount / totalOrders) * 100 : 0,
},
largest: metrics.orders?.largest || { value: 0, items: 0 },
smallest: metrics.orders?.smallest || { value: 0, items: 0 },
},
products: {
brands: Object.keys(metrics.orders?.brands || {}).length,
categories: Object.keys(metrics.orders?.categories || {}).length,
},
peak: metrics.peak_hour || null,
bestDay: metrics.revenue?.best_day || null,
refunds: {
count: refundCount,
total: refundTotal,
},
cancellations: {
count: cancelCount,
total: cancelTotal,
},
};
console.log("[KLAVIYO STATS] Processed metrics:", processed);
return processed;
}, [metrics, previousMetrics, timeRange]);
const RevenueCard = useMemo(() => {
const comparison = processedMetrics?.revenue.comparison;
return (
) : formatCurrency(processedMetrics?.revenue.current, 0)
}
subtitle={
{timeRangeChanging || isLoadingPrevious ? (
) : previousMetrics ? (
<>
{`prev: ${formatCurrency(previousMetrics.revenue?.total || 0, 0)}`}
{comparison && (
{comparison.increased ?
:
}
{Math.abs(comparison.percent).toFixed(1)}%
)}
>
) : null}
}
icon={DollarSign}
iconColor="text-green-500"
onClick={() => setSelectedMetric("revenue")}
/>
);
}, [timeRangeChanging, isLoadingPrevious, processedMetrics, previousMetrics]);
const hasFetchedExtendedData = useRef(false);
const fetchExtendedData = useCallback(async () => {
if (hasFetchedExtendedData.current) return; // Skip fetch if already fetched
setIsLoadingExtraData(true);
try {
const extendedResponse = await fetch(`/api/klaviyo/metrics/last30days`);
const extendedData = await extendedResponse.json();
setExtendedData(extendedData);
hasFetchedExtendedData.current = true; // Mark as fetched
} catch (error) {
console.error("Error fetching extended data:", error);
} finally {
setIsLoadingExtraData(false);
}
}, []);
const fetchData = useCallback(async () => {
console.log("[KLAVIYO STATS] Starting fetchData:", { timeRange });
setTimeRangeChanging(true);
setError(null);
try {
// Fetch current period data
const currentResponse = await fetch(`/api/klaviyo/metrics/${timeRange}`);
if (!currentResponse.ok) {
throw new Error(`Failed to fetch current metrics: ${currentResponse.status}`);
}
const rawData = await currentResponse.json();
// Validate and normalize the data
try {
validateMetricsData(rawData);
const currentData = normalizeMetricsData(rawData);
setMetrics(currentData);
setLastUpdate(DateTime.now().setZone('America/New_York'));
} catch (validationError) {
console.error("[KLAVIYO STATS] Data validation error:", validationError);
throw new Error(`Invalid data structure: ${validationError.message}`);
}
// Fetch previous period data
const prevPeriod = getPreviousPeriod(timeRange);
setIsLoadingPrevious(true);
try {
const previousResponse = await fetch(`/api/klaviyo/metrics/${prevPeriod}`);
if (!previousResponse.ok) {
console.warn(`Failed to fetch previous period metrics: ${previousResponse.status}`);
} else {
const rawPreviousData = await previousResponse.json();
try {
validateMetricsData(rawPreviousData);
const previousData = normalizeMetricsData(rawPreviousData);
setPreviousMetrics(previousData);
} catch (validationError) {
console.warn("[KLAVIYO STATS] Previous period data validation error:", validationError);
}
}
} catch (error) {
console.error("[KLAVIYO STATS] Error fetching previous period:", error);
} finally {
setIsLoadingPrevious(false);
}
console.log("[KLAVIYO STATS] Processed data:", {
timeRange,
totalRevenue: metrics?.revenue?.total,
dailyCount: metrics?.revenue?.daily?.length,
hasPreviousData: !!previousMetrics
});
} catch (error) {
console.error("[KLAVIYO STATS] Error fetching data:", error);
setError(error.message);
} finally {
setTimeRangeChanging(false);
}
}, [timeRange, getPreviousPeriod]);
// Add helper function for getting previous period
const getPreviousPeriod = useCallback((currentRange) => {
switch (currentRange) {
case 'today':
return 'yesterday';
case 'yesterday':
return 'last2days';
case 'last7days':
return 'previous7days';
case 'last30days':
return 'previous30days';
case 'last90days':
return 'previous90days';
default:
return currentRange;
}
}, []);
useEffect(() => {
let isSubscribed = true;
const loadMainData = async () => {
if (!isSubscribed) return;
console.log("[KLAVIYO STATS] Loading main data for timeRange:", timeRange);
await fetchData();
};
const loadExtendedData = async () => {
if (!isSubscribed) return;
console.log("[KLAVIYO STATS] Loading extended data");
await fetchExtendedData();
};
loadMainData().then(() => {
loadExtendedData();
});
let interval;
if (timeRange === "today") {
interval = setInterval(() => {
if (isSubscribed) {
console.log("[KLAVIYO STATS] Auto-refreshing today's data");
loadMainData();
}
}, 5 * 60 * 1000);
}
return () => {
isSubscribed = false;
if (interval) clearInterval(interval);
};
}, [timeRange, fetchData, fetchExtendedData]);
const dataForDetails = extendedData || metrics;
const renderMetricDetails = useCallback(() => {
if (!selectedMetric || !metrics) return null;
const isDetailLoading = !extendedData || timeRangeChanging || isLoadingExtraData;
switch (selectedMetric) {
case "revenue":
return ;
case "orders":
return ;
case "average_order":
return ;
case "brands_categories":
return ;
case "shipped":
return ;
case "pre_orders":
return ;
case "local_pickup":
return ;
case "on_hold":
return ;
case "refunds":
return ;
case "cancellations":
return ;
case "order_range":
return ;
case "peak_hour":
return ;
default:
return null;
}
}, [selectedMetric, metrics, timeRangeChanging, isLoadingExtraData, extendedData, dataForDetails]);
if (error) {
return (
Error loading metrics: {error}
);
}return (
Sales Dashboard
{timeRange === "today" ? (
`Today (${DateTime.now().setZone('America/New_York').toFormat('M/d h:mm a')} ET)`
) : timeRange === "yesterday" ? (
`Yesterday (${DateTime.now().setZone('America/New_York').minus({ days: 1 }).toFormat('M/d')} 1:00 AM - 12:59 AM ET)`
) : (
`${DateTime.now().setZone('America/New_York').minus({
days: timeRange === "last7days" ? 6 :
timeRange === "last30days" ? 29 : 89
}).toFormat('M/d')} - ${DateTime.now().setZone('America/New_York').toFormat('M/d h:mm a')} ET`
)}
{lastUpdate && !timeRangeChanging && (
{lastUpdate.toFormat("hh:mm a")}
)}
{error ? (
) : timeRangeChanging ? (
) : processedMetrics ? (
<>
{RevenueCard}
setSelectedMetric("orders")}
/>
setSelectedMetric("average_order")}
/>
setSelectedMetric("brands_categories")}
/>
setSelectedMetric("shipped")}
/>
setSelectedMetric("pre_orders")}
/>
setSelectedMetric("local_pickup")}
/>
setSelectedMetric("on_hold")}
/>
{isSingleDay ? (
setSelectedMetric("peak_hour")}
/>
) : (
setSelectedMetric("revenue")}
/>
)}
setSelectedMetric("refunds")}
/>
setSelectedMetric("cancellations")}
/>
setSelectedMetric("order_range")}
/>
{selectedMetric && (
)}
>
) : null}
);
};
export default KlaviyoStats;