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:
@@ -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();
|
||||
|
9
src/hooks/useCategorizedJobs.ts
Normal file
9
src/hooks/useCategorizedJobs.ts
Normal 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]);
|
||||
};
|
50
src/hooks/useJobCardMetrics.ts
Normal file
50
src/hooks/useJobCardMetrics.ts
Normal 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
|
||||
]);
|
||||
};
|
@@ -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
|
||||
};
|
||||
|
@@ -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) {
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
Reference in New Issue
Block a user