FASE 7d: Lazy Loading + Minificación + Auto-serve minified

Cambios implementados:

1. Lazy loading de imágenes:
   - catalog.js: loading="lazy" decoding="async" en part cards y detail panel
   - inventory.js: lazy loading en imagen de detalle de item

2. Minificación de assets:
   - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard
   - 25 archivos .min.js + 5 .min.css generados en pos/static/
   - 14 archivos .min.js + 8 .min.css generados en dashboard/

3. Nginx auto-serve minified:
   - try_files $1.min.js antes de servir .js original
   - try_files $1.min.css antes de servir .css original
   - Transparente para los templates HTML (cero cambios en HTML)

4. Cache warming script:
   - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches
   - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas
   - Corre en background, procesa ~1.5M parts

Tests: 73/73 pasando
This commit is contained in:
2026-04-27 08:34:24 +00:00
parent e21722a3a9
commit 21959f1b37
56 changed files with 5629 additions and 4 deletions

1
pos/static/js/accounting.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/app-init.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e=localStorage.getItem("pos_token");if(e){try{var t=JSON.parse(atob(e.split(".")[1]));if(1e3*t.exp<Date.now())return localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),void(window.location.href="/pos/login")}catch(e){return localStorage.removeItem("pos_token"),void(window.location.href="/pos/login")}var o={};try{o=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var n=o.name||t.name||"Usuario",a=o.role||t.role||"",r="function"==typeof window.t?window.t:function(e){return e},i={owner:r("role_owner"),admin:r("role_admin"),cashier:r("role_cashier"),warehouse:r("role_warehouse"),accountant:r("role_accountant")}[a]||a,c=n.split(" ").map((function(e){return e[0]})).join("").toUpperCase().substring(0,2);document.querySelectorAll(".sidebar__user-name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".sidebar__user-role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".sidebar__user-avatar, .sidebar__avatar").forEach((function(e){e.textContent=c})),document.querySelectorAll(".profile-info__name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".profile-info__role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".theme-bar__label").forEach((function(e){-1===e.textContent.indexOf("Usuario:")&&-1===e.textContent.indexOf("Sucursal")||(e.textContent="Sucursal Principal — "+n)})),document.querySelectorAll(".status-bar .user-name, .status-info span").forEach((function(e){var t=e.textContent;["Hugo M.","Hugo García","J. Ramírez","José Ramírez","Carlos M.","Admin"].forEach((function(o){-1!==t.indexOf(o)&&(e.textContent=t.replace(o,n))}))}));var l=window.location.pathname;document.querySelectorAll(".nav-item, .nav-link").forEach((function(e){e.classList.remove("is-active","active"),(e.getAttribute("href")||"")===l&&(e.classList.add("is-active"),e.classList.add("active"))})),window.posLogout=function(){localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),localStorage.removeItem("pos_tenant_id"),localStorage.removeItem("pos_cart"),window.location.href="/pos/login"},document.querySelectorAll('[data-action="logout"], .btn-logout, .logout-btn').forEach((function(e){e.addEventListener("click",(function(e){e.preventDefault(),posLogout()}))}));var s=localStorage.getItem("pos_theme");s||(s=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"industrial":"modern"),document.documentElement.setAttribute("data-theme",s),document.querySelectorAll(".theme-bar").forEach((function(e){e.style.display="none"})),window.posSetTheme=function(e){document.documentElement.setAttribute("data-theme",e),localStorage.setItem("pos_theme",e)},window.setTheme=window.posSetTheme,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",(function(e){if(!localStorage.getItem("pos_theme")){var t=e.matches?"industrial":"modern";document.documentElement.setAttribute("data-theme",t)}})),setTimeout((function(){document.documentElement.setAttribute("data-theme",s)}),100),window.POS_USER={name:n,role:a,roleLabel:i,initials:c,token:e,tenantId:t.tenant_id,employeeId:t.employee_id,branchId:t.branch_id,permissions:t.permissions||[]}}else window.location.href="/pos/login"}();

View File

