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
1 line
12 KiB
JavaScript
1 line
12 KiB
JavaScript
const Config=(()=>{const e="/pos/api/config";let t=[];function a(){return localStorage.getItem("pos_token")||""}function n(){return!!a()||(window.location.href="/pos/login",!1)}function r(){return{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"}}function o(e,t){var a=document.createElement("div");a.className="cfg-toast cfg-toast--"+(t||"ok"),a.textContent=e,document.body.appendChild(a),setTimeout((function(){a.remove()}),3e3)}function d(e){var t=document.getElementById(e);t&&(t.style.display="flex")}function c(e){var t=document.getElementById(e);t&&(t.style.display="none")}function i(e){document.documentElement.setAttribute("data-theme",e);try{localStorage.setItem("nexus-theme",e)}catch(e){}document.querySelectorAll(".theme-btn").forEach((function(t){t.classList.toggle("is-active",t.dataset.themeTarget===e)})),document.querySelectorAll(".theme-option").forEach((function(e){e.classList.remove("is-selected")}));var t="industrial"===e?0:1,a=document.querySelectorAll(".theme-option");a[t]&&a[t].classList.add("is-selected")}function l(e){i(e)}function s(){var e=new Date,t=String(e.getHours()).padStart(2,"0"),a=String(e.getMinutes()).padStart(2,"0"),n=document.getElementById("live-clock");n&&(n.textContent=t+":"+a)}window.setTheme=i,window.selectThemeOption=l;var u={owner:"Dueno",admin:"Admin",cashier:"Cajero",warehouse:"Almacenista",accountant:"Contador"},m={owner:"badge--owner",admin:"badge--blue",cashier:"badge--green",warehouse:"badge--yellow",accountant:"badge--purple"};function v(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}async function y(){var a=document.getElementById("branches-grid");if(!a)return[];try{var n=await fetch(e+"/branches",{headers:r()});if(!n.ok)throw new Error("Failed");var o=await n.json();t=o.data||[]}catch(e){return console.error("Config.loadBranches:",e),t=[],a.innerHTML='<div class="device-card"><div class="device-card__body" style="text-align:center;color:var(--color-error,red);">Error al cargar sucursales</div></div>',t}return function(){var e=document.getElementById("branches-grid");if(!e)return;var a="";t.forEach((function(e,t){var n=e.is_active?'<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">'+(0===t?"Principal":"Activa")+"</span>":'<span class="badge badge--inactive" style="padding:0 4px;font-size:0.625rem;">Inactiva</span>';a+='<div class="device-card"><div class="device-card__icon"><svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div><div class="device-card__body"><div class="device-card__name">'+v(e.name)+'</div><div class="device-card__detail">'+n+"</div>"+(e.address?'<div class="device-card__detail">'+v(e.address)+"</div>":"")+(e.phone?'<div class="device-card__detail">'+v(e.phone)+"</div>":"")+"</div></div>"})),a+='<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openModal(\'modal-branch\')"><div class="device-card__icon" style="background:transparent;border:2px dashed var(--color-border);"><svg viewBox="0 0 24 24" style="stroke:var(--color-text-muted);"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div><div class="device-card__body"><div class="device-card__name" style="color:var(--color-text-muted);">Agregar Sucursal</div><div class="device-card__detail">Configura una nueva ubicacion</div></div></div>',e.innerHTML=a}(),function(){var e=document.getElementById("emp-branch");if(!e)return;e.innerHTML='<option value="">-- Todas --</option>',t.forEach((function(t){if(t.is_active){var a=document.createElement("option");a.value=t.id,a.textContent=t.name,e.appendChild(a)}}))}(),t}async function g(t){var a=await fetch(e+"/branches",{method:"POST",headers:r(),body:JSON.stringify(t)});if(!a.ok){var n=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(n.error||"Save failed")}return a.json()}async function f(){var t=document.getElementById("employees-tbody"),a=document.getElementById("employees-count");if(!t)return[];var n=[];try{var o=await fetch(e+"/employees",{headers:r()});if(!o.ok)throw new Error("Failed");n=(await o.json()).data||[]}catch(e){return console.error("Config.loadEmployees:",e),t.innerHTML='<tr><td colspan="7" style="text-align:center;padding:var(--space-5);color:var(--color-error,red);">Error al cargar empleados</td></tr>',[]}var d=n.filter((function(e){return e.is_active})).length;if(a&&(a.textContent=d+" empleado"+(1!==d?"s":"")+" activo"+(1!==d?"s":"")),0===n.length)return t.innerHTML='<tr><td colspan="7" style="text-align:center;padding:var(--space-5);color:var(--color-text-muted);">Sin empleados registrados</td></tr>',n;var c="";return n.forEach((function(e){var t,a=function(e){if(!e)return"??";var t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[t.length-1][0]).toUpperCase():t[0].substring(0,2).toUpperCase()}(e.name),n=e.is_active?'<span class="badge badge--ok">Activo</span>':'<span class="badge badge--inactive">Inactivo</span>';c+='<tr><td><div class="user-cell"><div class="user-cell__avatar">'+a+'</div><span class="user-cell__name">'+v(e.name)+"</span></div></td><td>"+v(e.email||"-")+"</td><td>"+(t=e.role,'<span class="badge '+(m[t]||"badge--ok")+'">'+v(u[t]||t)+"</span></td><td>")+v(e.branch_name||"Todas")+"</td><td>"+n+"</td><td>"+(e.max_discount_pct||0)+'%</td><td><button class="btn btn--ghost btn--sm" onclick="Config.editEmployee('+e.id+')">Editar</button></td></tr>'})),t.innerHTML=c,n}async function p(t){var a=document.getElementById("employee-modal"),n=a?a.dataset.editId:null,o=e+"/employees",d="POST";n&&(o=e+"/employees/"+n,d="PUT",delete a.dataset.editId);var c=await fetch(o,{method:d,headers:r(),body:JSON.stringify(t)});if(!c.ok){var i=await c.json().catch((function(){return{error:c.statusText}}));throw new Error(i.error||"Save failed")}return c.json()}async function h(){try{var t=await fetch(e+"/business",{headers:r()});if(!t.ok)return;var a=await t.json();b("biz-razon-social",a.razon_social),b("biz-nombre",a.nombre),b("biz-rfc",a.rfc),b("biz-regimen",a.regimen_fiscal),b("biz-direccion",a.direccion),b("biz-telefono",a.telefono),b("biz-email",a.email)}catch(e){console.error("Config.loadBusiness:",e)}}function b(e,t){var a=document.getElementById(e);a&&(a.value=t||"")}function E(e){var t=document.getElementById(e);return t?t.value.trim():""}async function w(){try{var t=await fetch(e+"/currency",{headers:r()});if(!t.ok)return;var a=await t.json(),n=document.getElementById("cfg-currency"),o=document.getElementById("cfg-exchange-rate");n&&(n.value=a.currency||"MXN"),o&&(o.value=a.exchange_rate||17.5),localStorage.setItem("pos_currency",a.currency||"MXN"),localStorage.setItem("pos_exchange_rate",a.exchange_rate||17.5)}catch(e){console.error("Config.loadCurrency:",e)}}function I(){if(n()){try{var e=localStorage.getItem("nexus-theme");"industrial"!==e&&"modern"!==e||i(e)}catch(e){}s(),setInterval(s,3e4),function(){document.querySelector("#branches-grid");var e=document.getElementById("btn-save-branch");e&&e.addEventListener("click",(async function(){var t=document.getElementById("branch-name").value.trim();if(t){e.disabled=!0,e.textContent="Guardando...";try{await g({name:t,address:document.getElementById("branch-address").value.trim(),phone:document.getElementById("branch-phone").value.trim()}),o("Sucursal creada"),c("modal-branch"),document.getElementById("branch-name").value="",document.getElementById("branch-address").value="",document.getElementById("branch-phone").value="",await y()}catch(e){o(e.message,"error")}finally{e.disabled=!1,e.textContent="Guardar Sucursal"}}else o("Nombre de sucursal requerido","error")}));var t=document.getElementById("btn-new-employee");t&&t.addEventListener("click",(function(){d("modal-employee")}));var a=document.getElementById("btn-save-employee");a&&a.addEventListener("click",(async function(){var e=document.getElementById("emp-name").value.trim(),t=document.getElementById("emp-role").value,n=document.getElementById("emp-pin").value.trim();if(e)if(t)if(n&&4===n.length&&/^\d{4}$/.test(n)){var r=document.getElementById("emp-branch").value;a.disabled=!0,a.textContent="Guardando...";try{await p({name:e,email:document.getElementById("emp-email").value.trim()||null,phone:document.getElementById("emp-phone").value.trim()||null,role:t,pin:n,branch_id:r?parseInt(r,10):null,max_discount_pct:parseFloat(document.getElementById("emp-discount").value)||0}),o("Empleado creado"),c("modal-employee"),document.getElementById("emp-name").value="",document.getElementById("emp-email").value="",document.getElementById("emp-phone").value="",document.getElementById("emp-role").value="",document.getElementById("emp-pin").value="",document.getElementById("emp-branch").value="",document.getElementById("emp-discount").value="0",await f()}catch(e){o(e.message,"error")}finally{a.disabled=!1,a.textContent="Guardar Empleado"}}else o("PIN debe ser 4 digitos","error");else o("Selecciona un rol","error");else o("Nombre requerido","error")})),document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.addEventListener("click",(function(t){t.target===e&&(e.style.display="none")}))})),document.addEventListener("keydown",(function(e){"Escape"===e.key&&document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.style.display="none"}))}))}();var t=document.getElementById("cfg-kiosk-mode");t&&window.NexusKiosk&&(t.checked=window.NexusKiosk.isEnabled(),t.addEventListener("change",(function(){this.checked?(window.NexusKiosk.enable(),o("Modo Kiosko activado")):(window.NexusKiosk.disable(),o("Modo Kiosko desactivado"))}))),y(),f(),h(),w()}}return document.addEventListener("DOMContentLoaded",I),{init:I,setTheme:i,selectThemeOption:l,loadBranches:y,loadEmployees:f,saveBranch:g,saveEmployee:p,editEmployee:async function(t){if(n())try{var a=await fetch(e+"/employees",{headers:r()});if(!a.ok)throw new Error("Failed to load employees");var c=((await a.json()).data||[]).find((function(e){return e.id===t}));if(!c)return void o("Empleado no encontrado","error");b("new-emp-name",c.name),b("new-emp-email",c.email||"");var i=document.getElementById("new-emp-role");i&&(i.value=c.role||"cashier");var l=document.getElementById("new-emp-branch");l&&(l.value=c.branch_id||""),b("new-emp-discount",c.max_discount_pct||""),b("new-emp-pin","");var s=document.getElementById("employee-modal");if(s){s.dataset.editId=t;var u=s.querySelector(".modal-title, h3");u&&(u.textContent="Editar Empleado")}d("employee-modal")}catch(e){o("Error: "+e.message,"error")}},loadBusiness:h,saveBusiness:async function(){if(n()){var t={razon_social:E("biz-razon-social"),nombre:E("biz-nombre"),rfc:E("biz-rfc"),regimen_fiscal:E("biz-regimen"),direccion:E("biz-direccion"),telefono:E("biz-telefono"),email:E("biz-email")};try{var a=await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)});if(!a.ok){var d=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(d.error||"Error al guardar")}o("Datos de empresa guardados","ok")}catch(e){o(e.message,"error")}}},saveTaxParams:async function(){if(n()){var t={tax_iva:E("tax-iva")||"16",tax_ieps:E("tax-ieps")||"0",invoice_serie:E("tax-serie")||"FA",invoice_folio:E("tax-folio")||"1",default_currency:document.getElementById("tax-moneda")?document.getElementById("tax-moneda").value:"MXN",default_payment_method:document.getElementById("tax-forma-pago")?document.getElementById("tax-forma-pago").value:"01"};try{if(!(await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)})).ok)throw new Error("Error al guardar");o("Parámetros de impuestos guardados","ok")}catch(e){o(e.message,"error")}}},loadCurrency:w,saveCurrency:async function(){var t=document.getElementById("cfg-currency"),a=document.getElementById("cfg-exchange-rate"),n=document.getElementById("currency-status"),d=document.getElementById("btn-save-currency");if(t&&a){var c=t.value,i=parseFloat(a.value);if(!i||i<=0)o("Tipo de cambio invalido","error");else{d&&(d.disabled=!0,d.textContent="Guardando...");try{var l=await fetch(e+"/currency",{method:"PUT",headers:r(),body:JSON.stringify({currency:c,exchange_rate:i})});if(!l.ok){var s=await l.json().catch((function(){return{error:l.statusText}}));throw new Error(s.error||"Save failed")}localStorage.setItem("pos_currency",c),localStorage.setItem("pos_exchange_rate",i),o("Moneda actualizada"),n&&(n.textContent=c+" — TC: "+i)}catch(e){o(e.message,"error")}finally{d&&(d.disabled=!1,d.textContent="Guardar Moneda")}}}},openModal:d,closeModal:c}})(); |