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);
|
||||
}, [orders, dateRange]);
|
||||
|
||||
// Filter expenses by the same date range
|
||||
// Filter expenses by the same date range as orders (shared logic)
|
||||
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;
|
||||
}
|
||||
});
|
||||
return ProfitAnalysisService.filterExpensesByDateRange(expenses || [], dateRange);
|
||||
}, [expenses, dateRange]);
|
||||
|
||||
// Calculate profit metrics using the service with expenses
|
||||
|
|
|
|||
|
|
@ -258,65 +258,67 @@ export class ProfitAnalysisService {
|
|||
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
|
||||
*/
|
||||
static filterOrdersByDateRange(orders: Order[], dateRange: string): Order[] {
|
||||
if (dateRange === 'all' || !orders) return orders;
|
||||
|
||||
const now = new Date();
|
||||
const filteredOrders = orders.filter(order => {
|
||||
const orderDate = new Date(order.dateOrdered);
|
||||
|
||||
// Handle specific month format (e.g., "2025-07" for July 2025)
|
||||
if (dateRange.match(/^\d{4}-\d{2}$/)) {
|
||||
const [year, month] = dateRange.split('-');
|
||||
return orderDate.getFullYear() === parseInt(year) &&
|
||||
orderDate.getMonth() === parseInt(month) - 1;
|
||||
return orders.filter(order => this.isInDateRange(order.dateOrdered, dateRange));
|
||||
}
|
||||
|
||||
// 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;
|
||||
/**
|
||||
* Filter expenses by date range (same logic as orders)
|
||||
*/
|
||||
static filterExpensesByDateRange(expenses: Expense[], dateRange: string): Expense[] {
|
||||
if (dateRange === 'all' || !expenses) return expenses;
|
||||
return expenses.filter(expense => this.isInDateRange(expense.date, dateRange));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue