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(); const months = new Set(); const quarters = new Set(); 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 (

Analytics

Business insights and performance metrics

{/* Custom Date Range Inputs */} {dateRange === 'custom' && (
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" />
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" />
)} {/* Key Metrics */}

Total Revenue

${totalRevenue.toFixed(2)}

12.5% vs last period

Net Profit

${netProfit.toFixed(2)}

Margin: {profitMargin.toFixed(1)}%

Orders

{filteredOrders.length}

AOV: ${averageOrderValue.toFixed(2)}

Customers

{totalCustomers}

Repeat rate: {totalCustomers > 0 && customers ? ((customers.filter(c => c && c.totalOrders > 1).length / totalCustomers) * 100).toFixed(1) : 0}%

Printing Costs

${totalPrintingCosts.toFixed(2)}

Avg per order: ${orders?.length > 0 ? (totalPrintingCosts / orders.length).toFixed(2) : '0.00'}
{/* Charts Row */}
{/* Revenue Chart */}

Revenue Trend

{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 (
0 ? '8px' : '0px' }} title={`${data.month}: $${data.revenue.toFixed(2)}`} /> {data.month}
); }) : (
No revenue data for selected period
)}
Revenue by Month {monthlyData.length > 0 && ( Peak: ${Math.max(...monthlyData.map(d => d.revenue)).toFixed(2)} )}
{/* Expense Breakdown */}

Expense Categories

{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 (
{category.category}
${category.amount.toFixed(2)}
{category.percentage.toFixed(1)}%
); }) : (
No expense data for selected period
)}
{/* Tables Row */}
{/* Top Products */}

Top Profitable Products

{topProducts.length > 0 ? topProducts.map((product, index) => (
#{index + 1}

{product?.title || 'Unknown Product'}

SKU: {product?.sku || 'N/A'}

${((product?.price || 0) - (product?.costOfGoods || 0) - (product?.printingCost || 0)).toFixed(2)}

profit per item

)) : (

No products found

)}
{/* Recent Orders */}

Recent Orders

{recentOrders.length > 0 ? recentOrders.map((order) => (

#{order?.orderNumber || 'N/A'}

{order?.dateOrdered ? formatAustralianDate(order.dateOrdered) : 'N/A'}

${(order?.total || 0).toFixed(2)}

{order?.status || 'unknown'}
)) : (

No recent orders

)}
); }; export default Analytics;