/** * IndexedDB setup using Dexie.js for workorder offline mode * * Schema mirrors the backend models: * - WorkorderModel * - WorkorderDocumentationModel * - WorkorderJournalModel * - WorkorderTenantConfigModel */ import Dexie from 'https://cdn.jsdelivr.net/npm/dexie@4/dist/dexie.min.mjs'; // Database instance export const db = new Dexie('xinon-workorder-offline'); // Schema definition db.version(1).stores({ // Workorder list and basic data // Mirrors WorkorderModel from backend workorders: 'id, companyId, status, [companyId+status], _syncStatus, _lastModified', // Full workorder details (lazy-loaded) // Contains customer info, address, etc. workorderDetails: 'workorderId, lastFetched', // Documentation metadata with thumbnail references // Mirrors WorkorderDocumentationModel documentation: 'id, workorderId, documentType, _syncStatus', // Documentation thumbnails (blob storage) thumbnails: 'documentationId, workorderId', // Journal entries (audit trail) // Mirrors WorkorderJournalModel journals: 'id, workorderId, create, _localId', // Tenant configurations // Mirrors WorkorderTenantConfigModel tenantConfigs: 'addressId, lastFetched', // Sync queue for pending mutations syncQueue: '++localId, operation, workorderId, status, createdAt', // Pending file uploads pendingFiles: '++localId, workorderId, documentType, status', // Sync metadata (lastSync, etc.) syncMeta: 'key' }); // Sync status enum export const SyncStatus = { SYNCED: 'synced', PENDING: 'pending', CONFLICT: 'conflict', ERROR: 'error' }; // Operation types for sync queue export const OperationType = { ADD_JOURNAL: 'addJournal', UPDATE_NOTES: 'updateNotes', SCHEDULE_APPOINTMENT: 'scheduleAppointment', REQUEST_INTERVENTION: 'requestIntervention', UPDATE_CABLE_DATA: 'updateCableData', UPLOAD_DOCUMENTATION: 'uploadDocumentation', COMPLETE_WORKORDER: 'completeWorkorder' }; // Operation priority (lower = higher priority) export const OperationPriority = { [OperationType.SCHEDULE_APPOINTMENT]: 1, [OperationType.UPDATE_NOTES]: 2, [OperationType.ADD_JOURNAL]: 3, [OperationType.UPDATE_CABLE_DATA]: 4, [OperationType.UPLOAD_DOCUMENTATION]: 5, [OperationType.REQUEST_INTERVENTION]: 6, [OperationType.COMPLETE_WORKORDER]: 7 // Must be last }; /** * Initialize database and return instance */ export async function initDatabase() { try { await db.open(); console.log('[DB] Database opened successfully'); return db; } catch (error) { console.error('[DB] Failed to open database:', error); throw error; } } /** * Clear all offline data (for logout or cache clear) */ export async function clearAllData() { try { await db.transaction('rw', db.workorders, db.workorderDetails, db.documentation, db.thumbnails, db.journals, db.tenantConfigs, db.syncQueue, db.pendingFiles, db.syncMeta, async () => { await db.workorders.clear(); await db.workorderDetails.clear(); await db.documentation.clear(); await db.thumbnails.clear(); await db.journals.clear(); await db.tenantConfigs.clear(); await db.syncQueue.clear(); await db.pendingFiles.clear(); await db.syncMeta.clear(); } ); console.log('[DB] All offline data cleared'); } catch (error) { console.error('[DB] Failed to clear data:', error); throw error; } } /** * Get sync metadata value */ export async function getSyncMeta(key) { const meta = await db.syncMeta.get(key); return meta?.value ?? null; } /** * Set sync metadata value */ export async function setSyncMeta(key, value) { await db.syncMeta.put({ key, value }); } /** * Get pending operations count */ export async function getPendingCount() { const queueCount = await db.syncQueue.where('status').equals('pending').count(); const filesCount = await db.pendingFiles.where('status').equals('pending').count(); return queueCount + filesCount; } /** * Check if database has any cached workorders */ export async function hasOfflineData() { const count = await db.workorders.count(); return count > 0; } /** * Get storage usage estimate */ export async function getStorageEstimate() { if (navigator.storage && navigator.storage.estimate) { const estimate = await navigator.storage.estimate(); return { usage: estimate.usage || 0, quota: estimate.quota || 0, usagePercent: estimate.quota ? Math.round((estimate.usage / estimate.quota) * 100) : 0 }; } return { usage: 0, quota: 0, usagePercent: 0 }; } /** * Request persistent storage (important for iOS Safari) */ export async function requestPersistentStorage() { if (navigator.storage && navigator.storage.persist) { const isPersisted = await navigator.storage.persist(); console.log('[DB] Persistent storage:', isPersisted ? 'granted' : 'denied'); return isPersisted; } return false; } export default db;