diff --git a/src/components/JobGroup.tsx b/src/components/JobGroup.tsx index 1bc9de1..f89bbb0 100644 --- a/src/components/JobGroup.tsx +++ b/src/components/JobGroup.tsx @@ -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 = ({ @@ -24,7 +26,8 @@ const JobGroup: React.FC = ({ onDelete, onUpdateProduced, onImportBOM, - isTracked = false + isTracked = false, + isLoading = false }) => { return (
@@ -36,6 +39,7 @@ const JobGroup: React.FC = ({

{status} ({jobs.length} jobs) + {isLoading && }

⌄ @@ -45,17 +49,24 @@ const JobGroup: React.FC = ({ {!isCollapsed && (
- {jobs.map(job => ( - - ))} + {isLoading ? ( +
+ + Loading jobs... +
+ ) : ( + jobs.map(job => ( + + )) + )}
)}
diff --git a/src/hooks/useDataService.ts b/src/hooks/useDataService.ts index 0237497..0a8eef7 100644 --- a/src/hooks/useDataService.ts +++ b/src/hooks/useDataService.ts @@ -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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [loadingStatuses, setLoadingStatuses] = useState>(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, diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 93bf28a..72d0b41 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -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(0); + const containerRef = useRef(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 (
@@ -170,13 +185,22 @@ const Index = () => { return groups; }, {} as Record); - 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 ( -
+
{ @@ -323,6 +347,7 @@ const Index = () => { onDelete={handleDeleteJob} onUpdateProduced={handleUpdateProduced} onImportBOM={handleImportBOM} + isLoading={loadingStatuses.has(status)} /> ))}
@@ -340,6 +365,7 @@ const Index = () => { onUpdateProduced={handleUpdateProduced} onImportBOM={handleImportBOM} isTracked={true} + isLoading={loadingStatuses.has('Tracked')} />
)} diff --git a/src/services/dataService.ts b/src/services/dataService.ts index e3698cd..ebc1866 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -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 | null = null; private initialized: Promise; + private loadedStatuses: Set = 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;