etsy-finance-tracker/client/src/pages/Analytics.tsx
dlawler489 701c805d0c feat: implement dynamic date selection with custom range support
- Add dynamic date range generation based on actual order data
- Implement custom date range picker with start/end date inputs
- Unify date filtering logic between Analytics and ProfitAnalysis pages
- Support multiple date formats (YYYY, YYYY-MM, YYYY-QX, custom ranges)
- Remove hardcoded years for future-proof date selection
- Enhance ProfitAnalysisService with custom date range support
2026-05-05 22:04:52 +10:00

747 lines
No EOL
31 KiB
TypeScript

import { useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
import { calculateOrderPrintingCost, enrichOrderItemsWithCosts } from '../utils/orderCalculations';
import { formatAustralianDate } from '../utils/dateFormatter';
import { TrendingUp, DollarSign, Package, Users, ShoppingCart, Download, Printer } from 'lucide-react';
const Analytics = () => {
const { orders } = useSelector((state: RootState) => state.orders);
const { products } = useSelector((state: RootState) => state.products);
const { customers } = useSelector((state: RootState) => state.customers);
const { expenses } = useSelector((state: RootState) => state.expenses);
const [dateRange, setDateRange] = useState(() => {
// Default to current year
return new Date().getFullYear().toString();
});
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
// Generate dynamic date range options based on actual order data
const dateRangeOptions = useMemo(() => {
const presets = [
{ value: 'all', label: 'All Time', type: 'preset' },
{ value: 'week', label: 'Last 7 Days', type: 'preset' },
{ value: 'month', label: 'Last 30 Days', type: 'preset' },
{ value: 'quarter', label: 'Last 90 Days', type: 'preset' },
{ value: 'year', label: 'This Year (Calendar)', type: 'preset' },
{ value: 'custom', label: 'Custom Date Range', type: 'preset' }
];
if (!orders || orders.length === 0) {
return {
presets,
years: [],
quarters: [],
months: []
};
}
// Get date ranges from actual order data
const years = new Set<number>();
const months = new Set<string>();
const quarters = new Set<string>();
orders.forEach(order => {
if (order.dateOrdered) {
const date = new Date(order.dateOrdered);
const year = date.getFullYear();
const month = date.getMonth();
const quarter = Math.floor(month / 3) + 1;
years.add(year);
months.add(`${year}-${String(month + 1).padStart(2, '0')}`);
quarters.add(`${year}-Q${quarter}`);
}
});
// Add year options (sorted newest first)
const yearOptions = Array.from(years)
.sort((a, b) => b - a)
.map(year => ({
value: year.toString(),
label: year.toString(),
type: 'year'
}));
// Add quarter options (last 8 quarters)
const quarterOptions = Array.from(quarters)
.sort((a, b) => b.localeCompare(a))
.slice(0, 8)
.map(quarter => ({
value: quarter,
label: quarter.replace('-', ' '),
type: 'quarter'
}));
// Add month options (last 24 months)
const monthOptions = Array.from(months)
.sort((a, b) => b.localeCompare(a))
.slice(0, 24)
.map(monthKey => {
const [year, month] = monthKey.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1);
const label = date.toLocaleDateString('en-AU', { year: 'numeric', month: 'long' });
return {
value: monthKey,
label,
type: 'month'
};
});
return {
presets,
years: yearOptions,
quarters: quarterOptions,
months: monthOptions
};
}, [orders]);
// Helper function to get updated printing cost using current product costs
const getUpdatedPrintingCost = (order: any) => {
const updatedItems = enrichOrderItemsWithCosts(order.items || [], products || []);
return calculateOrderPrintingCost(updatedItems);
};
// Filter orders and expenses by selected date range using consistent logic
const filteredOrders = useMemo(() => {
if (!orders || dateRange === 'all') return orders || [];
const now = new Date();
return orders.filter(order => {
if (!order.dateOrdered) return false;
const orderDate = new Date(order.dateOrdered);
// Handle specific month format (e.g., "2026-03" for March 2026)
if (dateRange.match(/^\d{4}-\d{2}$/)) {
const [year, month] = dateRange.split('-');
return orderDate.getFullYear() === parseInt(year) &&
orderDate.getMonth() === parseInt(month) - 1;
}
// Handle quarter format (e.g., "2026-Q1")
if (dateRange.match(/^\d{4}-Q[1-4]$/)) {
const [year, quarter] = dateRange.split('-Q');
const targetYear = parseInt(year);
const targetQuarter = parseInt(quarter);
const orderYear = orderDate.getFullYear();
const orderQuarter = Math.floor(orderDate.getMonth() / 3) + 1;
return orderYear === targetYear && orderQuarter === targetQuarter;
}
// Handle custom date range format (e.g., "2025-01-01_2025-12-31")
if (dateRange.includes('_')) {
const [startDate, endDate] = dateRange.split('_');
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include the end date
return orderDate >= start && orderDate <= end;
}
// Handle year format (e.g., "2026") - calendar year, not rolling 365 days
if (dateRange.match(/^\d{4}$/)) {
return orderDate.getFullYear() === parseInt(dateRange);
}
// Handle legacy preset ranges (rolling periods from today)
const timeDiff = now.getTime() - orderDate.getTime();
const daysDiff = timeDiff / (24 * 60 * 60 * 1000);
switch (dateRange) {
case 'week':
return daysDiff >= 0 && daysDiff <= 7;
case 'month':
return daysDiff >= 0 && daysDiff <= 30;
case 'quarter':
return daysDiff >= 0 && daysDiff <= 90;
case 'year':
// For "This Year" option, show current calendar year
return orderDate.getFullYear() === now.getFullYear();
case 'custom':
// Handle custom date range using state variables
if (customStartDate && customEndDate) {
const start = new Date(customStartDate);
const end = new Date(customEndDate);
end.setHours(23, 59, 59, 999); // Include the end date
return orderDate >= start && orderDate <= end;
}
return true;
default:
return true;
}
});
}, [orders, dateRange]);
const filteredExpenses = useMemo(() => {
if (!expenses || dateRange === 'all') return expenses || [];
const now = new Date();
return expenses.filter(expense => {
if (!expense.date) return false;
const expenseDate = new Date(expense.date);
// Handle specific month format (e.g., "2026-03" for March 2026)
if (dateRange.match(/^\d{4}-\d{2}$/)) {
const [year, month] = dateRange.split('-');
return expenseDate.getFullYear() === parseInt(year) &&
expenseDate.getMonth() === parseInt(month) - 1;
}
// Handle quarter format (e.g., "2026-Q1")
if (dateRange.match(/^\d{4}-Q[1-4]$/)) {
const [year, quarter] = dateRange.split('-Q');
const targetYear = parseInt(year);
const targetQuarter = parseInt(quarter);
const expenseYear = expenseDate.getFullYear();
const expenseQuarter = Math.floor(expenseDate.getMonth() / 3) + 1;
return expenseYear === targetYear && expenseQuarter === targetQuarter;
}
// Handle custom date range format (e.g., "2025-01-01_2025-12-31")
if (dateRange.includes('_')) {
const [startDate, endDate] = dateRange.split('_');
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include the end date
return expenseDate >= start && expenseDate <= end;
}
// Handle year format (e.g., "2026") - calendar year, not rolling 365 days
if (dateRange.match(/^\d{4}$/)) {
return expenseDate.getFullYear() === parseInt(dateRange);
}
// Handle legacy preset ranges (rolling periods from today)
const timeDiff = now.getTime() - expenseDate.getTime();
const daysDiff = timeDiff / (24 * 60 * 60 * 1000);
switch (dateRange) {
case 'week':
return daysDiff >= 0 && daysDiff <= 7;
case 'month':
return daysDiff >= 0 && daysDiff <= 30;
case 'quarter':
return daysDiff >= 0 && daysDiff <= 90;
case 'year':
// For "This Year" option, show current calendar year
return expenseDate.getFullYear() === now.getFullYear();
case 'custom':
// Handle custom date range using state variables
if (customStartDate && customEndDate) {
const start = new Date(customStartDate);
const end = new Date(customEndDate);
end.setHours(23, 59, 59, 999); // Include the end date
return expenseDate >= start && expenseDate <= end;
}
return true;
default:
return true;
}
});
}, [expenses, dateRange]);
// Calculate metrics with filtered data
const totalRevenue = filteredOrders.reduce((sum, order) => sum + (order?.total || 0), 0);
const totalPrintingCosts = filteredOrders.reduce((sum, order) => {
return sum + getUpdatedPrintingCost(order);
}, 0);
// Calculate expenses excluding only sale transaction fees to avoid double-counting
// (Sale transaction fees are already deducted from order totals in the CSV)
// But we DO want to include Etsy Ads, Listing Fees, GST, subscriptions, etc.
const totalExpenses = filteredExpenses.reduce((sum, expense) => {
// Only exclude transaction fees that are tied to specific orders (already deducted from order totals)
// Include: Listing Fees, Marketing/Ads, GST, and other business expenses
const isSaleTransactionFee = (
expense.vendor?.toLowerCase().includes('etsy') &&
expense.category?.toLowerCase() === 'transaction fees' &&
expense.reference && // Has an order reference
(expense.description?.toLowerCase().includes('transaction fee') ||
expense.description?.toLowerCase().includes('processing fee'))
);
if (isSaleTransactionFee) {
console.log('Excluding sale transaction fee from profit calc:', expense.description, '$' + expense.amount, 'Category:', expense.category);
return sum; // Skip sale transaction fees as they're already deducted from order totals
}
return sum + (expense?.amount || 0);
}, 0);
const netProfit = totalRevenue - totalExpenses - totalPrintingCosts;
const profitMargin = totalRevenue > 0 ? (netProfit / totalRevenue) * 100 : 0;
// Debug logging to verify the separation
console.log('=== EXPENSE SEPARATION DEBUG ===');
console.log('Date range filter:', dateRange);
console.log('Filtered orders count:', filteredOrders.length);
console.log('Filtered expenses count:', filteredExpenses.length);
const allExpensesTotal = filteredExpenses.reduce((sum, expense) => sum + (expense.amount || 0), 0);
const transactionFeesTotal = filteredExpenses.reduce((sum, expense) => {
const isSaleTransactionFee = (
expense.vendor?.toLowerCase().includes('etsy') &&
expense.category?.toLowerCase() === 'transaction fees' &&
expense.reference && // Has an order reference
(expense.description?.toLowerCase().includes('transaction fee') ||
expense.description?.toLowerCase().includes('processing fee'))
);
if (isSaleTransactionFee) {
console.log('Found sale transaction fee:', expense.description, '$' + expense.amount, 'Category:', expense.category);
}
return isSaleTransactionFee ? sum + (expense.amount || 0) : sum;
}, 0);
console.log('All Expenses Total:', allExpensesTotal);
console.log('Etsy Transaction Fees Total:', transactionFeesTotal);
console.log('Expenses for Profit Calc (excluding transaction fees):', totalExpenses);
console.log('Difference (should equal transaction fees):', allExpensesTotal - totalExpenses);
console.log('Total Revenue:', totalRevenue);
console.log('Total Printing Costs:', totalPrintingCosts);
console.log('Net Profit:', netProfit);
console.log('Profit Margin:', profitMargin.toFixed(1) + '%');
const averageOrderValue = filteredOrders.length > 0 ? totalRevenue / filteredOrders.length : 0;
const totalCustomers = customers?.length || 0;
// Create monthly revenue data from actual orders
const monthlyData = useMemo(() => {
if (!filteredOrders.length) 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 });
});
// 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);
if (current) {
current.revenue += order.total || 0;
}
}
});
// Aggregate expenses by month (showing all expenses for visibility)
filteredExpenses.forEach(expense => {
if (expense.date) {
const date = new Date(expense.date);
const monthName = months[date.getMonth()];
const current = monthlyMap.get(monthName);
if (current) {
// For monthly chart, exclude transaction fees to match profit calculation
const isEtsyTransactionFee = (
(expense.vendor?.toLowerCase() === 'etsy' &&
(expense.category?.toLowerCase().includes('transaction fee') ||
expense.description?.toLowerCase().includes('transaction fee')))
);
if (!isEtsyTransactionFee) {
current.expenses += expense.amount || 0;
}
}
}
});
// Calculate profit for each month
monthlyMap.forEach(data => {
data.profit = data.revenue - data.expenses;
});
return Array.from(monthlyMap.values()).filter(data => data.revenue > 0 || data.expenses > 0);
}, [filteredOrders, filteredExpenses]);
// Create expense categories data from actual expenses
const expenseCategories = useMemo(() => {
const categories = new Map();
filteredExpenses.forEach(expense => {
// Show ALL expenses in categories (including Etsy transaction fees for visibility)
// The profit calculation will handle excluding transaction fees separately
const category = expense.category || 'Other';
const current = categories.get(category) || 0;
categories.set(category, current + (expense.amount || 0));
});
const totalExpenseAmount = Array.from(categories.values()).reduce((sum, amount) => sum + amount, 0);
const categoryData = Array.from(categories.entries()).map(([category, amount]) => ({
category,
amount,
percentage: totalExpenseAmount > 0 ? (amount / totalExpenseAmount) * 100 : 0
})).sort((a, b) => b.amount - a.amount);
// Debug: Log detailed expense breakdown
console.log('=== DETAILED EXPENSE BREAKDOWN ===');
console.log('Total Expenses for Display (all expenses):', totalExpenseAmount.toFixed(2));
console.log('Total Expenses for Profit Calc (excluding transaction fees):', totalExpenses.toFixed(2));
console.log('Expense categories breakdown:');
categoryData.forEach(cat => {
console.log(` ${cat.category}: $${cat.amount.toFixed(2)} (${cat.percentage.toFixed(1)}%)`);
});
console.log('PROFIT CALCULATION:');
console.log(`Revenue: $${totalRevenue.toFixed(2)}`);
console.log(`Printing Costs: $${totalPrintingCosts.toFixed(2)}`);
console.log(`Other Expenses (excluding transaction fees): $${totalExpenses.toFixed(2)}`);
console.log(`Net Profit: $${totalRevenue.toFixed(2)} - $${totalPrintingCosts.toFixed(2)} - $${totalExpenses.toFixed(2)} = $${netProfit.toFixed(2)}`);
console.log('===');
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);
const recentOrders = filteredOrders
.filter(order => order && order.dateOrdered)
.sort((a, b) => new Date(b.dateOrdered).getTime() - new Date(a.dateOrdered).getTime())
.slice(0, 5);
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Analytics</h1>
<p className="text-gray-600 mt-1">Business insights and performance metrics</p>
</div>
<div className="flex gap-3">
<select
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
>
{/* Preset Ranges */}
{dateRangeOptions.presets.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
{/* Years */}
{dateRangeOptions.years.length > 0 && (
<optgroup label="Years">
{dateRangeOptions.years.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
)}
{/* Quarters */}
{dateRangeOptions.quarters.length > 0 && (
<optgroup label="Quarters">
{dateRangeOptions.quarters.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
)}
{/* Months */}
{dateRangeOptions.months.length > 0 && (
<optgroup label="Months">
{dateRangeOptions.months.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
)}
</select>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
<Download className="w-4 h-4" />
Export Report
</button>
</div>
</div>
{/* Custom Date Range Inputs */}
{dateRange === 'custom' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-4">
<div>
<label htmlFor="startDate" className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
id="startDate"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="endDate" className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
id="endDate"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="pt-6">
<button
onClick={() => {
if (customStartDate && customEndDate) {
setDateRange(`${customStartDate}_${customEndDate}`);
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
disabled={!customStartDate || !customEndDate}
>
Apply Range
</button>
</div>
</div>
</div>
)}
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-500">Total Revenue</p>
<p className="text-2xl font-semibold text-gray-900">${totalRevenue.toFixed(2)}</p>
</div>
<div className="flex-shrink-0">
<DollarSign className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
<span className="text-green-600 font-medium">12.5%</span>
<span className="text-gray-600 ml-1">vs last period</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-500">Net Profit</p>
<p className="text-2xl font-semibold text-gray-900">${netProfit.toFixed(2)}</p>
</div>
<div className="flex-shrink-0">
<TrendingUp className="h-8 w-8 text-blue-500" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-gray-600">Margin: </span>
<span className="text-blue-600 font-medium ml-1">{profitMargin.toFixed(1)}%</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-500">Orders</p>
<p className="text-2xl font-semibold text-gray-900">{filteredOrders.length}</p>
</div>
<div className="flex-shrink-0">
<ShoppingCart className="h-8 w-8 text-purple-500" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-gray-600">AOV: </span>
<span className="text-purple-600 font-medium ml-1">${averageOrderValue.toFixed(2)}</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-500">Customers</p>
<p className="text-2xl font-semibold text-gray-900">{totalCustomers}</p>
</div>
<div className="flex-shrink-0">
<Users className="h-8 w-8 text-orange-500" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-gray-600">Repeat rate: </span>
<span className="text-orange-600 font-medium ml-1">
{totalCustomers > 0 && customers ?
((customers.filter(c => c && c.totalOrders > 1).length / totalCustomers) * 100).toFixed(1) :
0}%
</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-500">Printing Costs</p>
<p className="text-2xl font-semibold text-gray-900">${totalPrintingCosts.toFixed(2)}</p>
</div>
<div className="flex-shrink-0">
<Printer className="h-8 w-8 text-indigo-500" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-gray-600">Avg per order: </span>
<span className="text-indigo-600 font-medium ml-1">
${orders?.length > 0 ? (totalPrintingCosts / orders.length).toFixed(2) : '0.00'}
</span>
</div>
</div>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* 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>
);
}) : (
<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>
{monthlyData.length > 0 && (
<span>Peak: ${Math.max(...monthlyData.map(d => d.revenue)).toFixed(2)}</span>
)}
</div>
</div>
{/* Expense Breakdown */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Expense Categories</h3>
<div className="space-y-4">
{expenseCategories.length > 0 ? expenseCategories.map((category, index) => {
const colors = ['bg-blue-600', 'bg-green-600', 'bg-purple-600', 'bg-orange-600', 'bg-red-600', 'bg-indigo-600'];
const colorClass = colors[index % colors.length];
return (
<div key={category.category} className="flex justify-between items-center">
<span className="text-sm text-gray-600">{category.category}</span>
<div className="flex items-center">
<div className="flex flex-col items-center mr-3">
<span className="text-xs text-gray-500 mb-1">${category.amount.toFixed(2)}</span>
<div className="w-24 bg-gray-200 rounded-full h-2">
<div
className={`${colorClass} h-2 rounded-full`}
style={{ width: `${category.percentage}%` }}
/>
</div>
</div>
<span className="text-sm font-medium">{category.percentage.toFixed(1)}%</span>
</div>
</div>
);
}) : (
<div className="flex items-center justify-center h-32 text-gray-500">
No expense data for selected period
</div>
)}
</div>
</div>
</div>
{/* Tables Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products */}
<div className="bg-white rounded-lg shadow p-6">
<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 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>
</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)}
</p>
<p className="text-xs text-gray-500">profit per item</p>
</div>
</div>
)) : (
<div className="text-center text-gray-500 py-4">
<p>No products found</p>
</div>
)}
</div>
</div>
{/* Recent Orders */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Recent Orders</h3>
<div className="space-y-3">
{recentOrders.length > 0 ? recentOrders.map((order) => (
<div key={order?._id || Math.random()} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0">
<div className="flex items-center">
<div className="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<Package className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">#{order?.orderNumber || 'N/A'}</p>
<p className="text-xs text-gray-500">{order?.dateOrdered ? formatAustralianDate(order.dateOrdered) : 'N/A'}</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">${(order?.total || 0).toFixed(2)}</p>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
order?.status === 'delivered' ? 'bg-green-100 text-green-800' :
order?.status === 'shipped' ? 'bg-blue-100 text-blue-800' :
order?.status === 'processing' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{order?.status || 'unknown'}
</span>
</div>
</div>
)) : (
<div className="text-center text-gray-500 py-4">
<p>No recent orders</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default Analytics;