import React, { useState, useEffect, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableHeader, TableHead, TableRow, TableBody, TableCell, } from "@/components/ui/table"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, ReferenceLine, } from "recharts"; import { DateTime } from "luxon"; import { TimeRangeSelect } from "@/components/dashboard/TimeRangeSelect"; import { Info, TrendingUp, TrendingDown } from "lucide-react"; const formatCurrency = (value) => { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); }; const formatPercent = (value) => { return new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: 1, maximumFractionDigits: 1, }).format(value / 100); }; const StatCard = ({ title, value, subtitle, previousValue, formatter = (v) => v.toLocaleString(), info, }) => { const percentChange = previousValue ? ((value - previousValue) / previousValue) * 100 : 0; return (
{title} {info && ( )}
{formatter(value)}
{subtitle && (
{subtitle}
)} {previousValue && (
0 ? "text-green-600" : "text-red-600"}`} > {percentChange > 0 ? ( ) : ( )} {Math.abs(percentChange).toFixed(1)}% vs prev
)}
); }; const formatChartDate = (value) => { if (!value) return ''; try { return DateTime.fromISO(value).setZone('America/New_York').toFormat('LLL d'); } catch (error) { console.error("[KLAVIYO SALES] Date formatting error:", error); return value; } }; const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return (

{formatChartDate(label)}

{payload.map((entry, index) => (

{entry.name}:{" "} {entry.dataKey.toLowerCase().includes("revenue") || entry.dataKey === "movingAverage" ? formatCurrency(entry.value) : entry.value.toLocaleString()}

))}
); } return null; }; const KlaviyoSalesChart = ({ className }) => { const [timeRange, setTimeRange] = useState("last7days"); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [showDailyTable, setShowDailyTable] = useState(false); const [visibleLines, setVisibleLines] = useState({ revenue: true, orders: true, movingAverage: true, previousRevenue: false, previousOrders: false, }); const calculate7DayAverage = useCallback((data) => { return data.map((day, index, array) => { const startIndex = Math.max(0, index - 6); const window = array.slice(startIndex, index + 1); const sum = window.reduce((acc, curr) => acc + (curr.revenue || 0), 0); const average = window.length > 0 ? sum / window.length : 0; return { ...day, movingAverage: Number(average.toFixed(2)) }; }); }, []); const validateMetricsData = (data) => { if (!data) { console.warn("[KLAVIYO SALES] No data received"); return false; } // Log the actual structure we're receiving console.log("[KLAVIYO SALES] Validating data structure:", { keys: Object.keys(data), revenueKeys: Object.keys(data.revenue || {}), ordersKeys: Object.keys(data.orders || {}), sampleDaily: data.revenue?.daily?.[0] }); // Check if we have the minimum required data const hasMinimumData = data.revenue?.daily && Array.isArray(data.revenue.daily) && data.revenue.daily.length > 0; if (!hasMinimumData) { console.warn("[KLAVIYO SALES] Missing minimum required data"); return false; } return true; }; 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 fetchData = useCallback(async () => { try { setIsLoading(true); const previousTimeRange = getPreviousPeriod(timeRange); console.log("[KLAVIYO SALES] Fetching data for:", { current: timeRange, previous: previousTimeRange }); // Fetch both current and previous period data const [currentResponse, previousResponse] = await Promise.all([ fetch(`/api/klaviyo/sales/${timeRange}`), fetch(`/api/klaviyo/sales/${previousTimeRange}`) ]); if (!currentResponse.ok || !previousResponse.ok) { throw new Error(`Failed to fetch data: ${currentResponse.status}, ${previousResponse.status}`); } const [currentData, previousData] = await Promise.all([ currentResponse.json(), previousResponse.json() ]); // Log the raw data received from the API console.log("[KLAVIYO SALES] Raw data received:", { current: currentData, previous: previousData }); if (!validateMetricsData(currentData)) { throw new Error('Invalid metrics data structure received'); } // Process the daily data const processedDaily = currentData.revenue.daily.map((day, index) => { const previousDay = previousData?.revenue?.daily?.[index] || {}; const hourlyTotal = (day.hourly_orders || []).reduce((sum, count) => sum + count, 0); const revenue = day.value || 0; const orders = day.orders || 0; return { date: day.date, revenue, orders, items: day.items || 0, avgOrderValue: orders > 0 ? revenue / orders : 0, preOrders: day.pre_orders || 0, refunds: day.refunds?.total || 0, cancellations: day.cancellations?.total || 0, previousRevenue: previousDay.value || 0, previousOrders: previousDay.orders || 0, hourly_orders: day.hourly_orders || Array(24).fill(0), hourly_total: hourlyTotal }; }); // Calculate 7-day moving average const withMovingAverage = calculate7DayAverage(processedDaily); // Prepare the complete data structure const processedData = { daily: withMovingAverage, totals: { revenue: currentData.revenue.total || 0, orders: currentData.orders.total || 0, items: currentData.orders.items_total || 0, avgOrderValue: currentData.orders.total > 0 ? currentData.revenue.total / currentData.orders.total : 0, previousRevenue: previousData?.revenue?.total || 0, previousOrders: previousData?.orders?.total || 0, refunds: currentData.refunds || { count: 0, total: 0 }, cancellations: currentData.cancellations || { count: 0, total: 0 }, preOrders: currentData.orders?.pre_orders || 0, localPickup: currentData.orders?.local_pickup || 0, revenue_best_day: currentData.revenue.best_day || { date: null, value: 0, orders: 0 } }, hourly_distribution: currentData.hourly_distribution || Array(24).fill(0), metadata: { brands: Object.keys(currentData.orders?.brands || {}).length, categories: Object.keys(currentData.orders?.categories || {}).length, shipping_states: Object.keys(currentData.orders?.shipping_states || {}).length } }; // Log the processed data before setting the state console.log("[KLAVIYO SALES] Processed data:", { dailyCount: processedData.daily.length, totalRevenue: processedData.totals.revenue, totalOrders: processedData.totals.orders }); setData(processedData); setError(null); } catch (err) { console.error('[KLAVIYO SALES] Error:', err); setError(err.message); } finally { setIsLoading(false); } }, [timeRange, calculate7DayAverage]); useEffect(() => { let isSubscribed = true; const loadData = async () => { await fetchData(); if (!isSubscribed) return; }; loadData(); // Set up refresh interval only for active time ranges const shouldAutoRefresh = ['today', 'last7days'].includes(timeRange); let interval; if (shouldAutoRefresh) { interval = setInterval(() => { if (isSubscribed) loadData(); }, 5 * 60 * 1000); // 5 minutes } return () => { isSubscribed = false; if (interval) clearInterval(interval); }; }, [timeRange, fetchData]); if (error) { return (
Error loading sales data: {error}
); } const LoadingState = () => (
{[...Array(4)].map((_, i) => ( ))}
); if (isLoading || !data) { return (
Sales & Orders

{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` )}

); } const averageRevenue = data.daily.reduce((sum, day) => sum + day.revenue, 0) / data.daily.length; return (
Sales & Orders

{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` )}

{Object.entries({ revenue: "Revenue", orders: "Orders", movingAverage: "7-Day Average", previousRevenue: "Previous Revenue", previousOrders: "Previous Orders", }).map(([key, label]) => ( ))}
formatCurrency(value)} tick={{ fill: "currentColor" }} /> value.toLocaleString()} tick={{ fill: "currentColor" }} /> } /> {visibleLines.revenue && ( )} {visibleLines.previousRevenue && ( )} {visibleLines.orders && ( )} {visibleLines.previousOrders && ( )} {visibleLines.movingAverage && ( )}
{showDailyTable && (
Date Orders Items Revenue Avg Order Pre-Orders Refunds Cancellations {data.daily.map((day) => ( {day.date} {day.orders.toLocaleString()} {day.items.toLocaleString()} {formatCurrency(day.revenue)} {formatCurrency(day.avgOrderValue)} {day.preOrders.toLocaleString()} {day.refunds > 0 ? `-${formatCurrency(day.refunds)}` : "-"} {day.cancelations > 0 ? `-${formatCurrency(day.cancelations)}` : "-"} ))}
)}
); }; export default KlaviyoSalesChart;