Fix performance issues

Address performance problems on the home page and when navigating from job details.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-28 19:12:12 +00:00
committed by PhatPhuckDave
parent b1e08815b3
commit cf5f666651
4 changed files with 244 additions and 3 deletions

View 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;

View 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;

View File

@@ -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')}
/>

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