diff --git a/client/src/pages/ProfitAnalysis.tsx b/client/src/pages/ProfitAnalysis.tsx index cce0fc8..f067ab6 100644 --- a/client/src/pages/ProfitAnalysis.tsx +++ b/client/src/pages/ProfitAnalysis.tsx @@ -7,6 +7,7 @@ import { TrendingUp, DollarSign, Package, Target, BarChart3, ChevronDown, Chevro const ProfitAnalysis = () => { const { orders } = useSelector((state: RootState) => state.orders); const { products } = useSelector((state: RootState) => state.products); + const { expenses } = useSelector((state: RootState) => state.expenses); const [dateRange, setDateRange] = useState('all'); const [selectedView, setSelectedView] = useState<'overview' | 'trends' | 'products' | 'orders'>('overview'); const [expandedOrder, setExpandedOrder] = useState(null); @@ -23,10 +24,41 @@ const ProfitAnalysis = () => { return ProfitAnalysisService.filterOrdersByDateRange(orders || [], dateRange); }, [orders, dateRange]); - // Calculate profit metrics using the service + // Filter expenses by the same date range + const filteredExpenses = useMemo(() => { + if (!expenses || dateRange === 'all') return expenses || []; + + const now = new Date(); + return expenses.filter(expense => { + const expenseDate = new Date(expense.date); + + // Handle specific month format (e.g., "2025-07" for July 2025) + if (dateRange.match(/^\d{4}-\d{2}$/)) { + const [year, month] = dateRange.split('-'); + return expenseDate.getFullYear() === parseInt(year) && + expenseDate.getMonth() === parseInt(month) - 1; + } + + // Handle preset ranges + switch (dateRange) { + case 'week': + return expenseDate >= new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case 'month': + return expenseDate >= new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + case 'quarter': + return expenseDate >= new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + case 'year': + return expenseDate >= new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + default: + return true; + } + }); + }, [expenses, dateRange]); + + // Calculate profit metrics using the service with expenses const profitMetrics = useMemo(() => { - return ProfitAnalysisService.calculateProfitMetrics(filteredOrders, products || []); - }, [filteredOrders, products]); + return ProfitAnalysisService.calculateProfitMetrics(filteredOrders, products || [], filteredExpenses); + }, [filteredOrders, products, filteredExpenses]); // Top performing products analysis const productPerformance = useMemo(() => { @@ -215,6 +247,48 @@ const ProfitAnalysis = () => { + {/* Second row for additional metrics */} +
+ {/* Total Expenses */} +
+
+
+ +
+
+

Other Expenses

+

${profitMetrics.totalExpenses.toFixed(2)}

+
+
+
+ + {/* Total Costs */} +
+
+
+ +
+
+

Total Costs

+

${(profitMetrics.totalPrintingCosts + profitMetrics.totalExpenses).toFixed(2)}

+
+
+
+ + {/* Average Order Value */} +
+
+
+ +
+
+

Avg Order Value

+

${profitMetrics.averageOrderValue.toFixed(2)}

+
+
+
+
+ {/* Profit Summary */}

Profit Summary

diff --git a/client/src/utils/profitAnalysisService.ts b/client/src/utils/profitAnalysisService.ts index 2e82db7..2123219 100644 --- a/client/src/utils/profitAnalysisService.ts +++ b/client/src/utils/profitAnalysisService.ts @@ -1,10 +1,12 @@ import { Order } from '../store/slices/orderSlice'; import { Product } from '../store/slices/productSlice'; +import { Expense } from '../store/slices/expenseSlice'; import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts } from './orderCalculations'; export interface ProfitMetrics { totalRevenue: number; totalPrintingCosts: number; + totalExpenses: number; // Added: All expenses excluding transaction fees totalProfit: number; averageMargin: number; orderCount: number; @@ -29,6 +31,7 @@ export interface MonthlyProfit { month: string; revenue: number; costs: number; + expenses: number; // Added: Total expenses for the month profit: number; margin: number; orderCount: number; @@ -66,13 +69,14 @@ export interface DateRangeOption { export class ProfitAnalysisService { /** - * Calculate comprehensive profit metrics from orders + * Calculate comprehensive profit metrics from orders and expenses */ - static calculateProfitMetrics(orders: Order[], products: Product[]): ProfitMetrics { + static calculateProfitMetrics(orders: Order[], products: Product[], expenses?: Expense[]): ProfitMetrics { if (!orders || orders.length === 0) { return { totalRevenue: 0, totalPrintingCosts: 0, + totalExpenses: 0, totalProfit: 0, averageMargin: 0, orderCount: 0, @@ -84,29 +88,49 @@ export class ProfitAnalysisService { let totalRevenue = 0; let totalPrintingCosts = 0; - let totalProfit = 0; let profitableOrderCount = 0; + // Calculate revenue and printing costs from orders orders.forEach(order => { const revenue = order.total || 0; const enrichedItems = enrichOrderItemsWithCosts(order.items || [], products || []); const printingCost = calculateOrderPrintingCost(enrichedItems); - const profit = calculateOrderProfit(enrichedItems, revenue); totalRevenue += revenue; totalPrintingCosts += printingCost; - totalProfit += profit; - if (profit > 0) profitableOrderCount++; + // Count profitable orders based on printing costs only (before expenses) + const orderProfit = calculateOrderProfit(enrichedItems, revenue); + if (orderProfit > 0) profitableOrderCount++; }); + // Calculate total expenses (excluding transaction fees to avoid double-counting) + const totalExpenses = expenses ? expenses.reduce((sum, expense) => { + // More comprehensive detection of Etsy transaction fees + const isEtsyTransactionFee = ( + expense.category?.toLowerCase() === 'transaction fees' || + (expense.vendor?.toLowerCase().includes('etsy') && + (expense.category?.toLowerCase().includes('transaction fee') || + expense.description?.toLowerCase().includes('transaction fee') || + expense.description?.toLowerCase().includes('etsy transaction fee'))) + ); + + if (isEtsyTransactionFee) { + return sum; // Skip transaction fees as they're already deducted from order totals + } + return sum + (expense?.amount || 0); + }, 0) : 0; + + // Calculate final profit including all expenses + const totalProfit = totalRevenue - totalPrintingCosts - totalExpenses; const averageMargin = totalRevenue > 0 ? (totalProfit / totalRevenue) * 100 : 0; const averageOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0; - const averageCost = orders.length > 0 ? totalPrintingCosts / orders.length : 0; + const averageCost = orders.length > 0 ? (totalPrintingCosts + totalExpenses) / orders.length : 0; return { totalRevenue, totalPrintingCosts, + totalExpenses, totalProfit, averageMargin, orderCount: orders.length, @@ -167,13 +191,14 @@ export class ProfitAnalysisService { } /** - * Calculate monthly profit trends + * Calculate monthly profit trends including expenses */ - static calculateMonthlyTrends(orders: Order[], products: Product[]): MonthlyProfit[] { + static calculateMonthlyTrends(orders: Order[], products: Product[], expenses?: Expense[]): MonthlyProfit[] { if (!orders || orders.length === 0) return []; const monthlyData = new Map(); + // Process orders by month orders.forEach(order => { const orderDate = new Date(order.dateOrdered); const monthKey = `${orderDate.getFullYear()}-${String(orderDate.getMonth() + 1).padStart(2, '0')}`; @@ -182,13 +207,13 @@ export class ProfitAnalysisService { const revenue = order.total || 0; const enrichedItems = enrichOrderItemsWithCosts(order.items || [], products || []); const costs = calculateOrderPrintingCost(enrichedItems); - const profit = revenue - costs; if (!monthlyData.has(monthKey)) { monthlyData.set(monthKey, { month: monthName, revenue: 0, costs: 0, + expenses: 0, profit: 0, margin: 0, orderCount: 0 @@ -198,13 +223,49 @@ export class ProfitAnalysisService { const monthStats = monthlyData.get(monthKey)!; monthStats.revenue += revenue; monthStats.costs += costs; - monthStats.profit += profit; monthStats.orderCount += 1; }); - // Calculate margins and sort by date + // Process expenses by month (if provided) + if (expenses) { + expenses.forEach(expense => { + const expenseDate = new Date(expense.date); + const monthKey = `${expenseDate.getFullYear()}-${String(expenseDate.getMonth() + 1).padStart(2, '0')}`; + const monthName = expenseDate.toLocaleDateString('en-AU', { year: 'numeric', month: 'long' }); + + // Skip transaction fees to avoid double-counting + const isEtsyTransactionFee = ( + expense.category?.toLowerCase() === 'transaction fees' || + (expense.vendor?.toLowerCase().includes('etsy') && + (expense.category?.toLowerCase().includes('transaction fee') || + expense.description?.toLowerCase().includes('transaction fee') || + expense.description?.toLowerCase().includes('etsy transaction fee'))) + ); + + if (isEtsyTransactionFee) return; + + // Create month entry if it doesn't exist (for expense-only months) + if (!monthlyData.has(monthKey)) { + monthlyData.set(monthKey, { + month: monthName, + revenue: 0, + costs: 0, + expenses: 0, + profit: 0, + margin: 0, + orderCount: 0 + }); + } + + const monthStats = monthlyData.get(monthKey)!; + monthStats.expenses += expense.amount || 0; + }); + } + + // Calculate final profits and margins const results = Array.from(monthlyData.entries()) .map(([key, data]) => { + data.profit = data.revenue - data.costs - data.expenses; data.margin = data.revenue > 0 ? (data.profit / data.revenue) * 100 : 0; return { key, ...data }; }) diff --git a/package.json b/package.json index 71eac08..b562739 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,17 @@ "client:test": "cd client && npm test", "server:test": "cd server && npm test" }, - "keywords": ["etsy", "business", "tracking", "analytics", "inventory", "orders"], + "keywords": [ + "etsy", + "business", + "tracking", + "analytics", + "inventory", + "orders" + ], "author": "Your Name", "license": "MIT", "devDependencies": { "concurrently": "^8.2.2" } -} \ No newline at end of file +}