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:
120
src/components/JobStatusDropdown.tsx
Normal file
120
src/components/JobStatusDropdown.tsx
Normal 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;
|
@@ -35,9 +35,18 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private notifyListeners() {
|
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[] {
|
getJobs(): IndJob[] {
|
||||||
return [...this.jobs];
|
return [...this.jobs];
|
||||||
}
|
}
|
||||||
@@ -105,16 +114,42 @@ export class DataService {
|
|||||||
|
|
||||||
async updateJob(id: string, updates: Partial<IndJobRecord>): Promise<IndJob> {
|
async updateJob(id: string, updates: Partial<IndJobRecord>): Promise<IndJob> {
|
||||||
console.log('Updating job:', id, updates);
|
console.log('Updating job:', id, updates);
|
||||||
const updatedRecord = await jobService.updateJob(id, updates);
|
|
||||||
|
|
||||||
const jobIndex = this.jobs.findIndex(job => job.id === id);
|
const jobIndex = this.jobs.findIndex(job => job.id === id);
|
||||||
if (jobIndex !== -1) {
|
if (jobIndex === -1) {
|
||||||
this.jobs[jobIndex] = updatedRecord;
|
throw new Error(`Job with id ${id} not found in local state`);
|
||||||
this.notifyListeners();
|
|
||||||
return this.jobs[jobIndex];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
async deleteJob(id: string): Promise<void> {
|
||||||
@@ -163,36 +198,43 @@ export class DataService {
|
|||||||
const job = this.getJob(jobId);
|
const job = this.getJob(jobId);
|
||||||
if (!job) throw new Error(`Job with id ${jobId} not found`);
|
if (!job) throw new Error(`Job with id ${jobId} not found`);
|
||||||
|
|
||||||
const createdTransactions: IndTransactionRecord[] = [];
|
// Optimistically update local state first for better UX
|
||||||
|
|
||||||
// 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
|
|
||||||
const jobIndex = this.jobs.findIndex(j => j.id === jobId);
|
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.jobs[jobIndex] = updatedJob;
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
return this.jobs[jobIndex];
|
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> {
|
async updateTransaction(jobId: string, transactionId: string, updates: Partial<IndTransactionRecord>): Promise<IndJob> {
|
||||||
|
Reference in New Issue
Block a user