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:
parent
04069ef954
commit
d325d547be
1 changed files with 77 additions and 25 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue