Fix profit analysis calculations to include shipping expenses and other costs

- Updated ProfitAnalysisService to accept and integrate expense data
- Modified calculateProfitMetrics to include expenses in profit calculations
- Added totalExpenses field to ProfitMetrics interface
- Updated ProfitAnalysis component to pass filtered expenses to calculations
- Enhanced UI to show breakdown of expenses vs printing costs
- Fixed inconsistency between Analytics and ProfitAnalysis profit calculations
- Now shipping expenses and Etsy fees are properly deducted from profits
- Excluded transaction fees to avoid double-counting
This commit is contained in:
dlawler489 2026-05-05 13:21:00 +10:00
parent 22799cb732
commit 761fce047a
3 changed files with 159 additions and 17 deletions

View file

@ -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<string | null>(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 = () => {
</div>
</div>
{/* Second row for additional metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{/* Total Expenses */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-3 rounded-full bg-red-100">
<BarChart3 className="w-6 h-6 text-red-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Other Expenses</p>
<p className="text-2xl font-bold text-gray-900">${profitMetrics.totalExpenses.toFixed(2)}</p>
</div>
</div>
</div>
{/* Total Costs */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-3 rounded-full bg-yellow-100">
<Target className="w-6 h-6 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Costs</p>
<p className="text-2xl font-bold text-gray-900">${(profitMetrics.totalPrintingCosts + profitMetrics.totalExpenses).toFixed(2)}</p>
</div>
</div>
</div>
{/* Average Order Value */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-3 rounded-full bg-indigo-100">
<DollarSign className="w-6 h-6 text-indigo-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Avg Order Value</p>
<p className="text-2xl font-bold text-gray-900">${profitMetrics.averageOrderValue.toFixed(2)}</p>
</div>
</div>
</div>
</div>
{/* Profit Summary */}
<div className="bg-white p-6 rounded-lg shadow mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Profit Summary</h3>

View file

@ -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<string, MonthlyProfit>();
// 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 };
})

View file

@ -18,7 +18,14 @@
"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": {