Attempt to add saleschart projections
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user