Attempt to add saleschart projections

This commit is contained in:
2025-01-06 09:15:30 -05:00
parent 304d09e3c4
commit 8ad566c7f4
3 changed files with 215 additions and 45 deletions

View File

@@ -165,11 +165,26 @@ const MiniSalesChart = ({ className = "" }) => {
totalOrders: 0, totalOrders: 0,
prevRevenue: 0, prevRevenue: 0,
prevOrders: 0, prevOrders: 0,
growth: { periodProgress: 100
revenue: 0,
orders: 0
}
}); });
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 () => { const fetchData = useCallback(async () => {
try { try {
@@ -200,33 +215,30 @@ const MiniSalesChart = ({ className = "" }) => {
totalOrders: acc.totalOrders + (Number(day.orders) || 0), totalOrders: acc.totalOrders + (Number(day.orders) || 0),
prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0), prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0),
prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0), prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0),
periodProgress: day.periodProgress || 100,
}), { }), {
totalRevenue: 0, totalRevenue: 0,
totalOrders: 0, totalOrders: 0,
prevRevenue: 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); setData(processedData);
setSummaryStats({ ...totals, growth }); setSummaryStats(totals);
setError(null); setError(null);
// Fetch projection if needed
if (totals.periodProgress < 100) {
fetchProjection();
}
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setError(error.message); setError(error.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [fetchProjection]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -294,8 +306,16 @@ const MiniSalesChart = ({ className = "" }) => {
title="30 Days Revenue" title="30 Days Revenue"
value={formatCurrency(summaryStats.totalRevenue, false)} value={formatCurrency(summaryStats.totalRevenue, false)}
previousValue={formatCurrency(summaryStats.prevRevenue, false)} previousValue={formatCurrency(summaryStats.prevRevenue, false)}
trend={summaryStats.growth.revenue >= 0 ? "up" : "down"} trend={
trendValue={`${Math.abs(Math.round(summaryStats.growth.revenue))}%`} 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" colorClass="text-emerald-300"
titleClass="text-emerald-300 font-bold text-md" titleClass="text-emerald-300 font-bold text-md"
descriptionClass="text-emerald-300 text-md font-semibold pb-1" descriptionClass="text-emerald-300 text-md font-semibold pb-1"
@@ -309,8 +329,16 @@ const MiniSalesChart = ({ className = "" }) => {
title="30 Days Orders" title="30 Days Orders"
value={summaryStats.totalOrders.toLocaleString()} value={summaryStats.totalOrders.toLocaleString()}
previousValue={summaryStats.prevOrders.toLocaleString()} previousValue={summaryStats.prevOrders.toLocaleString()}
trend={summaryStats.growth.orders >= 0 ? "up" : "down"} trend={
trendValue={`${Math.abs(Math.round(summaryStats.growth.orders))}%`} 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" colorClass="text-blue-300"
titleClass="text-blue-300 font-bold text-md" titleClass="text-blue-300 font-bold text-md"
descriptionClass="text-blue-300 text-md font-semibold pb-1" descriptionClass="text-blue-300 text-md font-semibold pb-1"

View File

@@ -342,16 +342,8 @@ const calculateSummaryStats = (data = []) => {
return best; return best;
}, null); }, null);
// Calculate growth percentages // Get period progress from the last day
const growth = { const periodProgress = data[data.length - 1]?.periodProgress || 100;
revenue: prevRevenue
? ((totalRevenue - prevRevenue) / prevRevenue) * 100
: 0,
orders: prevOrders ? ((totalOrders - prevOrders) / prevOrders) * 100 : 0,
avgOrderValue: prevAvgOrderValue
? ((avgOrderValue - prevAvgOrderValue) / prevAvgOrderValue) * 100
: 0,
};
return { return {
totalRevenue, totalRevenue,
@@ -361,7 +353,7 @@ const calculateSummaryStats = (data = []) => {
prevRevenue, prevRevenue,
prevOrders, prevOrders,
prevAvgOrderValue, prevAvgOrderValue,
growth, periodProgress,
movingAverages: { movingAverages: {
revenue: data[data.length - 1]?.movingAverage || 0, revenue: data[data.length - 1]?.movingAverage || 0,
orders: data[data.length - 1]?.orderMovingAverage || 0, orders: data[data.length - 1]?.orderMovingAverage || 0,
@@ -371,7 +363,7 @@ const calculateSummaryStats = (data = []) => {
}; };
// Add memoized SummaryStats component // Add memoized SummaryStats component
const SummaryStats = memo(({ stats = {} }) => { const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => {
const { const {
totalRevenue = 0, totalRevenue = 0,
totalOrders = 0, totalOrders = 0,
@@ -380,17 +372,39 @@ const SummaryStats = memo(({ stats = {} }) => {
prevRevenue = 0, prevRevenue = 0,
prevOrders = 0, prevOrders = 0,
prevAvgOrderValue = 0, prevAvgOrderValue = 0,
growth = { revenue: 0, orders: 0, avgOrderValue: 0 }, periodProgress = 100
} = stats; } = 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 ( return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
<StatCard <StatCard
title="Total Revenue" title="Total Revenue"
value={formatCurrency(totalRevenue, false)} value={formatCurrency(totalRevenue, false)}
description={`Previous: ${formatCurrency(prevRevenue, false)}`} description={
trend={growth.revenue >= 0 ? "up" : "down"} periodProgress < 100
trendValue={formatPercentage(growth.revenue)} ? `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" info="Total revenue for the selected period"
colorClass="text-green-600 dark:text-green-400" colorClass="text-green-600 dark:text-green-400"
/> />
@@ -398,9 +412,13 @@ const SummaryStats = memo(({ stats = {} }) => {
<StatCard <StatCard
title="Total Orders" title="Total Orders"
value={totalOrders.toLocaleString()} value={totalOrders.toLocaleString()}
description={`Previous: ${prevOrders.toLocaleString()} orders`} description={
trend={growth.orders >= 0 ? "up" : "down"} periodProgress < 100
trendValue={formatPercentage(growth.orders)} ? `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" info="Total number of orders for the selected period"
colorClass="text-blue-600 dark:text-blue-400" colorClass="text-blue-600 dark:text-blue-400"
/> />
@@ -408,9 +426,13 @@ const SummaryStats = memo(({ stats = {} }) => {
<StatCard <StatCard
title="AOV" title="AOV"
value={formatCurrency(avgOrderValue)} value={formatCurrency(avgOrderValue)}
description={`Previous: ${formatCurrency(prevAvgOrderValue)}`} description={
trend={growth.avgOrderValue >= 0 ? "up" : "down"} periodProgress < 100
trendValue={formatPercentage(growth.avgOrderValue)} ? `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" info="Average value per order for the selected period"
colorClass="text-purple-600 dark:text-purple-400" colorClass="text-purple-600 dark:text-purple-400"
/> />
@@ -519,6 +541,25 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
showPrevious: false, showPrevious: false,
}); });
const [summaryStats, setSummaryStats] = useState({}); 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 // Fetch data function
const fetchData = useCallback(async (params) => { const fetchData = useCallback(async (params) => {
@@ -551,13 +592,18 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
setData(processedData); setData(processedData);
setSummaryStats(stats); setSummaryStats(stats);
setError(null); setError(null);
// Fetch projection if needed
if (stats.periodProgress < 100) {
fetchProjection(params);
}
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setError(error.message); setError(error.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [fetchProjection]);
// Handle time range change // Handle time range change
const handleTimeRangeChange = useCallback( const handleTimeRangeChange = useCallback(
@@ -832,7 +878,11 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
(loading ? ( (loading ? (
<SkeletonStats /> <SkeletonStats />
) : ( ) : (
<SummaryStats stats={summaryStats} /> <SummaryStats
stats={summaryStats}
projection={projection}
projectionLoading={projectionLoading}
/>
))} ))}
{/* Show metric toggles only if not in error state */} {/* Show metric toggles only if not in error state */}

View File

@@ -1256,6 +1256,98 @@ const SkeletonTable = ({ rows = 8 }) => (
</div> </div>
); );
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 (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
<StatCard
title="Total Revenue"
value={formatCurrency(totalRevenue, false)}
description={
periodProgress < 100
? `Projected: ${formatCurrency(currentRevenue, 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"
/>
<StatCard
title="Total Orders"
value={totalOrders.toLocaleString()}
description={
periodProgress < 100
? `Projected: ${currentOrders.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"
/>
<StatCard
title="AOV"
value={formatCurrency(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"
/>
<StatCard
title="Best Day"
value={formatCurrency(bestDay?.revenue || 0, false)}
description={
bestDay?.timestamp
? `${new Date(bestDay.timestamp).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})} - ${bestDay.orders} orders`
: "No data"
}
info="Day with highest revenue in the selected period"
colorClass="text-orange-600 dark:text-orange-400"
/>
</div>
);
});
const StatCards = ({ const StatCards = ({
timeRange: initialTimeRange = "today", timeRange: initialTimeRange = "today",
startDate, startDate,