diff --git a/dashboard/src/components/dashboard/SalesChart.jsx b/dashboard/src/components/dashboard/SalesChart.jsx index 9a580d0..a19996e 100644 --- a/dashboard/src/components/dashboard/SalesChart.jsx +++ b/dashboard/src/components/dashboard/SalesChart.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useMemo, useCallback, memo } from 'react'; -import axios from 'axios'; +import React, { useState, useEffect, useMemo, useCallback, memo } from "react"; +import axios from "axios"; import { Card, CardContent, @@ -17,7 +17,15 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; -import { Loader2, TrendingUp, TrendingDown, Info, AlertCircle } from "lucide-react"; +import { + Loader2, + TrendingUp, + TrendingDown, + Info, + AlertCircle, + ArrowUp, + ArrowDown, +} from "lucide-react"; import { LineChart, Line, @@ -28,8 +36,13 @@ import { ResponsiveContainer, Legend, ReferenceLine, -} from 'recharts'; -import { TIME_RANGES, GROUP_BY_OPTIONS, formatDateForInput, parseDateFromInput } from "@/lib/constants"; +} from "recharts"; +import { + TIME_RANGES, + GROUP_BY_OPTIONS, + formatDateForInput, + parseDateFromInput, +} from "@/lib/constants"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, @@ -53,38 +66,38 @@ import { Separator } from "@/components/ui/separator"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; const METRIC_IDS = { - PLACED_ORDER: 'Y8cqcF', - PAYMENT_REFUNDED: 'R7XUYh' + PLACED_ORDER: "Y8cqcF", + PAYMENT_REFUNDED: "R7XUYh", }; // Map current periods to their previous equivalents const PREVIOUS_PERIOD_MAP = { - today: 'yesterday', - thisWeek: 'lastWeek', - thisMonth: 'lastMonth', - last7days: 'previous7days', - last30days: 'previous30days', - last90days: 'previous90days', - yesterday: 'twoDaysAgo' + today: "yesterday", + thisWeek: "lastWeek", + thisMonth: "lastMonth", + last7days: "previous7days", + last30days: "previous30days", + last90days: "previous90days", + yesterday: "twoDaysAgo", }; // Add helper function to calculate previous period dates const calculatePreviousPeriodDates = (timeRange, startDate, endDate) => { - if (timeRange && timeRange !== 'custom') { + if (timeRange && timeRange !== "custom") { return { - timeRange: PREVIOUS_PERIOD_MAP[timeRange] + timeRange: PREVIOUS_PERIOD_MAP[timeRange], }; } else if (startDate && endDate) { const start = new Date(startDate); const end = new Date(endDate); const duration = end.getTime() - start.getTime(); - + const prevEnd = new Date(start.getTime() - 1); const prevStart = new Date(prevEnd.getTime() - duration); - + return { startDate: prevStart.toISOString(), - endDate: prevEnd.toISOString() + endDate: prevEnd.toISOString(), }; } return null; @@ -92,113 +105,134 @@ const calculatePreviousPeriodDates = (timeRange, startDate, endDate) => { // Enhanced helper function for consistent currency formatting with explicit rounding const formatCurrency = (value, useFractionDigits = true) => { - if (typeof value !== 'number') return '$0.00'; + if (typeof value !== "number") return "$0.00"; const roundedValue = parseFloat(value.toFixed(useFractionDigits ? 2 : 0)); - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", minimumFractionDigits: useFractionDigits ? 2 : 0, - maximumFractionDigits: useFractionDigits ? 2 : 0 + maximumFractionDigits: useFractionDigits ? 2 : 0, }).format(roundedValue); }; // Add a helper function for percentage formatting const formatPercentage = (value) => { - if (typeof value !== 'number') return '0%'; - return `${value >= 0 ? '+' : ''}${Math.round(value)}%`; + if (typeof value !== "number") return "0%"; + return `${Math.abs(Math.round(value))}%`; }; // Add color mapping for metrics const METRIC_COLORS = { - revenue: '#8b5cf6', - orders: '#10b981', - avgOrderValue: '#9333ea', - movingAverage: '#f59e0b', - prevRevenue: '#f97316', - prevOrders: '#0ea5e9', - prevAvgOrderValue: '#f59e0b' + revenue: "#8b5cf6", + orders: "#10b981", + avgOrderValue: "#9333ea", + movingAverage: "#f59e0b", + prevRevenue: "#f97316", + prevOrders: "#0ea5e9", + prevAvgOrderValue: "#f59e0b", }; // Memoize the StatCard component -const StatCard = memo(({ - title, - value, - description, - trend, - trendValue, - valuePrefix = "", - valueSuffix = "", - trendPrefix = "", - trendSuffix = "", - className = "", - colorClass = "text-gray-900 dark:text-gray-100", - info -}) => ( - - -
+const StatCard = memo( + ({ + title, + value, + description, + trend, + trendValue, + valuePrefix = "", + valueSuffix = "", + trendPrefix = "", + trendSuffix = "", + className = "", + colorClass = "text-gray-900 dark:text-gray-100", + }) => ( + + {title} - {info && ( - + {trend && ( + + {trend === "up" ? ( + + ) : ( + + )} + {trendPrefix} + {trendValue} + {trendSuffix} + )} -
-
- -
- {valuePrefix}{value}{valueSuffix} -
- {description && ( -
- {description} + + +
+ {valuePrefix} + {value} + {valueSuffix}
- )} - {trend && ( -
- {trend === 'up' ? : } - {trendPrefix}{trendValue}{trendSuffix} -
- )} -
- -)); + {description && ( +
{description}
+ )} + + + ) +); // Add display name for debugging -StatCard.displayName = 'StatCard'; +StatCard.displayName = "StatCard"; const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { const date = new Date(label); - const formattedDate = date.toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric' + const formattedDate = date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", }); // Group metrics by type (current vs previous) - const currentMetrics = payload.filter(p => !p.dataKey.toLowerCase().includes('prev')); - const previousMetrics = payload.filter(p => p.dataKey.toLowerCase().includes('prev')); + const currentMetrics = payload.filter( + (p) => !p.dataKey.toLowerCase().includes("prev") + ); + const previousMetrics = payload.filter((p) => + p.dataKey.toLowerCase().includes("prev") + ); return ( -

{formattedDate}

- +

+ {formattedDate} +

+
{currentMetrics.map((entry, index) => { - const value = entry.dataKey.toLowerCase().includes('revenue') || - entry.dataKey === 'avgOrderValue' || - entry.dataKey === 'movingAverage' || - entry.dataKey === 'aovMovingAverage' - ? formatCurrency(entry.value) - : entry.value.toLocaleString(); + const value = + entry.dataKey.toLowerCase().includes("revenue") || + entry.dataKey === "avgOrderValue" || + entry.dataKey === "movingAverage" || + entry.dataKey === "aovMovingAverage" + ? formatCurrency(entry.value) + : entry.value.toLocaleString(); return ( -
- +
+ {entry.name}: {value} @@ -211,17 +245,29 @@ const CustomTooltip = ({ active, payload, label }) => { <>
-

Previous Period

+

+ Previous Period +

{previousMetrics.map((entry, index) => { - const value = entry.dataKey.toLowerCase().includes('revenue') || - entry.dataKey.includes('avgOrderValue') - ? formatCurrency(entry.value) - : entry.value.toLocaleString(); + const value = + entry.dataKey.toLowerCase().includes("revenue") || + entry.dataKey.includes("avgOrderValue") + ? formatCurrency(entry.value) + : entry.value.toLocaleString(); return ( -
- - {entry.name.replace('Previous ', '')}: +
+ + {entry.name.replace("Previous ", "")}: {value}
@@ -244,14 +290,15 @@ const calculate7DayAverage = (data) => { // Get up to 7 days of data, including current day const startIndex = Math.max(0, index - 6); const window = array.slice(startIndex, index + 1); - + // Calculate averages for all metrics - const validPoints = window.filter(point => - point && - typeof point.revenue === 'number' && - typeof point.orders === 'number' && - !isNaN(point.revenue) && - !isNaN(point.orders) + const validPoints = window.filter( + (point) => + point && + typeof point.revenue === "number" && + typeof point.orders === "number" && + !isNaN(point.revenue) && + !isNaN(point.orders) ); if (validPoints.length === 0) { @@ -259,22 +306,22 @@ const calculate7DayAverage = (data) => { ...day, movingAverage: 0, orderMovingAverage: 0, - aovMovingAverage: 0 + aovMovingAverage: 0, }; } const revenueSum = validPoints.reduce((acc, curr) => acc + curr.revenue, 0); const orderSum = validPoints.reduce((acc, curr) => acc + curr.orders, 0); - + const revenueAvg = revenueSum / validPoints.length; const orderAvg = orderSum / validPoints.length; const aovAvg = orderAvg > 0 ? revenueAvg / orderAvg : 0; - + return { ...day, movingAverage: Number(revenueAvg.toFixed(2)), orderMovingAverage: Number(orderAvg.toFixed(2)), - aovMovingAverage: Number(aovAvg.toFixed(2)) + aovMovingAverage: Number(aovAvg.toFixed(2)), }; }); }; @@ -283,28 +330,44 @@ const processData = (stats = []) => { if (!Array.isArray(stats)) return []; // First, convert the stats array into the base format - const baseData = stats.map(day => ({ + const baseData = stats.map((day) => ({ timestamp: day.date || day.timestamp, revenue: Number(day.revenue || 0), orders: Number(day.orders || 0), - avgOrderValue: Number(day.averageOrderValue || (day.orders > 0 ? day.revenue / day.orders : 0)), + avgOrderValue: Number( + day.averageOrderValue || (day.orders > 0 ? day.revenue / day.orders : 0) + ), prevRevenue: Number(day.prevRevenue || 0), prevOrders: Number(day.prevOrders || 0), - prevAvgOrderValue: Number(day.prevAvgOrderValue || (day.prevOrders > 0 ? day.prevRevenue / day.prevOrders : 0)), + prevAvgOrderValue: Number( + day.prevAvgOrderValue || + (day.prevOrders > 0 ? day.prevRevenue / day.prevOrders : 0) + ), growth: { revenue: 0, orders: 0, - avgOrderValue: 0 - } + avgOrderValue: 0, + }, })); // Calculate growth rates - baseData.forEach(day => { + baseData.forEach((day) => { // Calculate growth day.growth = { - revenue: day.prevRevenue > 0 ? ((day.revenue - day.prevRevenue) / day.prevRevenue) * 100 : 0, - orders: day.prevOrders > 0 ? ((day.orders - day.prevOrders) / day.prevOrders) * 100 : 0, - avgOrderValue: day.prevAvgOrderValue > 0 ? ((day.avgOrderValue - day.prevAvgOrderValue) / day.prevAvgOrderValue) * 100 : 0 + revenue: + day.prevRevenue > 0 + ? ((day.revenue - day.prevRevenue) / day.prevRevenue) * 100 + : 0, + orders: + day.prevOrders > 0 + ? ((day.orders - day.prevOrders) / day.prevOrders) * 100 + : 0, + avgOrderValue: + day.prevAvgOrderValue > 0 + ? ((day.avgOrderValue - day.prevAvgOrderValue) / + day.prevAvgOrderValue) * + 100 + : 0, }; }); @@ -332,7 +395,7 @@ const calculateSummaryStats = (data = []) => { revenue: current.revenue, timestamp: current.timestamp, orders: current.orders, - avgOrderValue: current.avgOrderValue + avgOrderValue: current.avgOrderValue, }; } return best; @@ -340,9 +403,13 @@ const calculateSummaryStats = (data = []) => { // Calculate growth percentages const growth = { - revenue: prevRevenue ? ((totalRevenue - prevRevenue) / prevRevenue) * 100 : 0, + revenue: prevRevenue + ? ((totalRevenue - prevRevenue) / prevRevenue) * 100 + : 0, orders: prevOrders ? ((totalOrders - prevOrders) / prevOrders) * 100 : 0, - avgOrderValue: prevAvgOrderValue ? ((avgOrderValue - prevAvgOrderValue) / prevAvgOrderValue) * 100 : 0 + avgOrderValue: prevAvgOrderValue + ? ((avgOrderValue - prevAvgOrderValue) / prevAvgOrderValue) * 100 + : 0, }; return { @@ -357,8 +424,8 @@ const calculateSummaryStats = (data = []) => { movingAverages: { revenue: data[data.length - 1]?.movingAverage || 0, orders: data[data.length - 1]?.orderMovingAverage || 0, - avgOrderValue: data[data.length - 1]?.aovMovingAverage || 0 - } + avgOrderValue: data[data.length - 1]?.aovMovingAverage || 0, + }, }; }; @@ -372,7 +439,7 @@ const SummaryStats = memo(({ stats = {} }) => { prevRevenue = 0, prevOrders = 0, prevAvgOrderValue = 0, - growth = { revenue: 0, orders: 0, avgOrderValue: 0 } + growth = { revenue: 0, orders: 0, avgOrderValue: 0 }, } = stats; return ( @@ -381,27 +448,27 @@ const SummaryStats = memo(({ stats = {} }) => { title="Total Revenue" value={formatCurrency(totalRevenue, false)} description={`Previous: ${formatCurrency(prevRevenue, false)}`} - trend={growth.revenue >= 0 ? 'up' : 'down'} + trend={growth.revenue >= 0 ? "up" : "down"} trendValue={formatPercentage(growth.revenue)} info="Total revenue for the selected period" colorClass="text-green-600 dark:text-green-400" /> - + = 0 ? 'up' : 'down'} + trend={growth.orders >= 0 ? "up" : "down"} trendValue={formatPercentage(growth.orders)} info="Total number of orders for the selected period" colorClass="text-blue-600 dark:text-blue-400" /> - + = 0 ? 'up' : 'down'} + trend={growth.avgOrderValue >= 0 ? "up" : "down"} trendValue={formatPercentage(growth.avgOrderValue)} info="Average value per order for the selected period" colorClass="text-purple-600 dark:text-purple-400" @@ -410,9 +477,14 @@ const SummaryStats = memo(({ stats = {} }) => { @@ -420,7 +492,7 @@ const SummaryStats = memo(({ stats = {} }) => { ); }); -SummaryStats.displayName = 'SummaryStats'; +SummaryStats.displayName = "SummaryStats"; // Add these skeleton components near the top of the file const SkeletonChart = () => ( @@ -493,11 +565,11 @@ const SkeletonTable = () => ( ); const SalesChart = ({ - timeRange = 'last30days', + timeRange = "last30days", startDate, endDate, title = "Sales Overview", - description = "Track your sales performance over time" + description = "Track your sales performance over time", }) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -509,12 +581,12 @@ const SalesChart = ({ orders: true, avgOrderValue: false, movingAverage: true, - showPrevious: false + showPrevious: false, }); const [summaryStats, setSummaryStats] = useState({}); const [customDateRange, setCustomDateRange] = useState({ - startDate: formatDateForInput(startDate) || '', - endDate: formatDateForInput(endDate) || '' + startDate: formatDateForInput(startDate) || "", + endDate: formatDateForInput(endDate) || "", }); // Fetch data function @@ -524,31 +596,32 @@ const SalesChart = ({ setError(null); // Fetch data - const response = await axios.get('/api/klaviyo/events/stats/details', { + const response = await axios.get("/api/klaviyo/events/stats/details", { params: { ...params, - metric: 'revenue', - daily: true - } + metric: "revenue", + daily: true, + }, }); if (!response.data) { - throw new Error('Invalid response format'); + throw new Error("Invalid response format"); } // Process the data - const currentStats = Array.isArray(response.data) ? response.data : response.data.stats || []; + const currentStats = Array.isArray(response.data) + ? response.data + : response.data.stats || []; // Process the data directly without remapping const processedData = processData(currentStats); const stats = calculateSummaryStats(processedData); - + setData(processedData); setSummaryStats(stats); setError(null); - } catch (error) { - console.error('Error fetching data:', error); + console.error("Error fetching data:", error); setError(error.message); } finally { setLoading(false); @@ -556,42 +629,62 @@ const SalesChart = ({ }, []); // Handle time range change - const handleTimeRangeChange = useCallback((value) => { - setSelectedTimeRange(value); - - const params = value === 'custom' - ? { - startDate: parseDateFromInput(customDateRange.startDate)?.toISOString(), - endDate: parseDateFromInput(customDateRange.endDate)?.toISOString() - } - : { timeRange: value }; + const handleTimeRangeChange = useCallback( + (value) => { + setSelectedTimeRange(value); - fetchData(params); - }, [customDateRange, fetchData]); + const params = + value === "custom" + ? { + startDate: parseDateFromInput( + customDateRange.startDate + )?.toISOString(), + endDate: parseDateFromInput( + customDateRange.endDate + )?.toISOString(), + } + : { timeRange: value }; + + fetchData(params); + }, + [customDateRange, fetchData] + ); // Handle custom date change - const handleCustomDateChange = useCallback((field, value) => { - setCustomDateRange(prev => ({ - ...prev, - [field]: value - })); + const handleCustomDateChange = useCallback( + (field, value) => { + setCustomDateRange((prev) => ({ + ...prev, + [field]: value, + })); - if (selectedTimeRange === 'custom' && customDateRange.startDate && customDateRange.endDate) { - fetchData({ - startDate: parseDateFromInput(customDateRange.startDate)?.toISOString(), - endDate: parseDateFromInput(customDateRange.endDate)?.toISOString() - }); - } - }, [selectedTimeRange, customDateRange, fetchData]); + if ( + selectedTimeRange === "custom" && + customDateRange.startDate && + customDateRange.endDate + ) { + fetchData({ + startDate: parseDateFromInput( + customDateRange.startDate + )?.toISOString(), + endDate: parseDateFromInput(customDateRange.endDate)?.toISOString(), + }); + } + }, + [selectedTimeRange, customDateRange, fetchData] + ); // Initial load effect useEffect(() => { - const params = selectedTimeRange === 'custom' - ? { - startDate: parseDateFromInput(customDateRange.startDate)?.toISOString(), - endDate: parseDateFromInput(customDateRange.endDate)?.toISOString() - } - : { timeRange: selectedTimeRange }; + const params = + selectedTimeRange === "custom" + ? { + startDate: parseDateFromInput( + customDateRange.startDate + )?.toISOString(), + endDate: parseDateFromInput(customDateRange.endDate)?.toISOString(), + } + : { timeRange: selectedTimeRange }; fetchData(params); }, [selectedTimeRange, customDateRange, fetchData]); @@ -600,13 +693,13 @@ const SalesChart = ({ useEffect(() => { let intervalId = null; - if (selectedTimeRange === 'today') { + if (selectedTimeRange === "today") { // Initial fetch - fetchData({ timeRange: 'today' }); - + fetchData({ timeRange: "today" }); + // Set up interval intervalId = setInterval(() => { - fetchData({ timeRange: 'today' }); + fetchData({ timeRange: "today" }); }, 60000); } @@ -618,14 +711,15 @@ const SalesChart = ({ }, [selectedTimeRange, fetchData]); const formatXAxis = (value) => { - if (!value) return ''; + if (!value) return ""; const date = new Date(value); - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + return date.toLocaleDateString([], { month: "short", day: "numeric" }); }; - const averageRevenue = data.length > 0 - ? data.reduce((sum, day) => sum + day.revenue, 0) / data.length - : 0; + const averageRevenue = + data.length > 0 + ? data.reduce((sum, day) => sum + day.revenue, 0) / data.length + : 0; return ( @@ -633,10 +727,15 @@ const SalesChart = ({
- {title} + + {title} +
- @@ -652,7 +751,7 @@ const SalesChart = ({
- {selectedTimeRange === 'custom' && ( + {selectedTimeRange === "custom" && (
@@ -660,7 +759,9 @@ const SalesChart = ({ id="startDate" type="datetime-local" value={customDateRange.startDate} - onChange={(e) => handleCustomDateChange('startDate', e.target.value)} + onChange={(e) => + handleCustomDateChange("startDate", e.target.value) + } className="h-9" />
@@ -670,76 +771,82 @@ const SalesChart = ({ id="endDate" type="datetime-local" value={customDateRange.endDate} - onChange={(e) => handleCustomDateChange('endDate', e.target.value)} + onChange={(e) => + handleCustomDateChange("endDate", e.target.value) + } className="h-9" />
)} - + {/* Show either skeletons or actual stats */} - {loading ? ( - - ) : ( - - )} + {loading ? : } {/* Metric Toggles */}
- value && key !== 'showPrevious') + .filter(([key, value]) => value && key !== "showPrevious") .map(([key]) => key)} onValueChange={(values) => { - setMetrics(prev => ({ + setMetrics((prev) => ({ ...prev, - revenue: values.includes('revenue'), - orders: values.includes('orders'), - avgOrderValue: values.includes('avgOrderValue'), - movingAverage: values.includes('movingAverage'), + revenue: values.includes("revenue"), + orders: values.includes("orders"), + movingAverage: values.includes("movingAverage"), + avgOrderValue: values.includes("avgOrderValue"), })); }} > - Revenue - Orders - - AOV - - 7-Day Avg + + AOV + - +
@@ -768,7 +875,9 @@ const SalesChart = ({
No sales data available
-
Try selecting a different time range
+
+ Try selecting a different time range +
) : ( @@ -779,7 +888,10 @@ const SalesChart = ({ data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }} > - + Avg Order Prev Orders Prev Revenue - Prev Avg Order + + Prev Avg Order + 7-Day Avg @@ -960,4 +1074,4 @@ const SalesChart = ({ ); }; -export default SalesChart; \ No newline at end of file +export default SalesChart;