/* * user-db.js — IndexedDB storage for the user portal. * * Stores task data (including heavy base64 image dataUrls) in IndexedDB * instead of localStorage to avoid the ~5 MB browser quota. * * IndexedDB provides hundreds of MB of storage (browser-managed, quota-based) * which makes it suitable for image-heavy task data. * * Usage: * import { openUserDB, loadTaskData, saveTaskData, getStorageEstimate } from './user-db.js'; * * await openUserDB(); // Call once on init * const data = await loadTaskData(); // Returns the full taskData object * await saveTaskData(data); // Persist updated taskData * const est = await getStorageEstimate(); // Get usage info */ /* ── Constants ──────────────────────────────────────────────────────────── */ const DB_NAME = 'user-portal-db'; const DB_VERSION = 1; const STORE_TASK_DATA = 'taskData'; /* ── Module-level DB reference ──────────────────────────────────────────── */ let db = null; /* ── Open / create database ─────────────────────────────────────────────── */ /** * Opens the IndexedDB database. Must be called once before any read/write. * Creates the object store on first run or version upgrade. */ export function openUserDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { db = request.result; resolve(db); }; request.onupgradeneeded = () => { const database = request.result; /* Single store keyed by taskId — each entry holds one task's data */ if (!database.objectStoreNames.contains(STORE_TASK_DATA)) { database.createObjectStore(STORE_TASK_DATA, { keyPath: 'taskId' }); } }; }); } /* ── Read all task data ─────────────────────────────────────────────────── */ /** * Loads all task data from IndexedDB and returns it as a plain object * keyed by taskId (same shape as the old localStorage structure). * * @returns {Promise} e.g. { "task-123": { visitDate: "", records: {...} }, ... } */ export function loadTaskData() { return new Promise((resolve, reject) => { if (!db) { resolve({}); return; } const tx = db.transaction(STORE_TASK_DATA, 'readonly'); const store = tx.objectStore(STORE_TASK_DATA); const request = store.getAll(); request.onsuccess = () => { const result = {}; for (const entry of request.result) { const { taskId, ...data } = entry; result[taskId] = data; } resolve(result); }; request.onerror = () => reject(request.error); }); } /* ── Save all task data ─────────────────────────────────────────────────── */ /** * Persists the full taskData object into IndexedDB. * Each taskId becomes a separate record in the store for efficient access. * * @param {Object} taskData - Object keyed by taskId * @returns {Promise} */ export function saveTaskData(taskData) { return new Promise((resolve, reject) => { if (!db) { reject(new Error('Database not open')); return; } const tx = db.transaction(STORE_TASK_DATA, 'readwrite'); const store = tx.objectStore(STORE_TASK_DATA); for (const [taskId, data] of Object.entries(taskData)) { store.put({ taskId, ...data }); } tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } /* ── Save single task entry ─────────────────────────────────────────────── */ /** * Saves or updates a single task's data. More efficient than saving everything * when only one task changed. * * @param {string} taskId * @param {Object} data - The task data (visitDate, records, etc.) * @returns {Promise} */ export function saveOneTaskData(taskId, data) { return new Promise((resolve, reject) => { if (!db) { reject(new Error('Database not open')); return; } const tx = db.transaction(STORE_TASK_DATA, 'readwrite'); const store = tx.objectStore(STORE_TASK_DATA); store.put({ taskId, ...data }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } /* ── Delete a task entry ────────────────────────────────────────────────── */ /** * Removes a single task's data from IndexedDB. * * @param {string} taskId * @returns {Promise} */ export function deleteTaskData(taskId) { return new Promise((resolve, reject) => { if (!db) { reject(new Error('Database not open')); return; } const tx = db.transaction(STORE_TASK_DATA, 'readwrite'); const store = tx.objectStore(STORE_TASK_DATA); store.delete(taskId); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } /* ── Storage estimate ───────────────────────────────────────────────────── */ /** * Returns an estimate of IndexedDB usage (if the StorageManager API is available). * Falls back to counting serialized task data size. * * @param {Object} taskData - Current in-memory taskData for fallback sizing * @returns {Promise<{usedMB: string, quotaMB: string, pct: number}>} */ export async function getStorageEstimate(taskData) { /* Try the modern Storage API (available in secure contexts) */ if (navigator.storage && navigator.storage.estimate) { const est = await navigator.storage.estimate(); const usedMB = ((est.usage || 0) / (1024 * 1024)).toFixed(2); const quotaMB = ((est.quota || 0) / (1024 * 1024)).toFixed(0); const pct = est.quota ? Math.min(100, ((est.usage / est.quota) * 100)) : 0; return { usedMB, quotaMB, pct: Math.round(pct) }; } /* Fallback: estimate from serialized data */ const json = JSON.stringify(taskData || {}); const bytes = json.length * 2; /* UTF-16 */ const usedMB = (bytes / (1024 * 1024)).toFixed(2); return { usedMB, quotaMB: '∞', pct: 0 }; }