From 7eb957f66ab2bc9c10c295acc696401ef6a475a8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:03:43 +0000 Subject: [PATCH] Optimize home page performance Address significant performance issues on the home page, where operations like moving jobs or adding transactions take several seconds despite fast database requests. Focus on optimizing client-side computations and rendering to improve responsiveness. --- src/components/JobStatusDropdown.tsx | 120 +++++++++++++++++++++++++++ src/services/dataService.ts | 108 ++++++++++++++++-------- 2 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 src/components/JobStatusDropdown.tsx diff --git a/src/components/JobStatusDropdown.tsx b/src/components/JobStatusDropdown.tsx new file mode 100644 index 0000000..6916d7d --- /dev/null +++ b/src/components/JobStatusDropdown.tsx @@ -0,0 +1,120 @@ + +import { useState, useRef } from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { IndJob } from '@/lib/types'; +import { getStatusColor, JOB_STATUSES } from '@/utils/jobStatusUtils'; +import { useJobs } from '@/hooks/useDataService'; +import { useToast } from '@/hooks/use-toast'; + +interface JobStatusDropdownProps { + job: IndJob; +} + +const JobStatusDropdown: React.FC = ({ job }) => { + const { updateJob } = useJobs(); + const { toast } = useToast(); + const [isUpdating, setIsUpdating] = useState(false); + const updateTimeoutRef = useRef(null); + + const handleStatusChange = async (newStatus: string, e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + // Prevent duplicate calls + if (isUpdating || job.status === newStatus) { + return; + } + + // Clear any pending timeout + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + + setIsUpdating(true); + + try { + const currentTime = new Date().toISOString(); + const updates: { status: string; [key: string]: any } = { status: newStatus }; + + // Automatically assign dates based on status + switch (newStatus) { + case 'Running': + updates.jobStart = currentTime; + break; + case 'Done': + updates.jobEnd = currentTime; + break; + case 'Selling': + updates.saleStart = currentTime; + break; + case 'Closed': + updates.saleEnd = currentTime; + break; + } + + await updateJob(job.id, updates); + + const dateMessages = []; + if (updates.jobStart) dateMessages.push('job start date set'); + if (updates.jobEnd) dateMessages.push('job end date set'); + if (updates.saleStart) dateMessages.push('sale start date set'); + if (updates.saleEnd) dateMessages.push('sale end date set'); + + const description = dateMessages.length > 0 + ? `Job status changed to ${newStatus} and ${dateMessages.join(', ')}` + : `Job status changed to ${newStatus}`; + + toast({ + title: "Status Updated", + description, + duration: 2000, + }); + } catch (error) { + console.error('Error updating status:', error); + toast({ + title: "Error", + description: "Failed to update status", + variant: "destructive", + duration: 2000, + }); + } finally { + // Reset updating state after a short delay + updateTimeoutRef.current = setTimeout(() => { + setIsUpdating(false); + }, 500); + } + }; + + return ( + + +
+ {job.status} +
+
+ + {JOB_STATUSES.map((status) => ( + handleStatusChange(status, e)} + className="hover:bg-gray-700 cursor-pointer" + data-no-navigate + > +
+ {status} + + ))} + + + ); +}; + +export default JobStatusDropdown; diff --git a/src/services/dataService.ts b/src/services/dataService.ts index 33539b8..6968b12 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -35,9 +35,18 @@ export class DataService { } private notifyListeners() { - this.listeners.forEach(listener => listener()); + // Debounce notifications to prevent excessive re-renders + if (this.notificationTimeout) { + clearTimeout(this.notificationTimeout); + } + this.notificationTimeout = setTimeout(() => { + this.listeners.forEach(listener => listener()); + this.notificationTimeout = null; + }, 10); } + private notificationTimeout: NodeJS.Timeout | null = null; + getJobs(): IndJob[] { return [...this.jobs]; } @@ -105,16 +114,42 @@ export class DataService { async updateJob(id: string, updates: Partial): Promise { console.log('Updating job:', id, updates); - const updatedRecord = await jobService.updateJob(id, updates); - + const jobIndex = this.jobs.findIndex(job => job.id === id); - if (jobIndex !== -1) { - this.jobs[jobIndex] = updatedRecord; - this.notifyListeners(); - return this.jobs[jobIndex]; + if (jobIndex === -1) { + throw new Error(`Job with id ${id} not found in local state`); } - throw new Error(`Job with id ${id} not found in local state`); + // Optimistic update - immediately update local state (only for simple properties) + const originalJob = { ...this.jobs[jobIndex] }; + + // Only apply optimistic updates for safe properties (not complex relations) + const safeUpdates = Object.fromEntries( + Object.entries(updates).filter(([key]) => + !['billOfMaterials', 'consumedMaterials', 'expenditures', 'income'].includes(key) + ) + ); + + if (Object.keys(safeUpdates).length > 0) { + this.jobs[jobIndex] = { ...this.jobs[jobIndex], ...safeUpdates }; + this.notifyListeners(); + } + + try { + // Update in database + const updatedRecord = await jobService.updateJob(id, updates); + + // Replace with server response + this.jobs[jobIndex] = updatedRecord; + this.notifyListeners(); + + return this.jobs[jobIndex]; + } catch (error) { + // Revert optimistic update on error + this.jobs[jobIndex] = originalJob; + this.notifyListeners(); + throw error; + } } async deleteJob(id: string): Promise { @@ -163,36 +198,43 @@ export class DataService { const job = this.getJob(jobId); if (!job) throw new Error(`Job with id ${jobId} not found`); - const createdTransactions: IndTransactionRecord[] = []; - - // Create all transactions - for (const transaction of transactions) { - transaction.job = jobId; - const createdTransaction = await transactionService.createTransaction(job, transaction); - createdTransactions.push(createdTransaction); - } - - // Update the job's transaction references in one database call - const field = type === 'expenditure' ? 'expenditures' : 'income'; - const currentIds = (job[field] || []).map(tr => tr.id); - const newIds = createdTransactions.map(tr => tr.id); - await jobService.updateJob(jobId, { - [field]: [...currentIds, ...newIds] - }); - - // Fetch fresh job data from the server - const updatedJob = await jobService.getJob(jobId); - if (!updatedJob) throw new Error(`Job with id ${jobId} not found after update`); - - // Update local state with fresh data + // Optimistically update local state first for better UX const jobIndex = this.jobs.findIndex(j => j.id === jobId); - if (jobIndex !== -1) { + if (jobIndex === -1) throw new Error(`Job with id ${jobId} not found`); + + const originalJob = { ...this.jobs[jobIndex] }; + + try { + // Create all transactions in parallel for better performance + const transactionPromises = transactions.map(transaction => { + transaction.job = jobId; + return transactionService.createTransaction(job, transaction); + }); + + const createdTransactions = await Promise.all(transactionPromises); + + // Update the job's transaction references in one database call + const field = type === 'expenditure' ? 'expenditures' : 'income'; + const currentIds = (job[field] || []).map(tr => tr.id); + const newIds = createdTransactions.map(tr => tr.id); + await jobService.updateJob(jobId, { + [field]: [...currentIds, ...newIds] + }); + + // Fetch fresh job data from the server + const updatedJob = await jobService.getJob(jobId); + if (!updatedJob) throw new Error(`Job with id ${jobId} not found after update`); + + // Update local state with fresh data this.jobs[jobIndex] = updatedJob; this.notifyListeners(); return this.jobs[jobIndex]; + } catch (error) { + // Revert optimistic update on error + this.jobs[jobIndex] = originalJob; + this.notifyListeners(); + throw error; } - - throw new Error(`Job with id ${jobId} not found in local state`); } async updateTransaction(jobId: string, transactionId: string, updates: Partial): Promise {