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.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-28 19:03:43 +00:00
committed by PhatPhuckDave
parent 57ae872cc6
commit 7eb957f66a
2 changed files with 195 additions and 33 deletions

View File

@@ -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<JobStatusDropdownProps> = ({ job }) => {
const { updateJob } = useJobs();
const { toast } = useToast();
const [isUpdating, setIsUpdating] = useState(false);
const updateTimeoutRef = useRef<NodeJS.Timeout | null>(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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={`${getStatusColor(job.status)} text-white px-3 py-1 rounded-sm text-xs font-semibold cursor-pointer hover:opacity-80 transition-opacity`}
data-no-navigate
>
{job.status}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-gray-800/50 border-gray-600 text-white">
{JOB_STATUSES.map((status) => (
<DropdownMenuItem
key={status}
onClick={(e) => handleStatusChange(status, e)}
className="hover:bg-gray-700 cursor-pointer"
data-no-navigate
>
<div className={`w-3 h-3 rounded-sm ${getStatusColor(status)} mr-2`} />
{status}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
export default JobStatusDropdown;

View File

@@ -35,8 +35,17 @@ 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<IndJobRecord>): Promise<IndJob> {
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<void> {
@@ -163,14 +198,20 @@ export class DataService {
const job = this.getJob(jobId);
if (!job) throw new Error(`Job with id ${jobId} not found`);
const createdTransactions: IndTransactionRecord[] = [];
// Optimistically update local state first for better UX
const jobIndex = this.jobs.findIndex(j => j.id === jobId);
if (jobIndex === -1) throw new Error(`Job with id ${jobId} not found`);
// Create all transactions
for (const transaction of transactions) {
const originalJob = { ...this.jobs[jobIndex] };
try {
// Create all transactions in parallel for better performance
const transactionPromises = transactions.map(transaction => {
transaction.job = jobId;
const createdTransaction = await transactionService.createTransaction(job, transaction);
createdTransactions.push(createdTransaction);
}
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';
@@ -185,14 +226,15 @@ export class DataService {
if (!updatedJob) throw new Error(`Job with id ${jobId} not found after update`);
// Update local state with fresh data
const jobIndex = this.jobs.findIndex(j => j.id === jobId);
if (jobIndex !== -1) {
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<IndTransactionRecord>): Promise<IndJob> {