@@ -893,7 +893,7 @@
}
var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">'
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>';
var tierClass = p.priority_tier === 1 ? ' part-card--tier1' : (p.priority_tier === 2 ? ' part-card--tier2' : '');
@@ -997,7 +997,7 @@
: '';
var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">'
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>';
// Local-mode extras: manufacturer badge + priority tier indicator
@@ -1125,7 +1125,7 @@
html += '<div class="detail-oem">' + esc(p.oem_part_number) + '</div>';
html += '<div class="detail-name">' + esc(p.name) + '</div>';
if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>';
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
html += '</div>';
// Local stock

1
pos/static/js/catalog.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/chat.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/config.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/customers.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/dashboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/diagrams.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/fleet.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/i18n.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -549,7 +549,7 @@
// Product image section
html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
if (data.image_url) {
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" loading="lazy" decoding="async" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
html += '<div style="display:flex;gap:8px;justify-content:center;">';
html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>';
html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>';

1
pos/static/js/inventory.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/invoicing.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/kiosk.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e="pos_kiosk_mode";function n(){return window.matchMedia("(display-mode: standalone)").matches||!0===window.navigator.standalone}function t(){return void 0!==window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform()}function o(){var o=localStorage.getItem(e);return"true"===o||"false"!==o&&(n()||t())}var a=!1;function i(){if(!a){var e=document.documentElement,n=e.requestFullscreen||e.webkitRequestFullscreen||e.mozRequestFullScreen||e.msRequestFullscreen;n&&(n.call(e).catch((function(){})),a=!0)}}var c=null;async function r(){if("wakeLock"in navigator)try{(c=await navigator.wakeLock.request("screen")).addEventListener("release",(function(){c=null}))}catch(e){}}function u(){window.addEventListener("beforeunload",(function(e){o()&&(e.preventDefault(),e.returnValue="")})),document.addEventListener("contextmenu",(function(e){o()&&e.preventDefault()}));var e=["click","touchstart","keydown"];function n(){o()&&(i(),r()),e.forEach((function(e){document.removeEventListener(e,n)}))}e.forEach((function(e){document.addEventListener(e,n,{once:!1})})),function(){var e=localStorage.getItem("pos_token");if(e&&-1!==window.location.pathname.indexOf("/pos/login"))try{var n=e.split(".");if(3!==n.length)return;var t=JSON.parse(atob(n[1].replace(/-/g,"+").replace(/_/g,"/"))).exp;t&&1e3*t>Date.now()&&(window.location.href="/pos/")}catch(e){}}()}document.addEventListener("visibilitychange",(function(){"visible"===document.visibilityState&&o()&&!c&&r()})),window.NexusKiosk={isEnabled:o,isPWA:n,isCapacitor:t,enable:function(){localStorage.setItem(e,"true"),i(),r()},disable:function(){localStorage.setItem(e,"false"),c&&(c.release(),c=null),document.fullscreenElement&&document.exitFullscreen().catch((function(){}))},toggle:function(){return o()?window.NexusKiosk.disable():window.NexusKiosk.enable(),o()}},o()&&u(),window.addEventListener("storage",(function(n){n.key===e&&"true"===n.newValue&&u()}))}();

1
pos/static/js/login.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var t="",e=document.querySelectorAll("#pinDots .pin-dot"),n=document.getElementById("loginError"),o=new URLSearchParams(window.location.search).get("tenant")||localStorage.getItem("pos_tenant_id"),a=localStorage.getItem("pos_device_id");function i(){e.forEach((function(e,n){e.classList.toggle("filled",n<t.length)}))}a||(a="dev-"+Date.now()+"-"+Math.random().toString(36).substr(2,9),localStorage.setItem("pos_device_id",a)),window.addDigit=function(e){t.length>=4||(t+=e,i(),n.textContent="",4===t.length&&submitPin())},window.clearPin=function(){t="",i(),n.textContent=""},window.submitPin=function(){4===t.length&&(n.textContent="",fetch("/pos/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenant_id:parseInt(o),pin:t,device_id:a})}).then((function(t){return t.json().then((function(e){return{ok:t.ok,data:e}}))})).then((function(t){if(!t.ok)return n.textContent=t.data.error||"Error de autenticacion",void clearPin();localStorage.setItem("pos_token",t.data.token),localStorage.setItem("pos_employee",JSON.stringify(t.data.employee)),localStorage.setItem("pos_tenant_id",o),window.location.href="/pos/catalog"})).catch((function(){n.textContent="Error de conexion",clearPin()})))},document.addEventListener("keydown",(function(t){t.key>="0"&&t.key<="9"?addDigit(t.key):"Backspace"===t.key?clearPin():"Enter"===t.key&&submitPin()}));var r=localStorage.getItem("pos_token");r&&o&&(!function(t){try{var e=t.split(".");if(3!==e.length)return!1;var n=e[1].replace(/-/g,"+").replace(/_/g,"/"),o=JSON.parse(atob(n));return!!o.exp&&1e3*o.exp>Date.now()+3e4}catch(t){return!1}}(r)?(localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee")):window.location.href="/pos/catalog")}();

