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,
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"

View File

@@ -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 (
<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={`Previous: ${formatCurrency(prevRevenue, false)}`}
trend={growth.revenue >= 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 = {} }) => {
<StatCard
title="Total Orders"
value={totalOrders.toLocaleString()}
description={`Previous: ${prevOrders.toLocaleString()} orders`}
trend={growth.orders >= 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 = {} }) => {
<StatCard
title="AOV"
value={formatCurrency(avgOrderValue)}
description={`Previous: ${formatCurrency(prevAvgOrderValue)}`}
trend={growth.avgOrderValue >= 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 ? (
<SkeletonStats />
) : (
<SummaryStats stats={summaryStats} />
<SummaryStats
stats={summaryStats}
projection={projection}
projectionLoading={projectionLoading}
/>
))}
{/* Show metric toggles only if not in error state */}

View File

@@ -1256,6 +1256,98 @@ const SkeletonTable = ({ rows = 8 }) => (
</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 = ({
timeRange: initialTimeRange = "today",
startDate,