From 8ad566c7f42b7e5b59e531f93df2410287da305e Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 6 Jan 2025 09:15:30 -0500 Subject: [PATCH] Attempt to add saleschart projections --- .../components/dashboard/MiniSalesChart.jsx | 70 +++++++++---- .../src/components/dashboard/SalesChart.jsx | 98 ++++++++++++++----- .../src/components/dashboard/StatCards.jsx | 92 +++++++++++++++++ 3 files changed, 215 insertions(+), 45 deletions(-) diff --git a/dashboard/src/components/dashboard/MiniSalesChart.jsx b/dashboard/src/components/dashboard/MiniSalesChart.jsx index 24d5ab3..193f1ec 100644 --- a/dashboard/src/components/dashboard/MiniSalesChart.jsx +++ b/dashboard/src/components/dashboard/MiniSalesChart.jsx @@ -165,11 +165,26 @@ const MiniSalesChart = ({ className = "" }) => { totalOrders: 0, prevRevenue: 0, prevOrders: 0, - growth: { - revenue: 0, - orders: 0 - } + periodProgress: 100 }); + const [projection, setProjection] = useState(null); + const [projectionLoading, setProjectionLoading] = useState(false); + + const fetchProjection = useCallback(async () => { + if (summaryStats.periodProgress >= 100) return; + + try { + setProjectionLoading(true); + const response = await axios.get("/api/klaviyo/events/projection", { + params: { timeRange: "last30days" } + }); + setProjection(response.data); + } catch (error) { + console.error("Error loading projection:", error); + } finally { + setProjectionLoading(false); + } + }, [summaryStats.periodProgress]); const fetchData = useCallback(async () => { try { @@ -200,33 +215,30 @@ const MiniSalesChart = ({ className = "" }) => { totalOrders: acc.totalOrders + (Number(day.orders) || 0), prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0), prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0), + periodProgress: day.periodProgress || 100, }), { totalRevenue: 0, totalOrders: 0, prevRevenue: 0, - prevOrders: 0 + prevOrders: 0, + periodProgress: 100 }); - // Calculate growth percentages - const growth = { - revenue: totals.prevRevenue > 0 - ? ((totals.totalRevenue - totals.prevRevenue) / totals.prevRevenue) * 100 - : 0, - orders: totals.prevOrders > 0 - ? ((totals.totalOrders - totals.prevOrders) / totals.prevOrders) * 100 - : 0 - }; - setData(processedData); - setSummaryStats({ ...totals, growth }); + setSummaryStats(totals); setError(null); + + // Fetch projection if needed + if (totals.periodProgress < 100) { + fetchProjection(); + } } catch (error) { console.error("Error fetching data:", error); setError(error.message); } finally { setLoading(false); } - }, []); + }, [fetchProjection]); useEffect(() => { fetchData(); @@ -294,8 +306,16 @@ const MiniSalesChart = ({ className = "" }) => { title="30 Days Revenue" value={formatCurrency(summaryStats.totalRevenue, false)} previousValue={formatCurrency(summaryStats.prevRevenue, false)} - trend={summaryStats.growth.revenue >= 0 ? "up" : "down"} - trendValue={`${Math.abs(Math.round(summaryStats.growth.revenue))}%`} + trend={ + summaryStats.periodProgress < 100 + ? ((projection?.projectedRevenue || summaryStats.totalRevenue) >= summaryStats.prevRevenue ? "up" : "down") + : (summaryStats.totalRevenue >= summaryStats.prevRevenue ? "up" : "down") + } + trendValue={ + summaryStats.periodProgress < 100 + ? `${Math.abs(Math.round(((projection?.projectedRevenue || summaryStats.totalRevenue) - summaryStats.prevRevenue) / summaryStats.prevRevenue * 100))}%` + : `${Math.abs(Math.round(((summaryStats.totalRevenue - summaryStats.prevRevenue) / summaryStats.prevRevenue) * 100))}%` + } colorClass="text-emerald-300" titleClass="text-emerald-300 font-bold text-md" descriptionClass="text-emerald-300 text-md font-semibold pb-1" @@ -309,8 +329,16 @@ const MiniSalesChart = ({ className = "" }) => { title="30 Days Orders" value={summaryStats.totalOrders.toLocaleString()} previousValue={summaryStats.prevOrders.toLocaleString()} - trend={summaryStats.growth.orders >= 0 ? "up" : "down"} - trendValue={`${Math.abs(Math.round(summaryStats.growth.orders))}%`} + trend={ + summaryStats.periodProgress < 100 + ? ((Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress))) >= summaryStats.prevOrders ? "up" : "down") + : (summaryStats.totalOrders >= summaryStats.prevOrders ? "up" : "down") + } + trendValue={ + summaryStats.periodProgress < 100 + ? `${Math.abs(Math.round(((Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress))) - summaryStats.prevOrders) / summaryStats.prevOrders * 100))}%` + : `${Math.abs(Math.round(((summaryStats.totalOrders - summaryStats.prevOrders) / summaryStats.prevOrders) * 100))}%` + } colorClass="text-blue-300" titleClass="text-blue-300 font-bold text-md" descriptionClass="text-blue-300 text-md font-semibold pb-1" diff --git a/dashboard/src/components/dashboard/SalesChart.jsx b/dashboard/src/components/dashboard/SalesChart.jsx index 98bbedb..22f456f 100644 --- a/dashboard/src/components/dashboard/SalesChart.jsx +++ b/dashboard/src/components/dashboard/SalesChart.jsx @@ -342,16 +342,8 @@ const calculateSummaryStats = (data = []) => { return best; }, null); - // Calculate growth percentages - const growth = { - revenue: prevRevenue - ? ((totalRevenue - prevRevenue) / prevRevenue) * 100 - : 0, - orders: prevOrders ? ((totalOrders - prevOrders) / prevOrders) * 100 : 0, - avgOrderValue: prevAvgOrderValue - ? ((avgOrderValue - prevAvgOrderValue) / prevAvgOrderValue) * 100 - : 0, - }; + // Get period progress from the last day + const periodProgress = data[data.length - 1]?.periodProgress || 100; return { totalRevenue, @@ -361,7 +353,7 @@ const calculateSummaryStats = (data = []) => { prevRevenue, prevOrders, prevAvgOrderValue, - growth, + periodProgress, movingAverages: { revenue: data[data.length - 1]?.movingAverage || 0, orders: data[data.length - 1]?.orderMovingAverage || 0, @@ -371,7 +363,7 @@ const calculateSummaryStats = (data = []) => { }; // Add memoized SummaryStats component -const SummaryStats = memo(({ stats = {} }) => { +const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => { const { totalRevenue = 0, totalOrders = 0, @@ -380,17 +372,39 @@ const SummaryStats = memo(({ stats = {} }) => { prevRevenue = 0, prevOrders = 0, prevAvgOrderValue = 0, - growth = { revenue: 0, orders: 0, avgOrderValue: 0 }, + periodProgress = 100 } = stats; + // Calculate projected values when period is incomplete + const currentRevenue = periodProgress < 100 ? (projection?.projectedRevenue || totalRevenue) : totalRevenue; + const revenueTrend = currentRevenue >= prevRevenue ? "up" : "down"; + const revenueDiff = Math.abs(currentRevenue - prevRevenue); + const revenuePercentage = (revenueDiff / prevRevenue) * 100; + + // Calculate order trends + const currentOrders = periodProgress < 100 ? (projection?.projectedOrders || totalOrders) : totalOrders; + const ordersTrend = currentOrders >= prevOrders ? "up" : "down"; + const ordersDiff = Math.abs(currentOrders - prevOrders); + const ordersPercentage = (ordersDiff / prevOrders) * 100; + + // Calculate AOV trends + const currentAOV = currentOrders ? currentRevenue / currentOrders : avgOrderValue; + const aovTrend = currentAOV >= prevAvgOrderValue ? "up" : "down"; + const aovDiff = Math.abs(currentAOV - prevAvgOrderValue); + const aovPercentage = (aovDiff / prevAvgOrderValue) * 100; + return (
= 0 ? "up" : "down"} - trendValue={formatPercentage(growth.revenue)} + description={ + periodProgress < 100 + ? `Projected: ${formatCurrency(projection?.projectedRevenue || totalRevenue, false)}` + : `Previous: ${formatCurrency(prevRevenue, false)}` + } + trend={projectionLoading && periodProgress < 100 ? undefined : revenueTrend} + trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(revenuePercentage)} info="Total revenue for the selected period" colorClass="text-green-600 dark:text-green-400" /> @@ -398,9 +412,13 @@ const SummaryStats = memo(({ stats = {} }) => { = 0 ? "up" : "down"} - trendValue={formatPercentage(growth.orders)} + description={ + periodProgress < 100 + ? `Projected: ${(projection?.projectedOrders || totalOrders).toLocaleString()}` + : `Previous: ${prevOrders.toLocaleString()}` + } + trend={projectionLoading && periodProgress < 100 ? undefined : ordersTrend} + trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(ordersPercentage)} info="Total number of orders for the selected period" colorClass="text-blue-600 dark:text-blue-400" /> @@ -408,9 +426,13 @@ const SummaryStats = memo(({ stats = {} }) => { = 0 ? "up" : "down"} - trendValue={formatPercentage(growth.avgOrderValue)} + description={ + periodProgress < 100 + ? `Projected: ${formatCurrency(currentAOV)}` + : `Previous: ${formatCurrency(prevAvgOrderValue)}` + } + trend={projectionLoading && periodProgress < 100 ? undefined : aovTrend} + trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(aovPercentage)} info="Average value per order for the selected period" colorClass="text-purple-600 dark:text-purple-400" /> @@ -519,6 +541,25 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { showPrevious: false, }); const [summaryStats, setSummaryStats] = useState({}); + const [projection, setProjection] = useState(null); + const [projectionLoading, setProjectionLoading] = useState(false); + + // Add function to fetch projection + const fetchProjection = useCallback(async (params) => { + if (!params) return; + + try { + setProjectionLoading(true); + const response = await axios.get("/api/klaviyo/events/projection", { + params, + }); + setProjection(response.data); + } catch (error) { + console.error("Error loading projection:", error); + } finally { + setProjectionLoading(false); + } + }, []); // Fetch data function const fetchData = useCallback(async (params) => { @@ -551,13 +592,18 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { setData(processedData); setSummaryStats(stats); setError(null); + + // Fetch projection if needed + if (stats.periodProgress < 100) { + fetchProjection(params); + } } catch (error) { console.error("Error fetching data:", error); setError(error.message); } finally { setLoading(false); } - }, []); + }, [fetchProjection]); // Handle time range change const handleTimeRangeChange = useCallback( @@ -832,7 +878,11 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { (loading ? ( ) : ( - + ))} {/* Show metric toggles only if not in error state */} diff --git a/dashboard/src/components/dashboard/StatCards.jsx b/dashboard/src/components/dashboard/StatCards.jsx index 5db3bea..f73207c 100644 --- a/dashboard/src/components/dashboard/StatCards.jsx +++ b/dashboard/src/components/dashboard/StatCards.jsx @@ -1256,6 +1256,98 @@ const SkeletonTable = ({ rows = 8 }) => (
); +const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => { + const { + totalRevenue = 0, + totalOrders = 0, + avgOrderValue = 0, + bestDay = null, + prevRevenue = 0, + prevOrders = 0, + prevAvgOrderValue = 0, + periodProgress = 100 + } = stats; + + // Calculate projected values when period is incomplete + const currentRevenue = periodProgress < 100 ? (projection?.projectedRevenue || totalRevenue) : totalRevenue; + const revenueTrend = currentRevenue >= prevRevenue ? "up" : "down"; + const revenueDiff = Math.abs(currentRevenue - prevRevenue); + const revenuePercentage = (revenueDiff / prevRevenue) * 100; + + // Calculate order trends + const currentOrders = periodProgress < 100 ? Math.round(totalOrders * (100 / periodProgress)) : totalOrders; + const ordersTrend = currentOrders >= prevOrders ? "up" : "down"; + const ordersDiff = Math.abs(currentOrders - prevOrders); + const ordersPercentage = (ordersDiff / prevOrders) * 100; + + // Calculate AOV trends + const currentAOV = currentOrders ? currentRevenue / currentOrders : avgOrderValue; + const aovTrend = currentAOV >= prevAvgOrderValue ? "up" : "down"; + const aovDiff = Math.abs(currentAOV - prevAvgOrderValue); + const aovPercentage = (aovDiff / prevAvgOrderValue) * 100; + + return ( +
+ + + + + + + +
+ ); +}); + const StatCards = ({ timeRange: initialTimeRange = "today", startDate,