1
pos/static/js/native-bridge.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";window.NexusNative={isNative:"undefined"!=typeof Capacitor,_scanStream:null,_scanVideo:null,async scanBarcode(){if(this.isNative)try{const{Camera:e}=await import("@capacitor/camera");return await e.getPhoto({quality:90,resultType:"base64"})}catch(e){return null}return"BarcodeDetector"in window?new Promise((async e=>{try{const t=await navigator.mediaDevices.getUserMedia({video:{facingMode:"environment",width:{ideal:1280},height:{ideal:720}}});this._scanStream=t;const a=document.createElement("div");a.id="barcode-scan-overlay",a.style.cssText="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;";const i=document.createElement("video");i.autoplay=!0,i.playsInline=!0,i.style.cssText="width:90%;max-width:500px;border-radius:12px;border:3px solid #F5A623;",i.srcObject=t,this._scanVideo=i;const o=document.createElement("p");o.textContent="Apunta al codigo de barras...",o.style.cssText="color:#fff;font-size:16px;margin-top:16px;font-family:sans-serif;";const s=document.createElement("button");s.textContent="Cancelar",s.style.cssText="margin-top:16px;padding:10px 24px;background:#F5A623;color:#000;border:none;border-radius:6px;font-size:15px;cursor:pointer;font-weight:bold;",s.onclick=()=>{this.stopScan(),a.remove(),e(null)},a.appendChild(i),a.appendChild(o),a.appendChild(s),document.body.appendChild(a);const n=new BarcodeDetector({formats:["ean_13","ean_8","code_128","code_39","qr_code","upc_a","upc_e"]}),r=async()=>{if(this._scanStream){try{const t=await n.detect(i);if(t.length>0){const i=t[0].rawValue;return o.textContent="Codigo detectado: "+i,o.style.color="#4CAF50",void setTimeout((()=>{this.stopScan(),a.remove(),e(i)}),400)}}catch(e){}requestAnimationFrame(r)}};i.onloadedmetadata=()=>{i.play(),requestAnimationFrame(r)}}catch(t){console.error("Camera access error:",t),alert("No se pudo acceder a la camara: "+t.message),e(null)}})):(alert("Tu navegador no soporta escaneo de codigos de barras. Usa Chrome 83+ o un dispositivo movil."),null)},stopScan(){this._scanStream&&(this._scanStream.getTracks().forEach((e=>e.stop())),this._scanStream=null),this._scanVideo=null;var e=document.getElementById("barcode-scan-overlay");e&&e.remove()},async registerPush(){if(!this.isNative)return null;try{const{PushNotifications:e}=await import("@capacitor/push-notifications");"granted"===(await e.requestPermissions()).receive&&await e.register(),e.addListener("registration",(e=>{console.log("Push token:",e.value)})),e.addListener("pushNotificationReceived",(e=>{console.log("Push received:",e)}))}catch(e){console.log("Push not available:",e)}},async vibrate(){if(this.isNative)try{const{Haptics:e,ImpactStyle:t}=await import("@capacitor/haptics");await e.impact({style:t.Light})}catch(e){}},async setupStatusBar(){if(this.isNative)try{const{StatusBar:e,Style:t}=await import("@capacitor/status-bar");await e.setStyle({style:t.Dark}),await e.setBackgroundColor({color:"#0d0d0d"})}catch(e){}}},window.NexusNative.isNative&&(window.NexusNative.setupStatusBar(),window.NexusNative.registerPush())}();

1
pos/static/js/offline-banner.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var n=document.getElementById("offlineBanner"),e=document.getElementById("offlineBannerText");if(n&&e){var t=null;navigator.onLine||o(),window.addEventListener("offline",o),window.addEventListener("online",(function(){clearTimeout(t),n.className="banner banner--success",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="<strong>Conexion restaurada</strong> — Sincronizando datos...",t=setTimeout((function(){n.style.animation="slideUp 0.3s ease-in forwards",n.addEventListener("animationend",(function e(){n.style.display="none",n.removeEventListener("animationend",e)}),{once:!0})}),3e3)}))}function o(){clearTimeout(t),n.className="banner banner--error",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="<strong>Conexion perdida</strong> — Intentando reconectar..."}}();

1
pos/static/js/onboarding.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/pos-utils.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/pos.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/printer.min.js vendored Normal file
View File

@@ -0,0 +1 @@
window.NexusPrinter=function(){"use strict";let e=null,t=null,r=null,n=null,a=null;async function i(){try{"usb"===a&&e&&await e.close(),"serial"===a&&(n&&(n.releaseLock(),n=null),r&&(await r.close(),r=null))}catch(e){}e=null,t=null,a=null,localStorage.removeItem("nexus_printer")}async function o(r){if(!a)return!1;try{if("usb"===a&&e&&null!==t){const n=512;for(let a=0;a<r.length;a+=n)await e.transferOut(t,r.slice(a,a+n));return!0}if("serial"===a&&n)return await n.write(r),!0}catch(e){console.error("[NexusPrinter] send error:",e),await i()}return!1}function l(){try{localStorage.setItem("nexus_printer",a||"")}catch(e){}}return{connect:async function(){if("usb"in navigator)try{e=await navigator.usb.requestDevice({filters:[]}),await e.open(),null===e.configuration&&await e.selectConfiguration(1);const r=e.configuration.interfaces[0];await e.claimInterface(r.interfaceNumber);const n=r.alternates[0].endpoints.find((e=>"out"===e.direction&&"bulk"===e.type));if(n)return t=n.endpointNumber,a="usb",l(),{ok:!0,type:"usb",name:e.productName||"USB Printer"}}catch(t){e=null}if("serial"in navigator)try{return r=await navigator.serial.requestPort(),await r.open({baudRate:9600}),n=r.writable.getWriter(),a="serial",l(),{ok:!0,type:"serial",name:"Serial Printer"}}catch(e){r=null,n=null}return{ok:!1,error:"No printer connected. Use a browser that supports WebUSB or Web Serial (Chrome/Edge)."}},disconnect:i,isConnected:function(){return null!==a},sendRaw:o,printSale:async function(e,t){t=t||80;const r=localStorage.getItem("pos_token"),n=await fetch("/pos/api/sales/"+e+"/print",{method:"POST",headers:{Authorization:"Bearer "+r,"Content-Type":"application/json"},body:JSON.stringify({printer_type:"escpos_raw",width:t})});if(!n.ok)return console.error("[NexusPrinter] backend error",n.status),!1;const a=await n.arrayBuffer();return o(new Uint8Array(a))}}}();

1
pos/static/js/push.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e={};try{e=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var o=e.role||"";if("owner"===o||"admin"===o)if("serviceWorker"in navigator&&"PushManager"in window){var r=localStorage.getItem("pos_token");r&&("complete"===document.readyState?setTimeout(i,2e3):window.addEventListener("load",(function(){setTimeout(i,2e3)})))}else console.log("[Push] Browser does not support push notifications");function t(e){for(var o=(e+"=".repeat((4-e.length%4)%4)).replace(/-/g,"+").replace(/_/g,"/"),r=window.atob(o),t=new Uint8Array(r.length),i=0;i<r.length;++i)t[i]=r.charCodeAt(i);return t}async function i(){try{var e=await navigator.serviceWorker.register("/pos/static/sw-push.js",{scope:"/pos/"});console.log("[Push] Service worker registered");var o=await fetch("/pos/api/push/vapid-key",{headers:{Authorization:"Bearer "+r}});if(!o.ok)return void console.log("[Push] VAPID key not available:",o.status);var i=(await o.json()).public_key;if(!i)return;if("granted"!==await Notification.requestPermission())return void console.log("[Push] Permission denied");var a=await e.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:t(i)});(await fetch("/pos/api/push/subscribe",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({subscription:a.toJSON()})})).ok&&console.log("[Push] Subscribed successfully")}catch(e){console.log("[Push] Setup error:",e)}}}();

