// /home/Autopartes/pos/static/js/sync-engine.js // Nexus POS — Offline sync engine using IndexedDB (function () { 'use strict'; var DB_NAME = 'nexus_pos_offline'; var DB_VERSION = 1; var QUEUE_STORE = 'sync_queue'; var INVENTORY_STORE = 'inventory_cache'; var db = null; // ─── IndexedDB setup ────────────────────────────────────────── function openDB() { return new Promise(function (resolve, reject) { if (db) { resolve(db); return; } var req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = function (e) { var d = e.target.result; if (!d.objectStoreNames.contains(QUEUE_STORE)) { d.createObjectStore(QUEUE_STORE, { keyPath: 'id', autoIncrement: true }); } if (!d.objectStoreNames.contains(INVENTORY_STORE)) { var inv = d.createObjectStore(INVENTORY_STORE, { keyPath: 'item_id' }); inv.createIndex('sku', 'sku', { unique: false }); inv.createIndex('name', 'name', { unique: false }); } }; req.onsuccess = function (e) { db = e.target.result; resolve(db); }; req.onerror = function () { reject(req.error); }; }); } // ─── Queue operations for offline replay ────────────────────── function queueOperation(url, method, body) { return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(QUEUE_STORE, 'readwrite'); tx.objectStore(QUEUE_STORE).add({ url: url, method: method, body: body || null, timestamp: Date.now() }); tx.oncomplete = function () { resolve(); }; tx.onerror = function () { reject(tx.error); }; }); }); } // ─── Process queued operations ──────────────────────────────── function processQueue() { return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(QUEUE_STORE, 'readonly'); var req = tx.objectStore(QUEUE_STORE).getAll(); req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); }).then(function (ops) { if (!ops.length) return Promise.resolve({ synced: 0 }); // Replay in order var chain = Promise.resolve(); var synced = 0; var failed = 0; ops.forEach(function (op) { chain = chain.then(function () { var opts = { method: op.method, headers: { 'Content-Type': 'application/json' } }; if (op.body) opts.body = JSON.stringify(op.body); return fetch(op.url, opts).then(function (resp) { if (resp.ok) { synced++; return removeFromQueue(op.id); } failed++; }).catch(function () { failed++; }); }); }); return chain.then(function () { return { synced: synced, failed: failed, total: ops.length }; }); }); } function removeFromQueue(id) { return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(QUEUE_STORE, 'readwrite'); tx.objectStore(QUEUE_STORE).delete(id); tx.oncomplete = function () { resolve(); }; tx.onerror = function () { reject(tx.error); }; }); }); } function getQueueCount() { return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(QUEUE_STORE, 'readonly'); var req = tx.objectStore(QUEUE_STORE).count(); req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); }); } // ─── Inventory cache ────────────────────────────────────────── function cacheInventory() { return fetch('/pos/api/sync/inventory').then(function (resp) { if (!resp.ok) throw new Error('Sync inventory failed: ' + resp.status); return resp.json(); }).then(function (data) { var items = data.items || []; return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(INVENTORY_STORE, 'readwrite'); var store = tx.objectStore(INVENTORY_STORE); store.clear(); items.forEach(function (item) { store.put(item); }); tx.oncomplete = function () { resolve(items.length); }; tx.onerror = function () { reject(tx.error); }; }); }); }); } function getCachedInventory(query) { return openDB().then(function (d) { return new Promise(function (resolve, reject) { var tx = d.transaction(INVENTORY_STORE, 'readonly'); var req = tx.objectStore(INVENTORY_STORE).getAll(); req.onsuccess = function () { var all = req.result; if (!query) { resolve(all); return; } var q = query.toLowerCase(); var filtered = all.filter(function (item) { return (item.sku && item.sku.toLowerCase().indexOf(q) !== -1) || (item.name && item.name.toLowerCase().indexOf(q) !== -1) || (item.barcode && item.barcode.toLowerCase().indexOf(q) !== -1); }); resolve(filtered); }; req.onerror = function () { reject(req.error); }; }); }); } // ─── Connectivity helpers ───────────────────────────────────── function isOnline() { return navigator.onLine; } // ─── Auto-sync on reconnect ─────────────────────────────────── window.addEventListener('online', function () { console.log('[SyncEngine] Online — processing queue...'); processQueue().then(function (result) { if (result.synced > 0) { console.log('[SyncEngine] Synced ' + result.synced + ' operations'); } }).catch(function (err) { console.error('[SyncEngine] Queue processing error:', err); }); }); window.addEventListener('offline', function () { console.log('[SyncEngine] Offline — operations will be queued'); }); // Listen for SW sync messages if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', function (event) { if (event.data && event.data.type === 'SYNC_REQUESTED') { processQueue(); } }); } // ─── Init DB on load ────────────────────────────────────────── openDB().catch(function (err) { console.error('[SyncEngine] Failed to open IndexedDB:', err); }); // ─── Public API ─────────────────────────────────────────────── window.SyncEngine = { queueOperation: queueOperation, processQueue: processQueue, getQueueCount: getQueueCount, isOnline: isOnline, cacheInventory: cacheInventory, getCachedInventory: getCachedInventory }; })();