diff --git a/dashboard/src/components/dashboard/KlaviyoStats.jsx b/dashboard/src/components/dashboard/KlaviyoStats.jsx deleted file mode 100644 index 0b9dc78..0000000 --- a/dashboard/src/components/dashboard/KlaviyoStats.jsx +++ /dev/null @@ -1,2064 +0,0 @@ -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 'twoDaysAgo'; - case 'last7days': return 'last7days'; - case 'last30days': return 'last30days'; - case 'last90days': return 'last90days'; - 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 = () => ( -
-
-
-
-

Loading data...

-
-); - -const ErrorState = ({ message }) => ( -
- -

Error Loading Metrics

-

{message}

-
-); - -// 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 { - const currentResponse = await fetch(`/api/klaviyo/metrics/${timeRange}`); - if (!currentResponse.ok) { - throw new Error(`Failed to fetch current metrics: ${currentResponse.status}`); - } - - const currentData = await currentResponse.json(); - - // Log the raw response data - console.log("[KLAVIYO STATS] Raw API Response:", { - timeRange, - revenue: currentData.revenue, - daily: currentData.revenue?.daily?.map(day => ({ - date: day.date, - value: day.value, - orders: day.orders - })) - }); - - // Ensure we have all required days in the data - const today = DateTime.now().setZone('America/New_York'); - const expectedDays = timeRange === 'last7days' ? 7 : - timeRange === 'last30days' ? 30 : - timeRange === 'last90days' ? 90 : 1; - - const startDate = today.minus({ days: expectedDays - 1 }).startOf('day'); - const endDate = today.endOf('day'); - - // Create an object to store expected dates - const expectedDates = {}; - let currentDate = startDate; - while (currentDate <= endDate) { - const dateKey = currentDate.toFormat('yyyy-MM-dd'); - expectedDates[dateKey] = { - date: dateKey, - value: 0, - orders: 0, - items: 0, - pre_orders: 0, - local_pickup: 0, - refunds: { total: 0, count: 0 }, - cancellations: { total: 0, count: 0 }, - status: { on_hold: 0, processing: 0, completed: 0 } - }; - currentDate = currentDate.plus({ days: 1 }); - } - - // Log expected dates - console.log("[KLAVIYO STATS] Expected dates:", { - timeRange, - dates: Object.keys(expectedDates) - }); - - // Merge existing data with expected dates - if (currentData?.revenue?.daily) { - currentData.revenue.daily.forEach(day => { - if (expectedDates[day.date]) { - expectedDates[day.date] = { ...expectedDates[day.date], ...day }; - } - }); - } - - // Convert back to array and sort by date - const sortedDaily = Object.values(expectedDates).sort((a, b) => - DateTime.fromISO(a.date).toMillis() - DateTime.fromISO(b.date).toMillis() - ); - - // Log the processed daily data - console.log("[KLAVIYO STATS] Processed daily data:", { - timeRange, - daily: sortedDaily.map(day => ({ - date: day.date, - value: day.value, - orders: day.orders - })) - }); - - // Update the daily data with the complete set - currentData.revenue = { - ...currentData.revenue, - daily: sortedDaily - }; - - // Recalculate totals based on daily data - const totals = sortedDaily.reduce((acc, day) => ({ - revenue: acc.revenue + (parseFloat(day.value) || 0), - orders: acc.orders + (parseInt(day.orders) || 0), - items: acc.items + (parseInt(day.items) || 0), - pre_orders: acc.pre_orders + (parseInt(day.pre_orders) || 0), - local_pickup: acc.local_pickup + (parseInt(day.local_pickup) || 0), - on_hold: acc.on_hold + (parseInt(day.status?.on_hold) || 0), - refunds: { - total: acc.refunds.total + (parseFloat(day.refunds?.total) || 0), - count: acc.refunds.count + (parseInt(day.refunds?.count) || 0) - }, - cancellations: { - total: acc.cancellations.total + (parseFloat(day.cancellations?.total) || 0), - count: acc.cancellations.count + (parseInt(day.cancellations?.count) || 0) - } - }), { - revenue: 0, - orders: 0, - items: 0, - pre_orders: 0, - local_pickup: 0, - on_hold: 0, - refunds: { total: 0, count: 0 }, - cancellations: { total: 0, count: 0 } - }); - - // Update the metrics with recalculated totals - currentData.revenue.total = totals.revenue; - currentData.orders = { - ...currentData.orders, - total: totals.orders, - items_total: totals.items, - pre_orders: totals.pre_orders, - local_pickup: totals.local_pickup, - status: { ...currentData.orders?.status, on_hold: totals.on_hold } - }; - currentData.refunds = totals.refunds; - currentData.cancellations = totals.cancellations; - - setMetrics(currentData); - setLastUpdate(DateTime.now()); - setTimeRangeChanging(false); - - // Fetch previous period - const prevPeriod = getPreviousPeriod(timeRange); - setIsLoadingPrevious(true); - - const previousResponse = await fetch(`/api/klaviyo/metrics/${prevPeriod}`); - const previousData = await previousResponse.json(); - - if (previousResponse.ok) { - setPreviousMetrics(previousData); - } - setIsLoadingPrevious(false); - } catch (error) { - console.error("[KLAVIYO STATS] Error fetching metrics:", error); - setError(error.message); - setTimeRangeChanging(false); - } - }, [timeRange]); - - 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 && ( - setSelectedMetric(null)} - > - - - - {selectedMetric - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ")}{" "} - Details - - - {renderMetricDetails()} - - - )} - - ) : null} -
-
- ); -}; - -export default KlaviyoStats; \ No newline at end of file