1
pos/static/js/reports.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/sidebar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/sync-engine.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var n="sync_queue",e="inventory_cache",t="cached_parts",r=null;function o(){return new Promise((function(o,c){if(r)o(r);else{var i=indexedDB.open("nexus_pos_offline",2);i.onupgradeneeded=function(r){var o=r.target.result;if(o.objectStoreNames.contains(n)||o.createObjectStore(n,{keyPath:"id",autoIncrement:!0}),!o.objectStoreNames.contains(e)){var c=o.createObjectStore(e,{keyPath:"item_id"});c.createIndex("sku","sku",{unique:!1}),c.createIndex("name","name",{unique:!1})}if(!o.objectStoreNames.contains(t)){var i=o.createObjectStore(t,{keyPath:"part_number"});i.createIndex("name","name",{unique:!1}),i.createIndex("category","category",{unique:!1})}},i.onsuccess=function(n){r=n.target.result,o(r)},i.onerror=function(){c(i.error)}}}))}function c(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).getAll();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))})).then((function(e){if(!e.length)return Promise.resolve({synced:0});var t=Promise.resolve(),r=0,c=0;return e.forEach((function(e){t=t.then((function(){var t={method:e.method,headers:{"Content-Type":"application/json"}};return e.body&&(t.body=JSON.stringify(e.body)),fetch(e.url,t).then((function(t){if(t.ok)return r++,i=e.id,o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readwrite");o.objectStore(n).delete(i),o.oncomplete=function(){t()},o.onerror=function(){r(o.error)}}))}));var i;c++})).catch((function(){c++}))}))})),t.then((function(){return{synced:r,failed:c,total:e.length}}))}))}window.addEventListener("online",(function(){console.log("[SyncEngine] Online — processing queue..."),c().then((function(n){n.synced>0&&console.log("[SyncEngine] Synced "+n.synced+" operations")})).catch((function(n){console.error("[SyncEngine] Queue processing error:",n)}))})),window.addEventListener("offline",(function(){console.log("[SyncEngine] Offline — operations will be queued")})),"serviceWorker"in navigator&&navigator.serviceWorker.addEventListener("message",(function(n){n.data&&"SYNC_REQUESTED"===n.data.type&&c()})),o().catch((function(n){console.error("[SyncEngine] Failed to open IndexedDB:",n)})),window.SyncEngine={queueOperation:function(e,t,r){return o().then((function(o){return new Promise((function(c,i){var a=o.transaction(n,"readwrite");a.objectStore(n).add({url:e,method:t,body:r||null,timestamp:Date.now()}),a.oncomplete=function(){c()},a.onerror=function(){i(a.error)}}))}))},processQueue:c,getQueueCount:function(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).count();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))}))},isOnline:function(){return navigator.onLine},cacheInventory:function(){return fetch("/pos/api/sync/inventory").then((function(n){if(!n.ok)throw new Error("Sync inventory failed: "+n.status);return n.json()})).then((function(n){var t=n.items||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(e,"readwrite"),i=c.objectStore(e);i.clear(),t.forEach((function(n){i.put(n)})),c.oncomplete=function(){r(t.length)},c.onerror=function(){o(c.error)}}))}))}))},getCachedInventory:function(n){return o().then((function(t){return new Promise((function(r,o){var c=t.transaction(e,"readonly").objectStore(e).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.sku&&-1!==n.sku.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.barcode&&-1!==n.barcode.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))},cacheTopParts:function(){return fetch("/pos/api/sync/top-parts").then((function(n){if(!n.ok)throw new Error("Sync top-parts failed: "+n.status);return n.json()})).then((function(n){var e=n.parts||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(t,"readwrite"),i=c.objectStore(t);i.clear(),e.forEach((function(n){i.put(n)})),c.oncomplete=function(){console.log("[SyncEngine] Cached "+e.length+" top parts"),r(e.length)},c.onerror=function(){o(c.error)}}))}))}))},searchCachedParts:function(n){return o().then((function(e){return new Promise((function(r,o){var c=e.transaction(t,"readonly").objectStore(t).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.part_number&&-1!==n.part_number.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.category&&-1!==n.category.toLowerCase().indexOf(t)||n.brand&&-1!==n.brand.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))}}}();

1
pos/static/js/whatsapp.min.js vendored Normal file

File diff suppressed because one or more lines are too long