fix: resolve revenue trend chart and top profitable products issues

- Fix revenue trend chart to display month-year format for multi-year data
- Implement proper date sorting (newest first) for monthly revenue data
- Enhance chart with gradients, hover tooltips, and better visual design
- Fix top profitable products to use actual sales data instead of catalog data
- Calculate profits based on real order performance within date range
- Add comprehensive debugging and empty state handling
- Change default date range to 'All Time' for better initial experience
- Improve month-year parsing and data validation
This commit is contained in:
dlawler489 2026-05-06 06:34:25 +10:00
parent 701c805d0c
commit 04069ef954

View file

@ -11,9 +11,20 @@ const Analytics = () => {
const { customers } = useSelector((state: RootState) => state.customers);
const { expenses } = useSelector((state: RootState) => state.expenses);
// Debug: Log the raw data
console.log('=== RAW DATA DEBUG ===');
console.log('Raw orders:', orders?.length || 0);
console.log('Raw products:', products?.length || 0);
console.log('Raw customers:', customers?.length || 0);
console.log('Raw expenses:', expenses?.length || 0);
if (orders && orders.length > 0) {
console.log('Sample order:', orders[0]);
}
console.log('=====================');
const [dateRange, setDateRange] = useState(() => {
// Default to current year
return new Date().getFullYear().toString();
// Default to "All Time" to show all data initially
return 'all';
});
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
@ -307,22 +318,48 @@ const Analytics = () => {
// Create monthly revenue data from actual orders
const monthlyData = useMemo(() => {
if (!filteredOrders.length) return [];
if (!filteredOrders.length) {
console.log('No filtered orders found for monthly data');
return [];
}
const monthlyMap = new Map();
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Initialize all months with 0
months.forEach(month => {
monthlyMap.set(month, { month, revenue: 0, expenses: 0, profit: 0 });
console.log('Building monthly data from', filteredOrders.length, 'orders');
// First pass: identify all months that have orders
const monthsWithData = new Set();
filteredOrders.forEach(order => {
if (order.dateOrdered) {
const date = new Date(order.dateOrdered);
const year = date.getFullYear();
const monthIndex = date.getMonth();
const monthKey = `${months[monthIndex]} ${year}`;
monthsWithData.add(monthKey);
}
});
// Initialize months that have data
Array.from(monthsWithData).forEach(monthKey => {
monthlyMap.set(monthKey, {
month: monthKey,
revenue: 0,
expenses: 0,
profit: 0
});
});
console.log('Months with data:', Array.from(monthsWithData));
// Aggregate orders by month
filteredOrders.forEach(order => {
if (order.dateOrdered) {
const date = new Date(order.dateOrdered);
const monthName = months[date.getMonth()];
const current = monthlyMap.get(monthName);
const year = date.getFullYear();
const monthIndex = date.getMonth();
const monthKey = `${months[monthIndex]} ${year}`;
const current = monthlyMap.get(monthKey);
if (current) {
current.revenue += order.total || 0;
}
@ -333,8 +370,10 @@ const Analytics = () => {
filteredExpenses.forEach(expense => {
if (expense.date) {
const date = new Date(expense.date);
const monthName = months[date.getMonth()];
const current = monthlyMap.get(monthName);
const year = date.getFullYear();
const monthIndex = date.getMonth();
const monthKey = `${months[monthIndex]} ${year}`;
const current = monthlyMap.get(monthKey);
if (current) {
// For monthly chart, exclude transaction fees to match profit calculation
@ -356,7 +395,21 @@ const Analytics = () => {
data.profit = data.revenue - data.expenses;
});
return Array.from(monthlyMap.values()).filter(data => data.revenue > 0 || data.expenses > 0);
// Sort by date (newest first) and return
const result = Array.from(monthlyMap.values())
.sort((a, b) => {
// Parse month and year from "Jan 2026" format
const parseMonthYear = (monthStr: string) => {
const [month, year] = monthStr.split(' ');
const monthIndex = months.indexOf(month);
return new Date(parseInt(year), monthIndex);
};
return parseMonthYear(b.month).getTime() - parseMonthYear(a.month).getTime();
});
console.log('Final monthly data:', result);
return result;
}, [filteredOrders, filteredExpenses]);
// Create expense categories data from actual expenses
@ -397,14 +450,46 @@ const Analytics = () => {
return categoryData;
}, [filteredExpenses, totalExpenses, totalRevenue, totalPrintingCosts, netProfit]);
const topProducts = (products || [])
.filter(product => product && typeof product.price === 'number' && typeof product.costOfGoods === 'number')
.sort((a, b) => {
const aTotalCosts = a.costOfGoods + (a.printingCost || 0);
const bTotalCosts = b.costOfGoods + (b.printingCost || 0);
return (b.price - bTotalCosts) - (a.price - aTotalCosts);
})
.slice(0, 5);
// Calculate top profitable products based on actual sales performance
const topProducts = useMemo(() => {
// Create a map to track sales and profits for each product
const productSalesMap = new Map();
filteredOrders.forEach(order => {
if (order.items && Array.isArray(order.items)) {
order.items.forEach(item => {
const key = item.title || 'Unknown Product';
const current = productSalesMap.get(key) || {
title: item.title,
totalRevenue: 0,
totalQuantity: 0,
totalPrintingCost: 0,
totalCostOfGoods: 0,
totalProfit: 0
};
const itemRevenue = (item.price || 0) * (item.quantity || 1);
const itemPrintingCost = (item.printingCost || 0) * (item.quantity || 1);
const itemCostOfGoods = (item.costOfGoods || 0) * (item.quantity || 1);
const itemProfit = itemRevenue - itemPrintingCost - itemCostOfGoods;
current.totalRevenue += itemRevenue;
current.totalQuantity += (item.quantity || 1);
current.totalPrintingCost += itemPrintingCost;
current.totalCostOfGoods += itemCostOfGoods;
current.totalProfit += itemProfit;
productSalesMap.set(key, current);
});
}
});
// Convert to array and sort by total profit
return Array.from(productSalesMap.values())
.filter(product => product.totalRevenue > 0)
.sort((a, b) => b.totalProfit - a.totalProfit)
.slice(0, 5);
}, [filteredOrders]);
const recentOrders = filteredOrders
.filter(order => order && order.dateOrdered)
@ -612,31 +697,66 @@ const Analytics = () => {
{/* Revenue Chart */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Revenue Trend</h3>
<div className="h-64 flex items-end justify-between space-x-2">
{monthlyData.length > 0 ? monthlyData.map((data, index) => {
const maxRevenue = Math.max(...monthlyData.map(d => d.revenue));
const height = maxRevenue > 0 ? (data.revenue / maxRevenue) * 100 : 0;
return (
<div key={index} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-blue-500 rounded-t"
style={{ height: `${height}%`, minHeight: data.revenue > 0 ? '8px' : '0px' }}
title={`${data.month}: $${data.revenue.toFixed(2)}`}
/>
<span className="text-xs text-gray-500 mt-2">{data.month}</span>
<div className="h-64">
{monthlyData.length > 0 ? (
<div className="h-full flex items-end justify-between space-x-2">
{monthlyData.map((data, index) => {
const maxRevenue = Math.max(...monthlyData.map(d => d.revenue));
const height = maxRevenue > 0 ? Math.max((data.revenue / maxRevenue) * 85, 2) : 0;
return (
<div key={`month-${index}`} className="flex-1 flex flex-col items-center group relative">
<div className="relative w-full">
<div
className="w-full bg-gradient-to-t from-blue-600 to-blue-400 rounded-t-sm transition-all duration-200 group-hover:from-blue-700 group-hover:to-blue-500"
style={{
height: `${height}%`,
minHeight: data.revenue > 0 ? '4px' : '2px',
maxHeight: '200px'
}}
/>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10">
<div className="bg-gray-900 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap shadow-lg">
<div className="font-medium">{data.month}</div>
<div>Revenue: ${data.revenue.toFixed(2)}</div>
<div>Profit: ${data.profit.toFixed(2)}</div>
</div>
</div>
</div>
<span className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-center whitespace-nowrap">
{data.month.substring(0, 3)}
</span>
</div>
);
})}
</div>
) : (
<div className="flex items-center justify-center w-full h-full text-gray-500 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200">
<div className="text-center">
<TrendingUp className="w-12 h-12 text-gray-400 mx-auto mb-2" />
<p className="text-sm">No revenue data for selected period</p>
<p className="text-xs text-gray-400 mt-1">Import orders to see revenue trends</p>
</div>
);
}) : (
<div className="flex items-center justify-center w-full h-32 text-gray-500">
No revenue data for selected period
</div>
)}
</div>
<div className="mt-4 flex justify-between text-sm text-gray-600">
<span>Revenue by Month</span>
<div className="mt-4 flex justify-between items-center text-sm text-gray-600">
<div className="flex items-center space-x-4">
<span>Revenue by Month</span>
{monthlyData.length > 0 && (
<span className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded">
{monthlyData.length} months
</span>
)}
</div>
{monthlyData.length > 0 && (
<span>Peak: ${Math.max(...monthlyData.map(d => d.revenue)).toFixed(2)}</span>
<div className="text-right">
<div>Peak: ${Math.max(...monthlyData.map(d => d.revenue)).toFixed(2)}</div>
<div className="text-xs text-gray-500">
Avg: ${(monthlyData.reduce((sum, d) => sum + d.revenue, 0) / monthlyData.length).toFixed(2)}
</div>
</div>
)}
</div>
</div>
@ -682,19 +802,19 @@ const Analytics = () => {
<h3 className="text-lg font-medium text-gray-900 mb-4">Top Profitable Products</h3>
<div className="space-y-3">
{topProducts.length > 0 ? topProducts.map((product, index) => (
<div key={product?._id || index} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0">
<div key={`product-${index}`} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0">
<div className="flex items-center">
<span className="text-sm font-medium text-gray-500 mr-3">#{index + 1}</span>
<div>
<p className="text-sm font-medium text-gray-900 truncate max-w-48">{product?.title || 'Unknown Product'}</p>
<p className="text-xs text-gray-500">SKU: {product?.sku || 'N/A'}</p>
<p className="text-sm font-medium text-gray-900 truncate max-w-48">{product.title || 'Unknown Product'}</p>
<p className="text-xs text-gray-500">Sold: {product.totalQuantity} units</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-green-600">
${((product?.price || 0) - (product?.costOfGoods || 0) - (product?.printingCost || 0)).toFixed(2)}
${product.totalProfit.toFixed(2)}
</p>
<p className="text-xs text-gray-500">profit per item</p>
<p className="text-xs text-gray-500">total profit</p>
</div>
</div>
)) : (