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:
parent
701c805d0c
commit
04069ef954
1 changed files with 164 additions and 44 deletions
|
|
@ -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;
|
||||
<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={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>
|
||||
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>
|
||||
)) : (
|
||||
|
|
|
|||
Loading…
Reference in a new issue