Fix Profit Analysis using all-time expenses for year/quarter filters
The Profit Analysis expense filter only handled month and rolling presets, so selecting a year (e.g. 2026) fell through to 'return true' and counted ALL expenses against that year's revenue — producing a wrong (often negative) profit that disagreed with the Analytics page. Unify date filtering: new ProfitAnalysisService.isInDateRange is the single matcher for months, quarters, years, custom ranges, and presets; filterOrdersByDateRange and the new filterExpensesByDateRange both use it, and Profit Analysis now filters expenses through the service so orders and expenses always share the same range. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a789f01bb4
commit
71a01e92ff
2 changed files with 57 additions and 81 deletions
|
|
@ -29,35 +29,9 @@ const ProfitAnalysis = () => {
|
||||||
return ProfitAnalysisService.filterOrdersByDateRange(orders || [], dateRange);
|
return ProfitAnalysisService.filterOrdersByDateRange(orders || [], dateRange);
|
||||||
}, [orders, dateRange]);
|
}, [orders, dateRange]);
|
||||||
|
|
||||||
// Filter expenses by the same date range
|
// Filter expenses by the same date range as orders (shared logic)
|
||||||
const filteredExpenses = useMemo(() => {
|
const filteredExpenses = useMemo(() => {
|
||||||
if (!expenses || dateRange === 'all') return expenses || [];
|
return ProfitAnalysisService.filterExpensesByDateRange(expenses || [], dateRange);
|
||||||
|
|
||||||
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]);
|
}, [expenses, dateRange]);
|
||||||
|
|
||||||
// Calculate profit metrics using the service with expenses
|
// Calculate profit metrics using the service with expenses
|
||||||
|
|
|
||||||
|
|
@ -258,65 +258,67 @@ export class ProfitAnalysisService {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a date falls within a date-range token. Single source of truth so
|
||||||
|
* orders and expenses are always filtered identically (months, quarters,
|
||||||
|
* years, custom ranges, and legacy presets).
|
||||||
|
*/
|
||||||
|
static isInDateRange(dateValue: string | Date, dateRange: string): boolean {
|
||||||
|
if (dateRange === 'all') return true;
|
||||||
|
|
||||||
|
const date = new Date(dateValue);
|
||||||
|
|
||||||
|
// Specific month format (e.g., "2025-07")
|
||||||
|
if (dateRange.match(/^\d{4}-\d{2}$/)) {
|
||||||
|
const [year, month] = dateRange.split('-');
|
||||||
|
return date.getFullYear() === parseInt(year) && date.getMonth() === parseInt(month) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quarter format (e.g., "2025-Q1")
|
||||||
|
if (dateRange.match(/^\d{4}-Q[1-4]$/)) {
|
||||||
|
const [year, quarter] = dateRange.split('-Q');
|
||||||
|
return date.getFullYear() === parseInt(year) &&
|
||||||
|
Math.floor(date.getMonth() / 3) + 1 === parseInt(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Year format (e.g., "2025")
|
||||||
|
if (dateRange.match(/^\d{4}$/)) {
|
||||||
|
return date.getFullYear() === parseInt(dateRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom range (e.g., "2025-01-01_2025-12-31")
|
||||||
|
if (dateRange.includes('_')) {
|
||||||
|
const [startDate, endDate] = dateRange.split('_');
|
||||||
|
const end = new Date(endDate);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
return date >= new Date(startDate) && date <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy preset ranges
|
||||||
|
const daysDiff = (Date.now() - date.getTime()) / (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': return daysDiff >= 0 && daysDiff <= 365;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter orders by date range
|
* Filter orders by date range
|
||||||
*/
|
*/
|
||||||
static filterOrdersByDateRange(orders: Order[], dateRange: string): Order[] {
|
static filterOrdersByDateRange(orders: Order[], dateRange: string): Order[] {
|
||||||
if (dateRange === 'all' || !orders) return orders;
|
if (dateRange === 'all' || !orders) return orders;
|
||||||
|
return orders.filter(order => this.isInDateRange(order.dateOrdered, dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
/**
|
||||||
const filteredOrders = orders.filter(order => {
|
* Filter expenses by date range (same logic as orders)
|
||||||
const orderDate = new Date(order.dateOrdered);
|
*/
|
||||||
|
static filterExpensesByDateRange(expenses: Expense[], dateRange: string): Expense[] {
|
||||||
// Handle specific month format (e.g., "2025-07" for July 2025)
|
if (dateRange === 'all' || !expenses) return expenses;
|
||||||
if (dateRange.match(/^\d{4}-\d{2}$/)) {
|
return expenses.filter(expense => this.isInDateRange(expense.date, dateRange));
|
||||||
const [year, month] = dateRange.split('-');
|
|
||||||
return orderDate.getFullYear() === parseInt(year) &&
|
|
||||||
orderDate.getMonth() === parseInt(month) - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle quarter format (e.g., "2025-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 year format (e.g., "2025")
|
|
||||||
if (dateRange.match(/^\d{4}$/)) {
|
|
||||||
return orderDate.getFullYear() === parseInt(dateRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
return orderDate >= start && orderDate <= end;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle legacy preset ranges
|
|
||||||
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':
|
|
||||||
return daysDiff >= 0 && daysDiff <= 365;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredOrders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue