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.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-28 18:54:40 +00:00
committed by PhatPhuckDave
parent 8fcaaf2332
commit e164cd8fbe
6 changed files with 134 additions and 47 deletions

View File

@@ -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<JobCardMetricsProps> = ({ 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();

View File

@@ -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]);
};

View File

@@ -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
]);
};

View File

@@ -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;
};
}, []);
// Memoize expensive aggregation calculations - only recalculate when jobs actually change
const metrics = useMemo(() => {
const totalJobs = jobs.length;
const totalProfit = jobs.reduce((sum, job) => {
// 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);
return sum + (income - expenditure);
}, 0);
const totalRevenue = jobs.reduce((sum, job) =>
sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0
);
totalRevenue += income;
totalProfit += (income - expenditure);
}
return {
totalJobs,
totalProfit,
totalRevenue,
totalProfit
};
}, [jobs]);
return {
...metrics,
calculateJobRevenue,
calculateJobProfit
};

View File

@@ -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) {

View File

@@ -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 };
};