diff --git a/src/components/JobCardMetrics.tsx b/src/components/JobCardMetrics.tsx index adb98ae..229cbe0 100644 --- a/src/components/JobCardMetrics.tsx +++ b/src/components/JobCardMetrics.tsx @@ -7,6 +7,7 @@ import { useToast } from '@/hooks/use-toast'; import { BarChart3 } from 'lucide-react'; import JobTransactionPopover from './JobTransactionPopover'; import TransactionChart from './TransactionChart'; +import { useJobCardMetrics } from '@/hooks/useJobCardMetrics'; interface JobCardMetricsProps { job: IndJob; @@ -19,31 +20,19 @@ const JobCardMetrics: React.FC = ({ job }) => { const { updateJob } = useJobs(); const { toast } = useToast(); - const sortedExpenditures = [...job.expenditures].sort((a, b) => - new Date(b.date).getTime() - new Date(a.date).getTime() - ); - const sortedIncome = [...job.income].sort((a, b) => - new Date(b.date).getTime() - new Date(a.date).getTime() - ); - - const totalExpenditure = sortedExpenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); - const totalIncome = sortedIncome.reduce((sum, tx) => sum + tx.totalPrice, 0); - const profit = totalIncome - totalExpenditure; - const margin = totalIncome > 0 ? ((profit / totalIncome) * 100) : 0; - - // Calculate performance metrics - Simple price per unit comparison - const itemsSold = sortedIncome.reduce((sum, tx) => sum + tx.quantity, 0); - const produced = job.produced || 0; - - // Only show performance if we have produced items and sold items - const showPerformanceIndicator = produced > 0 && itemsSold > 0 && job.projectedRevenue > 0; - - let performancePercentage = 0; - if (showPerformanceIndicator) { - const expectedPPU = job.projectedRevenue / produced; - const actualPPU = totalIncome / itemsSold; - performancePercentage = (actualPPU / expectedPPU) * 100; - } + // Use optimized hook for all expensive calculations + const { + sortedExpenditures, + sortedIncome, + totalExpenditure, + totalIncome, + profit, + margin, + itemsSold, + produced, + showPerformanceIndicator, + performancePercentage + } = useJobCardMetrics(job); const handleFieldClick = (fieldName: string, currentValue: number, e: React.MouseEvent) => { e.stopPropagation(); diff --git a/src/hooks/useCategorizedJobs.ts b/src/hooks/useCategorizedJobs.ts new file mode 100644 index 0000000..9ffca0e --- /dev/null +++ b/src/hooks/useCategorizedJobs.ts @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; +import { IndJob } from '@/lib/types'; +import { categorizeJobs } from '@/utils/jobFiltering'; + +export const useCategorizedJobs = (jobs: IndJob[], searchQuery: string) => { + return useMemo(() => { + return categorizeJobs(jobs, searchQuery); + }, [jobs, searchQuery]); +}; \ No newline at end of file diff --git a/src/hooks/useJobCardMetrics.ts b/src/hooks/useJobCardMetrics.ts new file mode 100644 index 0000000..828afe5 --- /dev/null +++ b/src/hooks/useJobCardMetrics.ts @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { IndJob } from '@/lib/types'; + +export const useJobCardMetrics = (job: IndJob) => { + return useMemo(() => { + // Sort transactions once and cache the results + const sortedExpenditures = [...job.expenditures].sort((a, b) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); + const sortedIncome = [...job.income].sort((a, b) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); + + // Calculate core metrics + const totalExpenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); + const totalIncome = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + const profit = totalIncome - totalExpenditure; + const margin = totalIncome > 0 ? ((profit / totalIncome) * 100) : 0; + + // Performance metrics calculation + const itemsSold = job.income.reduce((sum, tx) => sum + tx.quantity, 0); + const produced = job.produced || 0; + const showPerformanceIndicator = produced > 0 && itemsSold > 0 && job.projectedRevenue > 0; + + let performancePercentage = 0; + if (showPerformanceIndicator) { + const expectedPPU = job.projectedRevenue / produced; + const actualPPU = totalIncome / itemsSold; + performancePercentage = (actualPPU / expectedPPU) * 100; + } + + return { + sortedExpenditures, + sortedIncome, + totalExpenditure, + totalIncome, + profit, + margin, + itemsSold, + produced, + showPerformanceIndicator, + performancePercentage + }; + }, [ + job.expenditures, + job.income, + job.produced, + job.projectedRevenue + ]); +}; \ No newline at end of file diff --git a/src/hooks/useJobMetrics.ts b/src/hooks/useJobMetrics.ts index 409a990..b28a80b 100644 --- a/src/hooks/useJobMetrics.ts +++ b/src/hooks/useJobMetrics.ts @@ -1,32 +1,44 @@ +import { useMemo } from 'react'; import { IndJob } from '@/lib/types'; export const useJobMetrics = (jobs: IndJob[]) => { - const calculateJobRevenue = (job: IndJob) => - job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + // Memoize individual job calculations to avoid recalculating on every render + const calculateJobRevenue = useMemo(() => (job: IndJob) => { + return job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + }, []); - const calculateJobProfit = (job: IndJob) => { + const calculateJobProfit = useMemo(() => (job: IndJob) => { const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); return income - expenditure; - }; + }, []); - const totalJobs = jobs.length; + // Memoize expensive aggregation calculations - only recalculate when jobs actually change + const metrics = useMemo(() => { + const totalJobs = jobs.length; + + // Single pass through jobs to calculate both revenue and profit + let totalRevenue = 0; + let totalProfit = 0; + + for (const job of jobs) { + const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); + const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + + totalRevenue += income; + totalProfit += (income - expenditure); + } - const totalProfit = jobs.reduce((sum, job) => { - const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); - const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); - return sum + (income - expenditure); - }, 0); - - const totalRevenue = jobs.reduce((sum, job) => - sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 - ); + return { + totalJobs, + totalRevenue, + totalProfit + }; + }, [jobs]); return { - totalJobs, - totalProfit, - totalRevenue, + ...metrics, calculateJobRevenue, calculateJobProfit }; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 2d3299f..3febcea 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -10,7 +10,7 @@ import JobsSection from '@/components/JobsSection'; import { useDashboard } from '@/hooks/useDashboard'; import { useDashboardHandlers } from '@/hooks/useDashboardHandlers'; import { useJobMetrics } from '@/hooks/useJobMetrics'; -import { categorizeJobs } from '@/utils/jobFiltering'; +import { useCategorizedJobs } from '@/hooks/useCategorizedJobs'; const Index = () => { const { @@ -85,7 +85,7 @@ const Index = () => { ); } - const { regularJobs, trackedJobs } = categorizeJobs(jobs, searchQuery); + const { regularJobs, trackedJobs } = useCategorizedJobs(jobs, searchQuery); const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useJobMetrics(regularJobs); if (showJobForm) { diff --git a/src/utils/jobFiltering.ts b/src/utils/jobFiltering.ts index 8f11912..0ef9d0f 100644 --- a/src/utils/jobFiltering.ts +++ b/src/utils/jobFiltering.ts @@ -21,10 +21,37 @@ export const sortJobs = (jobs: IndJob[]) => { }); }; +// Optimized categorizeJobs function - single pass through data export const categorizeJobs = (jobs: IndJob[], searchQuery: string) => { - const sortedJobs = sortJobs(jobs); - const regularJobs = filterJobs(sortedJobs.filter(job => job.status !== 'Tracked'), searchQuery); - const trackedJobs = filterJobs(sortedJobs.filter(job => job.status === 'Tracked'), searchQuery); + const query = searchQuery.toLowerCase(); + const hasQuery = Boolean(searchQuery); + + // Single pass: sort, filter, and categorize in one operation + const sortedJobs = [...jobs].sort((a, b) => { + const priorityA = getStatusPriority(a.status); + const priorityB = getStatusPriority(b.status); + if (priorityA === priorityB) { + return new Date(b.created || '').getTime() - new Date(a.created || '').getTime(); + } + return priorityA - priorityB; + }); + + const regularJobs: IndJob[] = []; + const trackedJobs: IndJob[] = []; + + for (const job of sortedJobs) { + // Apply search filter if needed + if (hasQuery && !job.outputItem.toLowerCase().includes(query)) { + continue; + } + + // Categorize based on status + if (job.status === 'Tracked') { + trackedJobs.push(job); + } else { + regularJobs.push(job); + } + } return { regularJobs, trackedJobs }; };