Fix performance issues
Address performance problems on the home page and when navigating from job details.
This commit is contained in:
68
src/components/OptimizedJobCard.tsx
Normal file
68
src/components/OptimizedJobCard.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { IndJob } from '@/lib/types';
|
||||
import { getStatusBackgroundColor } from '@/utils/jobStatusUtils';
|
||||
import { getAttentionGlowClasses } from '@/utils/jobAttentionUtils';
|
||||
import JobCardHeader from './JobCardHeader';
|
||||
import JobCardDetails from './JobCardDetails';
|
||||
import JobCardMetrics from './JobCardMetrics';
|
||||
|
||||
interface OptimizedJobCardProps {
|
||||
job: IndJob;
|
||||
onEdit: (job: any) => void;
|
||||
onDelete: (jobId: string) => void;
|
||||
onUpdateProduced?: (jobId: string, produced: number) => void;
|
||||
onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void;
|
||||
needsAttention: boolean; // Pre-calculated
|
||||
isTracked?: boolean;
|
||||
}
|
||||
|
||||
const OptimizedJobCard: React.FC<OptimizedJobCardProps> = React.memo(({
|
||||
job,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onUpdateProduced,
|
||||
onImportBOM,
|
||||
needsAttention,
|
||||
isTracked = false
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const hasNoNavigate = target.closest('[data-no-navigate]');
|
||||
|
||||
if (hasNoNavigate) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/${job.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`bg-gray-900 border-gray-700 text-white h-full flex flex-col cursor-pointer hover:bg-gray-800/50 transition-colors ${job.status === 'Tracked' ? 'border-l-4 border-l-cyan-600' : ''} ${getStatusBackgroundColor(job.status)} ${needsAttention ? getAttentionGlowClasses() : ''}`}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<JobCardHeader
|
||||
job={job}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col space-y-4">
|
||||
<JobCardDetails job={job} />
|
||||
<div className="flex-1" />
|
||||
<JobCardMetrics job={job} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
OptimizedJobCard.displayName = 'OptimizedJobCard';
|
||||
|
||||
export default OptimizedJobCard;
|
84
src/components/OptimizedJobGroup.tsx
Normal file
84
src/components/OptimizedJobGroup.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { IndJob } from '@/lib/types';
|
||||
import { getStatusColor } from '@/utils/jobStatusUtils';
|
||||
import { getAttentionGlowClasses } from '@/utils/jobAttentionUtils';
|
||||
import OptimizedJobCard from './OptimizedJobCard';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface OptimizedJobGroupProps {
|
||||
status: string;
|
||||
jobs: IndJob[];
|
||||
isCollapsed: boolean;
|
||||
onToggle: (status: string) => void;
|
||||
onEdit: (job: IndJob) => void;
|
||||
onDelete: (jobId: string) => void;
|
||||
onUpdateProduced?: (jobId: string, produced: number) => void;
|
||||
onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void;
|
||||
hasAttentionJobs: boolean; // Pre-calculated
|
||||
jobAttentionMap: (jobId: string) => boolean; // Pre-calculated lookup
|
||||
isTracked?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const OptimizedJobGroup: React.FC<OptimizedJobGroupProps> = React.memo(({
|
||||
status,
|
||||
jobs,
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onUpdateProduced,
|
||||
onImportBOM,
|
||||
hasAttentionJobs,
|
||||
jobAttentionMap,
|
||||
isTracked = false,
|
||||
isLoading = false
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={`${getStatusColor(status)} rounded-lg cursor-pointer select-none transition-colors hover:opacity-90 ${hasAttentionJobs ? getAttentionGlowClasses() : ''}`}
|
||||
onClick={() => onToggle(status)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<h3 className="text-xl font-semibold text-white flex items-center gap-3">
|
||||
<span>{status}</span>
|
||||
<span className="text-gray-200 text-lg">({jobs.length} jobs)</span>
|
||||
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
</h3>
|
||||
<div className={`text-white text-lg transition-transform ${isCollapsed ? '-rotate-90' : 'rotate-0'}`}>
|
||||
⌄
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full flex items-center justify-center p-8 text-gray-400">
|
||||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||||
Loading jobs...
|
||||
</div>
|
||||
) : (
|
||||
jobs.map(job => (
|
||||
<OptimizedJobCard
|
||||
key={job.id}
|
||||
job={job}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
needsAttention={jobAttentionMap(job.id)}
|
||||
isTracked={isTracked}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
OptimizedJobGroup.displayName = 'OptimizedJobGroup';
|
||||
|
||||
export default OptimizedJobGroup;
|
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { IndJob } from '@/lib/types';
|
||||
import JobGroup from './JobGroup';
|
||||
import OptimizedJobGroup from './OptimizedJobGroup';
|
||||
import { useJobAttentionMetrics } from '@/hooks/useJobAttentionMetrics';
|
||||
|
||||
interface OptimizedJobsSectionProps {
|
||||
regularJobs: IndJob[];
|
||||
@@ -37,11 +38,15 @@ const OptimizedJobsSection = React.memo(({
|
||||
}, {} as Record<string, IndJob[]>);
|
||||
}, [regularJobs]);
|
||||
|
||||
// Pre-calculate all attention metrics once
|
||||
const allJobs = useMemo(() => [...regularJobs, ...trackedJobs], [regularJobs, trackedJobs]);
|
||||
const { jobNeedsAttention, groupNeedsAttention } = useJobAttentionMetrics(allJobs);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{Object.entries(jobGroups).map(([status, statusJobs]) => (
|
||||
<JobGroup
|
||||
<OptimizedJobGroup
|
||||
key={status}
|
||||
status={status}
|
||||
jobs={statusJobs}
|
||||
@@ -51,6 +56,8 @@ const OptimizedJobsSection = React.memo(({
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
hasAttentionJobs={groupNeedsAttention(status)}
|
||||
jobAttentionMap={jobNeedsAttention}
|
||||
isLoading={loadingStatuses.has(status)}
|
||||
/>
|
||||
))}
|
||||
@@ -58,7 +65,7 @@ const OptimizedJobsSection = React.memo(({
|
||||
|
||||
{trackedJobs.length > 0 && (
|
||||
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
|
||||
<JobGroup
|
||||
<OptimizedJobGroup
|
||||
status="Tracked"
|
||||
jobs={trackedJobs}
|
||||
isCollapsed={collapsedGroups['Tracked'] || false}
|
||||
@@ -67,6 +74,8 @@ const OptimizedJobsSection = React.memo(({
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
hasAttentionJobs={groupNeedsAttention('Tracked')}
|
||||
jobAttentionMap={jobNeedsAttention}
|
||||
isTracked={true}
|
||||
isLoading={loadingStatuses.has('Tracked')}
|
||||
/>
|
||||
|
80
src/hooks/useJobAttentionMetrics.ts
Normal file
80
src/hooks/useJobAttentionMetrics.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IndJob } from '@/lib/types';
|
||||
|
||||
// Optimized attention calculation with caching
|
||||
export const useJobAttentionMetrics = (jobs: IndJob[]) => {
|
||||
return useMemo(() => {
|
||||
const attentionMap = new Map<string, boolean>();
|
||||
const groupAttentionMap = new Map<string, boolean>();
|
||||
const statusGroups = new Map<string, IndJob[]>();
|
||||
|
||||
// Group jobs by status
|
||||
jobs.forEach(job => {
|
||||
const status = job.status;
|
||||
if (!statusGroups.has(status)) {
|
||||
statusGroups.set(status, []);
|
||||
}
|
||||
statusGroups.get(status)!.push(job);
|
||||
});
|
||||
|
||||
// Calculate attention for all jobs in one pass
|
||||
jobs.forEach(job => {
|
||||
let needsAttention = false;
|
||||
|
||||
// Acquisition jobs need attention when all materials are satisfied
|
||||
if (job.status === 'Acquisition') {
|
||||
if (job.billOfMaterials && job.billOfMaterials.length > 0) {
|
||||
const requiredMaterials = new Map<string, number>();
|
||||
job.billOfMaterials.forEach(item => {
|
||||
requiredMaterials.set(item.name, item.quantity);
|
||||
});
|
||||
|
||||
const ownedMaterials = new Map<string, number>();
|
||||
job.expenditures?.forEach(transaction => {
|
||||
const currentOwned = ownedMaterials.get(transaction.itemName) || 0;
|
||||
ownedMaterials.set(transaction.itemName, currentOwned + transaction.quantity);
|
||||
});
|
||||
|
||||
let allMaterialsSatisfied = true;
|
||||
requiredMaterials.forEach((required, materialName) => {
|
||||
const owned = ownedMaterials.get(materialName) || 0;
|
||||
if (owned < required) {
|
||||
allMaterialsSatisfied = false;
|
||||
}
|
||||
});
|
||||
|
||||
needsAttention = allMaterialsSatisfied;
|
||||
}
|
||||
}
|
||||
// Running jobs need attention when they have finished
|
||||
else if (job.status === 'Running') {
|
||||
if (job.jobStart && job.runtime) {
|
||||
const startTime = new Date(job.jobStart).getTime();
|
||||
const runtimeMs = job.runtime * 1000;
|
||||
const finishTime = startTime + runtimeMs;
|
||||
const currentTime = Date.now();
|
||||
needsAttention = currentTime >= finishTime;
|
||||
}
|
||||
}
|
||||
// Selling jobs need attention when sold count reaches produced count
|
||||
else if (job.status === 'Selling') {
|
||||
const produced = job.produced || 0;
|
||||
const sold = job.income?.reduce((sum, tx) => sum + tx.quantity, 0) || 0;
|
||||
needsAttention = sold >= produced && produced > 0;
|
||||
}
|
||||
|
||||
attentionMap.set(job.id, needsAttention);
|
||||
});
|
||||
|
||||
// Calculate group attention
|
||||
statusGroups.forEach((jobs, status) => {
|
||||
const hasAttention = jobs.some(job => attentionMap.get(job.id) || false);
|
||||
groupAttentionMap.set(status, hasAttention);
|
||||
});
|
||||
|
||||
return {
|
||||
jobNeedsAttention: (jobId: string) => attentionMap.get(jobId) || false,
|
||||
groupNeedsAttention: (status: string) => groupAttentionMap.get(status) || false
|
||||
};
|
||||
}, [jobs]);
|
||||
};
|
Reference in New Issue
Block a user