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 { BarChart3 } from 'lucide-react';
|
||||||
import JobTransactionPopover from './JobTransactionPopover';
|
import JobTransactionPopover from './JobTransactionPopover';
|
||||||
import TransactionChart from './TransactionChart';
|
import TransactionChart from './TransactionChart';
|
||||||
|
import { useJobCardMetrics } from '@/hooks/useJobCardMetrics';
|
||||||
|
|
||||||
interface JobCardMetricsProps {
|
interface JobCardMetricsProps {
|
||||||
job: IndJob;
|
job: IndJob;
|
||||||
@@ -19,31 +20,19 @@ const JobCardMetrics: React.FC<JobCardMetricsProps> = ({ job }) => {
|
|||||||
const { updateJob } = useJobs();
|
const { updateJob } = useJobs();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const sortedExpenditures = [...job.expenditures].sort((a, b) =>
|
// Use optimized hook for all expensive calculations
|
||||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
const {
|
||||||
);
|
sortedExpenditures,
|
||||||
const sortedIncome = [...job.income].sort((a, b) =>
|
sortedIncome,
|
||||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
totalExpenditure,
|
||||||
);
|
totalIncome,
|
||||||
|
profit,
|
||||||
const totalExpenditure = sortedExpenditures.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
margin,
|
||||||
const totalIncome = sortedIncome.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
itemsSold,
|
||||||
const profit = totalIncome - totalExpenditure;
|
produced,
|
||||||
const margin = totalIncome > 0 ? ((profit / totalIncome) * 100) : 0;
|
showPerformanceIndicator,
|
||||||
|
performancePercentage
|
||||||
// Calculate performance metrics - Simple price per unit comparison
|
} = useJobCardMetrics(job);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFieldClick = (fieldName: string, currentValue: number, e: React.MouseEvent) => {
|
const handleFieldClick = (fieldName: string, currentValue: number, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
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';
|
import { IndJob } from '@/lib/types';
|
||||||
|
|
||||||
export const useJobMetrics = (jobs: IndJob[]) => {
|
export const useJobMetrics = (jobs: IndJob[]) => {
|
||||||
const calculateJobRevenue = (job: IndJob) =>
|
// Memoize individual job calculations to avoid recalculating on every render
|
||||||
job.income.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
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 expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
||||||
const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
||||||
return income - expenditure;
|
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) => {
|
return {
|
||||||
const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
totalJobs,
|
||||||
const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0);
|
totalRevenue,
|
||||||
return sum + (income - expenditure);
|
totalProfit
|
||||||
}, 0);
|
};
|
||||||
|
}, [jobs]);
|
||||||
const totalRevenue = jobs.reduce((sum, job) =>
|
|
||||||
sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalJobs,
|
...metrics,
|
||||||
totalProfit,
|
|
||||||
totalRevenue,
|
|
||||||
calculateJobRevenue,
|
calculateJobRevenue,
|
||||||
calculateJobProfit
|
calculateJobProfit
|
||||||
};
|
};
|
||||||
|
@@ -10,7 +10,7 @@ import JobsSection from '@/components/JobsSection';
|
|||||||
import { useDashboard } from '@/hooks/useDashboard';
|
import { useDashboard } from '@/hooks/useDashboard';
|
||||||
import { useDashboardHandlers } from '@/hooks/useDashboardHandlers';
|
import { useDashboardHandlers } from '@/hooks/useDashboardHandlers';
|
||||||
import { useJobMetrics } from '@/hooks/useJobMetrics';
|
import { useJobMetrics } from '@/hooks/useJobMetrics';
|
||||||
import { categorizeJobs } from '@/utils/jobFiltering';
|
import { useCategorizedJobs } from '@/hooks/useCategorizedJobs';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {
|
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);
|
const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useJobMetrics(regularJobs);
|
||||||
|
|
||||||
if (showJobForm) {
|
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) => {
|
export const categorizeJobs = (jobs: IndJob[], searchQuery: string) => {
|
||||||
const sortedJobs = sortJobs(jobs);
|
const query = searchQuery.toLowerCase();
|
||||||
const regularJobs = filterJobs(sortedJobs.filter(job => job.status !== 'Tracked'), searchQuery);
|
const hasQuery = Boolean(searchQuery);
|
||||||
const trackedJobs = filterJobs(sortedJobs.filter(job => job.status === 'Tracked'), 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 };
|
return { regularJobs, trackedJobs };
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user