diff --git a/src/components/DashboardStats.tsx b/src/components/DashboardStats.tsx index 928b0f2..d27349e 100644 --- a/src/components/DashboardStats.tsx +++ b/src/components/DashboardStats.tsx @@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Factory, TrendingUp, Briefcase, BarChart3 } from 'lucide-react'; import { formatISK } from '@/utils/priceUtils'; -import RecapPopover from './RecapPopover'; +import OptimizedRecapPopover from './OptimizedRecapPopover'; import { IndJob } from '@/lib/types'; interface DashboardStatsProps { @@ -57,7 +57,7 @@ const DashboardStats = ({ - {formatISK(totalRevenue)} - + @@ -85,7 +85,7 @@ const DashboardStats = ({ - = 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`}> {formatISK(totalProfit)} - + diff --git a/src/components/OptimizedJobsSection.tsx b/src/components/OptimizedJobsSection.tsx new file mode 100644 index 0000000..859d04f --- /dev/null +++ b/src/components/OptimizedJobsSection.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { IndJob } from '@/lib/types'; +import JobGroup from './JobGroup'; + +interface OptimizedJobsSectionProps { + regularJobs: IndJob[]; + trackedJobs: IndJob[]; + collapsedGroups: Record; + loadingStatuses: Set; + onToggleGroup: (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; +} + +const OptimizedJobsSection = React.memo(({ + regularJobs, + trackedJobs, + collapsedGroups, + loadingStatuses, + onToggleGroup, + onEdit, + onDelete, + onUpdateProduced, + onImportBOM +}: OptimizedJobsSectionProps) => { + // Memoize expensive grouping operation + const jobGroups = useMemo(() => { + return regularJobs.reduce((groups, job) => { + const status = job.status; + if (!groups[status]) { + groups[status] = []; + } + groups[status].push(job); + return groups; + }, {} as Record); + }, [regularJobs]); + + return ( +
+
+ {Object.entries(jobGroups).map(([status, statusJobs]) => ( + + ))} +
+ + {trackedJobs.length > 0 && ( +
+ +
+ )} +
+ ); +}); + +OptimizedJobsSection.displayName = 'OptimizedJobsSection'; + +export default OptimizedJobsSection; \ No newline at end of file diff --git a/src/components/OptimizedRecapPopover.tsx b/src/components/OptimizedRecapPopover.tsx new file mode 100644 index 0000000..d160601 --- /dev/null +++ b/src/components/OptimizedRecapPopover.tsx @@ -0,0 +1,117 @@ +import React, { useState, useMemo } from 'react'; +import { IndJob } from '@/lib/types'; +import { formatISK } from '@/utils/priceUtils'; +import { CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { useNavigate } from 'react-router-dom'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { getStatusBackgroundColor } from '@/utils/jobStatusUtils'; + +interface OptimizedRecapPopoverProps { + title: string; + jobs: IndJob[]; + children: React.ReactNode; + calculateJobValue: (job: IndJob) => number; +} + +const OptimizedRecapPopover: React.FC = React.memo(({ + title, + jobs, + children, + calculateJobValue +}) => { + const navigate = useNavigate(); + const [sortDescending, setSortDescending] = useState(true); + + // Memoize expensive calculations + const jobContributions = useMemo(() => { + const contributions = jobs + .map(job => ({ + job, + value: calculateJobValue(job) + })) + .filter(({ value }) => value !== 0); + + return contributions.sort((a, b) => { + if (sortDescending) { + if (a.value < 0 && b.value >= 0) return -1; + if (a.value >= 0 && b.value < 0) return 1; + return Math.abs(b.value) - Math.abs(a.value); + } else { + if (a.value >= 0 && b.value < 0) return -1; + if (a.value < 0 && b.value >= 0) return 1; + return Math.abs(a.value) - Math.abs(b.value); + } + }); + }, [jobs, calculateJobValue, sortDescending]); + + const handleJobClick = (jobId: string) => { + navigate(`/${jobId}`); + }; + + const toggleSort = (e: React.MouseEvent) => { + e.stopPropagation(); + setSortDescending(!sortDescending); + }; + + const handleItemClick = (e: React.MouseEvent, jobId: string) => { + const target = e.target as HTMLElement; + const hasNoNavigate = target.closest('[data-no-navigate]'); + + if (!hasNoNavigate) { + handleJobClick(jobId); + } + }; + + return ( + + + {children} + + + + + {title} + + + + + {jobContributions.length === 0 ? ( +

No contributions to display

+ ) : ( + jobContributions.map(({ job, value }) => ( +
handleItemClick(e, job.id)} + className={`flex justify-between items-center p-2 rounded hover:bg-gray-700/50 cursor-pointer transition-colors border-l-2 border-l-gray-600 ${getStatusBackgroundColor(job.status)}`} + > +
+
+ {job.outputItem} +
+
+ ID: {job.id} +
+
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatISK(value)} +
+
+ )) + )} +
+
+
+ ); +}); + +OptimizedRecapPopover.displayName = 'OptimizedRecapPopover'; + +export default OptimizedRecapPopover; \ No newline at end of file diff --git a/src/hooks/useOptimizedJobMetrics.ts b/src/hooks/useOptimizedJobMetrics.ts new file mode 100644 index 0000000..7353f0d --- /dev/null +++ b/src/hooks/useOptimizedJobMetrics.ts @@ -0,0 +1,51 @@ +import { useMemo } from 'react'; +import { IndJob } from '@/lib/types'; + +// Memoized job metrics calculation with single-pass optimization +export const useOptimizedJobMetrics = (jobs: IndJob[]) => { + // Single-pass calculation with memoization on jobs array reference + const metrics = useMemo(() => { + const totalJobs = jobs.length; + let totalRevenue = 0; + let totalProfit = 0; + + // Pre-compute job metrics in single pass + const jobMetricsMap = new Map(); + + for (const job of jobs) { + const expenditure = job.expenditures?.reduce((sum, tx) => sum + tx.totalPrice, 0) || 0; + const income = job.income?.reduce((sum, tx) => sum + tx.totalPrice, 0) || 0; + const profit = income - expenditure; + + totalRevenue += income; + totalProfit += profit; + + jobMetricsMap.set(job.id, { + revenue: income, + profit, + expenditure, + income + }); + } + + // Create optimized calculation functions that use pre-computed values + const calculateJobRevenue = (job: IndJob) => { + return jobMetricsMap.get(job.id)?.revenue || 0; + }; + + const calculateJobProfit = (job: IndJob) => { + return jobMetricsMap.get(job.id)?.profit || 0; + }; + + return { + totalJobs, + totalRevenue, + totalProfit, + calculateJobRevenue, + calculateJobProfit, + jobMetricsMap + }; + }, [jobs]); + + return metrics; +}; \ No newline at end of file diff --git a/src/lib/pbtypes.ts b/src/lib/pbtypes.ts index bc87f93..baa5803 100644 --- a/src/lib/pbtypes.ts +++ b/src/lib/pbtypes.ts @@ -238,6 +238,16 @@ export type SigviewResponse = Required & BaseS export type SystemResponse = Required & BaseSystemFields export type WormholeSystemsResponse = Required & BaseSystemFields +// Facility types (mock for compatibility) +export type IndFacilityRecord = { + id: string; + name: string; + created?: IsoDateString; + updated?: IsoDateString; +} + +export type IndFacilityResponse = Required & BaseSystemFields + // Types containing all Records and Responses, useful for creating typing helper functions export type CollectionRecords = { diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 03fcdab..c50db11 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,10 +6,10 @@ import SearchOverlay from '@/components/SearchOverlay'; import TransactionChart from '@/components/TransactionChart'; import DashboardStats from '@/components/DashboardStats'; import JobsToolbar from '@/components/JobsToolbar'; -import JobsSection from '@/components/JobsSection'; +import OptimizedJobsSection from '@/components/OptimizedJobsSection'; import { useDashboard } from '@/hooks/useDashboard'; import { useDashboardHandlers } from '@/hooks/useDashboardHandlers'; -import { useJobMetrics } from '@/hooks/useJobMetrics'; +import { useOptimizedJobMetrics } from '@/hooks/useOptimizedJobMetrics'; import { useCategorizedJobs } from '@/hooks/useCategorizedJobs'; const Index = () => { @@ -71,7 +71,7 @@ const Index = () => { // Always call hooks before any conditional returns const { regularJobs, trackedJobs } = useCategorizedJobs(jobs, searchQuery); - const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useJobMetrics(regularJobs); + const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useOptimizedJobMetrics(regularJobs); if (loading) { return ( @@ -138,7 +138,7 @@ const Index = () => { onBatchExpenditure={() => setShowBatchExpenditureForm(true)} /> -