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 React, { useMemo } from 'react';
|
||||||
import { IndJob } from '@/lib/types';
|
import { IndJob } from '@/lib/types';
|
||||||
import JobGroup from './JobGroup';
|
import OptimizedJobGroup from './OptimizedJobGroup';
|
||||||
|
import { useJobAttentionMetrics } from '@/hooks/useJobAttentionMetrics';
|
||||||
|
|
||||||
interface OptimizedJobsSectionProps {
|
interface OptimizedJobsSectionProps {
|
||||||
regularJobs: IndJob[];
|
regularJobs: IndJob[];
|
||||||
@@ -37,11 +38,15 @@ const OptimizedJobsSection = React.memo(({
|
|||||||
}, {} as Record<string, IndJob[]>);
|
}, {} as Record<string, IndJob[]>);
|
||||||
}, [regularJobs]);
|
}, [regularJobs]);
|
||||||
|
|
||||||
|
// Pre-calculate all attention metrics once
|
||||||
|
const allJobs = useMemo(() => [...regularJobs, ...trackedJobs], [regularJobs, trackedJobs]);
|
||||||
|
const { jobNeedsAttention, groupNeedsAttention } = useJobAttentionMetrics(allJobs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(jobGroups).map(([status, statusJobs]) => (
|
{Object.entries(jobGroups).map(([status, statusJobs]) => (
|
||||||
<JobGroup
|
<OptimizedJobGroup
|
||||||
key={status}
|
key={status}
|
||||||
status={status}
|
status={status}
|
||||||
jobs={statusJobs}
|
jobs={statusJobs}
|
||||||
@@ -51,6 +56,8 @@ const OptimizedJobsSection = React.memo(({
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onUpdateProduced={onUpdateProduced}
|
onUpdateProduced={onUpdateProduced}
|
||||||
onImportBOM={onImportBOM}
|
onImportBOM={onImportBOM}
|
||||||
|
hasAttentionJobs={groupNeedsAttention(status)}
|
||||||
|
jobAttentionMap={jobNeedsAttention}
|
||||||
isLoading={loadingStatuses.has(status)}
|
isLoading={loadingStatuses.has(status)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -58,7 +65,7 @@ const OptimizedJobsSection = React.memo(({
|
|||||||
|
|
||||||
{trackedJobs.length > 0 && (
|
{trackedJobs.length > 0 && (
|
||||||
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
|
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
|
||||||
<JobGroup
|
<OptimizedJobGroup
|
||||||
status="Tracked"
|
status="Tracked"
|
||||||
jobs={trackedJobs}
|
jobs={trackedJobs}
|
||||||
isCollapsed={collapsedGroups['Tracked'] || false}
|
isCollapsed={collapsedGroups['Tracked'] || false}
|
||||||
@@ -67,6 +74,8 @@ const OptimizedJobsSection = React.memo(({
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onUpdateProduced={onUpdateProduced}
|
onUpdateProduced={onUpdateProduced}
|
||||||
onImportBOM={onImportBOM}
|
onImportBOM={onImportBOM}
|
||||||
|
hasAttentionJobs={groupNeedsAttention('Tracked')}
|
||||||
|
jobAttentionMap={jobNeedsAttention}
|
||||||
isTracked={true}
|
isTracked={true}
|
||||||
isLoading={loadingStatuses.has('Tracked')}
|
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