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:
parent
22799cb732
commit
761fce047a
3 changed files with 159 additions and 17 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue