Fix jarring job loading and scrolling

Improve job loading to ensure total revenue is accurate and prevent page refresh/scroll to top when expanding job categories.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-09 13:04:27 +00:00
committed by PhatPhuckDave
parent 6ce39c89d0
commit 8b431eaeca
4 changed files with 106 additions and 30 deletions

View File

@@ -2,6 +2,7 @@
import { IndJob } from '@/lib/types';
import { getStatusColor } from '@/utils/jobStatusUtils';
import JobCard from './JobCard';
import { Loader2 } from 'lucide-react';
interface JobGroupProps {
status: string;
@@ -13,6 +14,7 @@ interface JobGroupProps {
onUpdateProduced?: (jobId: string, produced: number) => void;
onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void;
isTracked?: boolean;
isLoading?: boolean;
}
const JobGroup: React.FC<JobGroupProps> = ({
@@ -24,7 +26,8 @@ const JobGroup: React.FC<JobGroupProps> = ({
onDelete,
onUpdateProduced,
onImportBOM,
isTracked = false
isTracked = false,
isLoading = false
}) => {
return (
<div className="space-y-4">
@@ -36,6 +39,7 @@ const JobGroup: React.FC<JobGroupProps> = ({
<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'}`}>
@@ -45,17 +49,24 @@ const JobGroup: React.FC<JobGroupProps> = ({
{!isCollapsed && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{jobs.map(job => (
<JobCard
key={job.id}
job={job}
onEdit={onEdit}
onDelete={onDelete}
onUpdateProduced={onUpdateProduced}
onImportBOM={onImportBOM}
isTracked={isTracked}
/>
))}
{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 => (
<JobCard
key={job.id}
job={job}
onEdit={onEdit}
onDelete={onDelete}
onUpdateProduced={onUpdateProduced}
onImportBOM={onImportBOM}
isTracked={isTracked}
/>
))
)}
</div>
)}
</div>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { dataService } from '@/services/dataService';
import { IndJob } from '@/lib/types';
@@ -6,17 +7,21 @@ export function useJobs() {
const [jobs, setJobs] = useState<IndJob[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [loadingStatuses, setLoadingStatuses] = useState<Set<string>>(new Set());
const initialLoadComplete = useRef(false);
useEffect(() => {
let mounted = true;
const loadJobs = async (visibleStatuses?: string[]) => {
const loadJobs = async () => {
try {
setLoading(true);
const loadedJobs = await dataService.loadJobs(visibleStatuses);
// Load all jobs initially to get accurate totals
const loadedJobs = await dataService.loadJobs();
if (mounted) {
setJobs(loadedJobs);
setError(null);
initialLoadComplete.current = true;
}
} catch (err) {
if (mounted) {
@@ -62,22 +67,38 @@ export function useJobs() {
const createMultipleBillItems = useCallback(dataService.createMultipleBillItems.bind(dataService), []);
const loadJobsForStatuses = useCallback(async (visibleStatuses: string[]) => {
// Prevent multiple concurrent loads of the same status
const statusesToLoad = visibleStatuses.filter(status => !loadingStatuses.has(status));
if (statusesToLoad.length === 0) return;
// Mark statuses as loading
setLoadingStatuses(prev => {
const newSet = new Set(prev);
statusesToLoad.forEach(status => newSet.add(status));
return newSet;
});
try {
setLoading(true);
// Load jobs for specific statuses without showing global loading
const loadedJobs = await dataService.loadJobs(visibleStatuses);
setJobs(loadedJobs);
setError(null);
// Jobs will be updated via the subscription, no need to manually update state here
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load jobs');
} finally {
setLoading(false);
// Remove statuses from loading set
setLoadingStatuses(prev => {
const newSet = new Set(prev);
statusesToLoad.forEach(status => newSet.delete(status));
return newSet;
});
}
}, []);
}, [loadingStatuses]);
return {
jobs,
loading,
error,
loadingStatuses,
createJob,
updateJob,
deleteJob,

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -23,6 +23,7 @@ const Index = () => {
jobs,
loading,
error,
loadingStatuses,
createJob,
updateJob,
deleteJob,
@@ -43,6 +44,10 @@ const Index = () => {
return saved ? JSON.parse(saved) : {};
});
// Track scroll position to prevent jarring jumps
const scrollPositionRef = useRef<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
@@ -55,6 +60,16 @@ const Index = () => {
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Save scroll position before updates
useEffect(() => {
const handleScroll = () => {
scrollPositionRef.current = window.scrollY;
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
if (loading) {
return (
<div className="min-h-screen bg-gray-950 p-6 flex items-center justify-center">
@@ -170,13 +185,22 @@ const Index = () => {
return groups;
}, {} as Record<string, IndJob[]>);
const toggleGroup = (status: string) => {
const toggleGroup = async (status: string) => {
// Save current scroll position
const currentScrollY = window.scrollY;
const newState = { ...collapsedGroups, [status]: !collapsedGroups[status] };
setCollapsedGroups(newState);
localStorage.setItem('jobGroupsCollapsed', JSON.stringify(newState));
if (collapsedGroups[status]) {
loadJobsForStatuses([status]);
// If expanding and not currently loading this status
if (collapsedGroups[status] && !loadingStatuses.has(status)) {
await loadJobsForStatuses([status]);
// Restore scroll position after a brief delay to allow for rendering
setTimeout(() => {
window.scrollTo(0, currentScrollY);
}, 50);
}
};
@@ -208,7 +232,7 @@ const Index = () => {
}
return (
<div className="container mx-auto p-4 space-y-4">
<div ref={containerRef} className="container mx-auto p-4 space-y-4">
<SearchOverlay
isOpen={searchOpen}
onClose={() => {
@@ -323,6 +347,7 @@ const Index = () => {
onDelete={handleDeleteJob}
onUpdateProduced={handleUpdateProduced}
onImportBOM={handleImportBOM}
isLoading={loadingStatuses.has(status)}
/>
))}
</div>
@@ -340,6 +365,7 @@ const Index = () => {
onUpdateProduced={handleUpdateProduced}
onImportBOM={handleImportBOM}
isTracked={true}
isLoading={loadingStatuses.has('Tracked')}
/>
</div>
)}

View File

@@ -1,3 +1,4 @@
import { IndJob } from '@/lib/types';
import { IndJobRecord, IndJobRecordNoId, IndTransactionRecord, IndTransactionRecordNoId, IndBillitemRecord, IndBillitemRecordNoId } from '@/lib/pbtypes';
import * as jobService from './jobService';
@@ -11,6 +12,7 @@ export class DataService {
private listeners: Set<() => void> = new Set();
private loadPromise: Promise<IndJob[]> | null = null;
private initialized: Promise<void>;
private loadedStatuses: Set<string> = new Set();
private constructor() {
// Initialize with admin login
@@ -53,22 +55,38 @@ export class DataService {
return this.loadPromise;
}
// If we already have jobs loaded and no specific statuses requested, return them immediately
// If we already have all jobs loaded and no specific statuses requested, return them immediately
if (this.jobs.length > 0 && !visibleStatuses) {
return Promise.resolve(this.getJobs());
}
// If requesting specific statuses that are already loaded, return current jobs
if (visibleStatuses && visibleStatuses.every(status => this.loadedStatuses.has(status))) {
return Promise.resolve(this.getJobs());
}
// Start a new load
console.log('Loading jobs from database', visibleStatuses ? `for statuses: ${visibleStatuses.join(', ')}` : '');
this.loadPromise = jobService.getJobs(visibleStatuses).then(jobs => {
if (visibleStatuses) {
// If filtering by statuses, merge with existing jobs
const existingJobs = this.jobs.filter(job => !visibleStatuses.includes(job.status));
this.jobs = [...existingJobs, ...jobs];
// Mark these statuses as loaded
visibleStatuses.forEach(status => this.loadedStatuses.add(status));
// Merge with existing jobs, replacing jobs with same IDs
const existingJobIds = new Set(jobs.map(job => job.id));
const otherJobs = this.jobs.filter(job => !existingJobIds.has(job.id));
this.jobs = [...otherJobs, ...jobs];
} else {
// Loading all jobs
this.jobs = jobs;
// Mark all unique statuses as loaded
const allStatuses = new Set(jobs.map(job => job.status));
allStatuses.forEach(status => this.loadedStatuses.add(status));
}
this.notifyListeners();
// Use setTimeout to defer the notification to prevent immediate re-renders
setTimeout(() => this.notifyListeners(), 0);
return this.getJobs();
}).finally(() => {
this.loadPromise = null;