diff --git a/client/src/pages/ProfitAnalysis.tsx b/client/src/pages/ProfitAnalysis.tsx index 2b27aeb..3f3511f 100644 --- a/client/src/pages/ProfitAnalysis.tsx +++ b/client/src/pages/ProfitAnalysis.tsx @@ -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 diff --git a/client/src/utils/profitAnalysisService.ts b/client/src/utils/profitAnalysisService.ts index d36bd98..8be732e 100644 --- a/client/src/utils/profitAnalysisService.ts +++ b/client/src/utils/profitAnalysisService.ts @@ -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; + return orders.filter(order => this.isInDateRange(order.dateOrdered, dateRange)); + } - 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; - } - - // 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)); } /**