enhance: improve revenue chart visualization and top products display

- Add value labels on revenue chart bars with smart formatting (1.2k for >)
- Implement proper bar scaling with 75% max height for better proportions
- Add enhanced hover tooltips with revenue and profit breakdown
- Fix top profitable products to show meaningful names instead of generic titles
- Improve product identification for orders with generic 'Product from Etsy' names
- Add comprehensive debugging for product sales tracking
- Enhanced visual design with better gradients and transitions
- Show units sold and total revenue in product details
- Improve month labels with cleaner formatting (no rotation)
- Better empty state handling and responsive layout
This commit is contained in:
dlawler489 2026-05-06 06:58:24 +10:00
parent 04069ef954
commit d325d547be

View file

@ -452,15 +452,28 @@ const Analytics = () => {
// Calculate top profitable products based on actual sales performance // Calculate top profitable products based on actual sales performance
const topProducts = useMemo(() => { const topProducts = useMemo(() => {
console.log('=== TOP PRODUCTS CALCULATION ===');
console.log('Filtered orders for products:', filteredOrders.length);
// Create a map to track sales and profits for each product // Create a map to track sales and profits for each product
const productSalesMap = new Map(); const productSalesMap = new Map();
filteredOrders.forEach(order => { filteredOrders.forEach((order, orderIndex) => {
if (order.items && Array.isArray(order.items)) { if (order.items && Array.isArray(order.items)) {
order.items.forEach(item => { console.log(`Order ${orderIndex + 1} items:`, order.items);
const key = item.title || 'Unknown Product'; order.items.forEach((item, itemIndex) => {
const current = productSalesMap.get(key) || { // Try to get a meaningful product identifier
title: item.title, let productKey = item.title || `Order Item ${orderIndex + 1}-${itemIndex + 1}`;
// If it's still a generic title, try to make it more specific
if (productKey === 'Product from Etsy' || productKey.includes('Product from')) {
productKey = `Product ${orderIndex + 1}-${itemIndex + 1} ($${item.price})`;
}
console.log(`Processing item: ${productKey}, Price: $${item.price}, Qty: ${item.quantity}`);
const current = productSalesMap.get(productKey) || {
title: productKey,
totalRevenue: 0, totalRevenue: 0,
totalQuantity: 0, totalQuantity: 0,
totalPrintingCost: 0, totalPrintingCost: 0,
@ -479,16 +492,23 @@ const Analytics = () => {
current.totalCostOfGoods += itemCostOfGoods; current.totalCostOfGoods += itemCostOfGoods;
current.totalProfit += itemProfit; current.totalProfit += itemProfit;
productSalesMap.set(key, current); productSalesMap.set(productKey, current);
}); });
} }
}); });
console.log('Product sales map:', Array.from(productSalesMap.entries()));
// Convert to array and sort by total profit // Convert to array and sort by total profit
return Array.from(productSalesMap.values()) const result = Array.from(productSalesMap.values())
.filter(product => product.totalRevenue > 0) .filter(product => product.totalRevenue > 0)
.sort((a, b) => b.totalProfit - a.totalProfit) .sort((a, b) => b.totalProfit - a.totalProfit)
.slice(0, 5); .slice(0, 5);
console.log('Top products result:', result);
console.log('===');
return result;
}, [filteredOrders]); }, [filteredOrders]);
const recentOrders = filteredOrders const recentOrders = filteredOrders
@ -697,35 +717,65 @@ const Analytics = () => {
{/* Revenue Chart */} {/* Revenue Chart */}
<div className="bg-white rounded-lg shadow p-6"> <div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Revenue Trend</h3> <h3 className="text-lg font-medium text-gray-900 mb-4">Revenue Trend</h3>
<div className="h-64"> <div className="h-64 relative">
{monthlyData.length > 0 ? ( {monthlyData.length > 0 ? (
<div className="h-full flex items-end justify-between space-x-2"> <div className="h-full flex items-end justify-between space-x-1 relative">
{monthlyData.map((data, index) => { {monthlyData.map((data, index) => {
const maxRevenue = Math.max(...monthlyData.map(d => d.revenue)); const maxRevenue = Math.max(...monthlyData.map(d => d.revenue));
const height = maxRevenue > 0 ? Math.max((data.revenue / maxRevenue) * 85, 2) : 0; const height = maxRevenue > 0 ? Math.max((data.revenue / maxRevenue) * 75, 3) : 0;
return ( return (
<div key={`month-${index}`} className="flex-1 flex flex-col items-center group relative"> <div key={`month-${index}`} className="flex-1 flex flex-col items-center group relative">
<div className="relative w-full"> {/* Value label above bar */}
<div className="absolute -top-8 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
<div className="bg-gray-900 text-white text-xs rounded py-1 px-2 whitespace-nowrap">
${data.revenue.toFixed(0)}
</div>
</div>
{/* Permanent value label for larger values */}
{data.revenue > maxRevenue * 0.3 && (
<div className="absolute left-1/2 transform -translate-x-1/2 text-xs text-white font-medium z-10"
style={{ bottom: `${height/2}%` }}>
${data.revenue > 1000 ? `${(data.revenue/1000).toFixed(1)}k` : data.revenue.toFixed(0)}
</div>
)}
<div className="relative w-full flex flex-col items-center">
<div <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" className="w-full bg-gradient-to-t from-blue-600 to-blue-400 rounded-t-sm transition-all duration-300 group-hover:from-blue-700 group-hover:to-blue-500 group-hover:shadow-lg relative"
style={{ style={{
height: `${height}%`, height: `${height}%`,
minHeight: data.revenue > 0 ? '4px' : '2px', minHeight: data.revenue > 0 ? '8px' : '2px',
maxHeight: '200px' maxHeight: '180px'
}} }}
/> >
{/* Tooltip */} {/* Value label on smaller bars */}
<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"> {data.revenue <= maxRevenue * 0.3 && data.revenue > 0 && (
<div className="bg-gray-900 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap shadow-lg"> <div className="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-gray-600 font-medium">
<div className="font-medium">{data.month}</div> ${data.revenue > 1000 ? `${(data.revenue/1000).toFixed(1)}k` : data.revenue.toFixed(0)}
<div>Revenue: ${data.revenue.toFixed(2)}</div> </div>
<div>Profit: ${data.profit.toFixed(2)}</div> )}
</div>
{/* Enhanced Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-8 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-30">
<div className="bg-gray-900 text-white text-xs rounded-lg py-3 px-4 whitespace-nowrap shadow-xl border border-gray-700">
<div className="font-semibold text-blue-300 mb-1">{data.month}</div>
<div className="flex items-center justify-between space-x-4">
<span>Revenue:</span>
<span className="font-medium text-green-300">${data.revenue.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between space-x-4">
<span>Profit:</span>
<span className="font-medium text-yellow-300">${data.profit.toFixed(2)}</span>
</div>
</div> </div>
</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 className="text-xs text-gray-500 mt-2 font-medium">
{data.month.split(' ')[0]}
</span> </span>
</div> </div>
); );
@ -806,8 +856,10 @@ const Analytics = () => {
<div className="flex items-center"> <div className="flex items-center">
<span className="text-sm font-medium text-gray-500 mr-3">#{index + 1}</span> <span className="text-sm font-medium text-gray-500 mr-3">#{index + 1}</span>
<div> <div>
<p className="text-sm font-medium text-gray-900 truncate max-w-48">{product.title || 'Unknown Product'}</p> <p className="text-sm font-medium text-gray-900 truncate max-w-48" title={product.title}>
<p className="text-xs text-gray-500">Sold: {product.totalQuantity} units</p> {product.title || 'Unknown Product'}
</p>
<p className="text-xs text-gray-500">Sold: {product.totalQuantity} units Revenue: ${product.totalRevenue.toFixed(2)}</p>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">