// PERMANENT: IndexedDB cache for jobs data // Provides lightning-fast job listing with zero-delay loading // // Strategy: // - Display cached jobs instantly on page load (0ms delay) // - Fetch fresh data in background after displaying cache // - Cache TTL: 5 minutes (configurable) // - Stores minimal fields for card display only // // Fields cached: Job_Number, Job_Name, Division, Active, Status, Estimator // // Usage: // await jobCache.initialize(); // const jobs = await jobCache.getCachedJobs(); // Instant // await jobCache.refreshJobs(); // Background refresh export interface JobCard { Job_Number: string; Job_Name: string; Division: string; Active: string; Status: string; Estimator: string; } interface CacheMetadata { lastUpdated: number; version: number; } const DB_NAME = 'JobInfoCache'; const DB_VERSION = 2; // bump to invalidate old minimal-field cache const JOBS_STORE = 'jobs'; const META_STORE = 'metadata'; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes const PAGE_SIZE = 500; // match Mode1 bulk fetch class JobCache { private db: IDBDatabase | null = null; async initialize(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; // Create jobs store with Job_Number as key if (!db.objectStoreNames.contains(JOBS_STORE)) { db.createObjectStore(JOBS_STORE, { keyPath: 'Job_Number' }); } // Create metadata store if (!db.objectStoreNames.contains(META_STORE)) { db.createObjectStore(META_STORE); } }; }); } async getCachedJobs(): Promise { if (!this.db) throw new Error('Database not initialized'); return new Promise((resolve, reject) => { const transaction = this.db!.transaction([JOBS_STORE], 'readonly'); const store = transaction.objectStore(JOBS_STORE); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async isCacheValid(): Promise { if (!this.db) return false; try { const metadata = await this.getMetadata(); if (!metadata || !metadata.lastUpdated) return false; const age = Date.now() - metadata.lastUpdated; return age < CACHE_TTL; } catch { return false; } } async saveJobs(jobs: JobCard[]): Promise { if (!this.db) throw new Error('Database not initialized'); return new Promise((resolve, reject) => { const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite'); const jobsStore = transaction.objectStore(JOBS_STORE); const metaStore = transaction.objectStore(META_STORE); // Clear existing jobs jobsStore.clear(); // Save new jobs jobs.forEach(job => jobsStore.add(job)); // Update metadata const metadata: CacheMetadata = { lastUpdated: Date.now(), version: DB_VERSION }; metaStore.put(metadata, 'main'); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async refreshJobs(): Promise { try { // Fetch fresh data from backend with pagination to mirror Mode1 behavior let page = 1; let all: JobCard[] = []; let totalPages = 1; while (page <= totalPages) { const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch jobs (status ${response.status})`); } const data = await response.json(); const items: JobCard[] = data.items || []; const reportedTotalPages = data.totalPages || 1; all = all.concat(items); totalPages = Math.max(totalPages, reportedTotalPages); page += 1; } // Save to cache await this.saveJobs(all); return all; } catch (err) { console.error('Failed to refresh jobs cache:', err); throw err; } } async getMetadata(): Promise { if (!this.db) return null; return new Promise((resolve, reject) => { const transaction = this.db!.transaction([META_STORE], 'readonly'); const store = transaction.objectStore(META_STORE); const request = store.get('main'); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } async clearCache(): Promise { if (!this.db) throw new Error('Database not initialized'); return new Promise((resolve, reject) => { const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite'); transaction.objectStore(JOBS_STORE).clear(); transaction.objectStore(META_STORE).clear(); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async getCacheAge(): Promise { const metadata = await this.getMetadata(); if (!metadata || !metadata.lastUpdated) return null; return Date.now() - metadata.lastUpdated; } } export const jobCache = new JobCache();