/** * Offline Support Utilities * Provides IndexedDB storage and background sync for offline functionality */ import { openDB, DBSchema, IDBPDatabase } from 'idb'; // Database schema interface LocalGreenChainDB extends DBSchema { 'pending-plants': { key: string; value: { id: string; data: any; createdAt: string; attempts: number; }; indexes: { 'by-created': string }; }; 'pending-transport': { key: string; value: { id: string; data: any; createdAt: string; attempts: number; }; indexes: { 'by-created': string }; }; 'cached-plants': { key: string; value: { id: string; data: any; cachedAt: string; }; indexes: { 'by-cached': string }; }; 'user-preferences': { key: string; value: any; }; } const DB_NAME = 'localgreenchain-offline'; const DB_VERSION = 1; let dbPromise: Promise> | null = null; async function getDB(): Promise> { if (!dbPromise) { dbPromise = openDB(DB_NAME, DB_VERSION, { upgrade(db) { // Pending plants store if (!db.objectStoreNames.contains('pending-plants')) { const plantStore = db.createObjectStore('pending-plants', { keyPath: 'id' }); plantStore.createIndex('by-created', 'createdAt'); } // Pending transport store if (!db.objectStoreNames.contains('pending-transport')) { const transportStore = db.createObjectStore('pending-transport', { keyPath: 'id' }); transportStore.createIndex('by-created', 'createdAt'); } // Cached plants store if (!db.objectStoreNames.contains('cached-plants')) { const cacheStore = db.createObjectStore('cached-plants', { keyPath: 'id' }); cacheStore.createIndex('by-cached', 'cachedAt'); } // User preferences store if (!db.objectStoreNames.contains('user-preferences')) { db.createObjectStore('user-preferences'); } }, }); } return dbPromise; } // Network status export function isOnline(): boolean { return typeof navigator !== 'undefined' ? navigator.onLine : true; } export function onNetworkChange(callback: (online: boolean) => void): () => void { if (typeof window === 'undefined') return () => {}; const handleOnline = () => callback(true); const handleOffline = () => callback(false); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; } // Pending operations export async function queuePlantRegistration(plantData: any): Promise { const db = await getDB(); const id = generateId(); await db.put('pending-plants', { id, data: plantData, createdAt: new Date().toISOString(), attempts: 0, }); // Register for background sync if available if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) { const registration = await navigator.serviceWorker.ready; await (registration as any).sync.register('sync-plants'); } return id; } export async function queueTransportEvent(eventData: any): Promise { const db = await getDB(); const id = generateId(); await db.put('pending-transport', { id, data: eventData, createdAt: new Date().toISOString(), attempts: 0, }); // Register for background sync if available if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) { const registration = await navigator.serviceWorker.ready; await (registration as any).sync.register('sync-transport'); } return id; } export async function getPendingPlants(): Promise { const db = await getDB(); return db.getAll('pending-plants'); } export async function getPendingTransport(): Promise { const db = await getDB(); return db.getAll('pending-transport'); } export async function removePendingPlant(id: string): Promise { const db = await getDB(); await db.delete('pending-plants', id); } export async function removePendingTransport(id: string): Promise { const db = await getDB(); await db.delete('pending-transport', id); } // Plant caching export async function cachePlant(plant: any): Promise { const db = await getDB(); await db.put('cached-plants', { id: plant.id, data: plant, cachedAt: new Date().toISOString(), }); } export async function getCachedPlant(id: string): Promise { const db = await getDB(); const cached = await db.get('cached-plants', id); return cached?.data || null; } export async function getCachedPlants(): Promise { const db = await getDB(); const all = await db.getAll('cached-plants'); return all.map((item) => item.data); } export async function clearCachedPlants(): Promise { const db = await getDB(); await db.clear('cached-plants'); } // User preferences export async function setPreference(key: string, value: any): Promise { const db = await getDB(); await db.put('user-preferences', value, key); } export async function getPreference(key: string): Promise { const db = await getDB(); return db.get('user-preferences', key) as Promise; } // Sync operations export async function syncPendingPlants(): Promise<{ success: number; failed: number }> { const pending = await getPendingPlants(); let success = 0; let failed = 0; for (const item of pending) { try { const response = await fetch('/api/plants/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item.data), }); if (response.ok) { await removePendingPlant(item.id); success++; } else { failed++; } } catch { failed++; } } return { success, failed }; } export async function syncPendingTransport(): Promise<{ success: number; failed: number }> { const pending = await getPendingTransport(); let success = 0; let failed = 0; for (const item of pending) { try { const response = await fetch('/api/transport/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item.data), }); if (response.ok) { await removePendingTransport(item.id); success++; } else { failed++; } } catch { failed++; } } return { success, failed }; } export async function syncAll(): Promise { if (!isOnline()) return; await Promise.all([ syncPendingPlants(), syncPendingTransport(), ]); } // Utility function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } // Auto-sync when coming online if (typeof window !== 'undefined') { window.addEventListener('online', () => { syncAll().catch(console.error); }); }