From e164cd8fbe0a55798ea24821458e2632b6b798de Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:54:40 +0000 Subject: [PATCH] Optimize home page performance Address significant performance issues on the home page by optimizing data processing and calculations. The focus is on reducing computational overhead, as database operations appear to be efficient. --- src/components/JobCardMetrics.tsx | 39 +++++++++--------------- src/hooks/useCategorizedJobs.ts | 9 ++++++ src/hooks/useJobCardMetrics.ts | 50 +++++++++++++++++++++++++++++++ src/hooks/useJobMetrics.ts | 46 +++++++++++++++++----------- src/pages/Index.tsx | 4 +-- src/utils/jobFiltering.ts | 33 ++++++++++++++++++-- 6 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 src/hooks/useCategorizedJobs.ts create mode 100644 src/hooks/useJobCardMetrics.ts 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 }; };