Files
Autoparts-DB/pos/static/js/pos.min.js
consultoria-as 21959f1b37 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
2026-04-27 08:34:24 +00:00

1 line
26 KiB
JavaScript

const POS=(()=>{let e=localStorage.getItem("pos_token")||"",t=[],n=-1,o=null,a=null,i="efectivo",r=!1,c=100,s=null,l=null,d=null,m=null;const u=localStorage.getItem("pos_currency")||"MXN",p={MXN:"$",USD:"US$"},y="USD"===u?"en-US":"es-MX",v=e=>(p[u]||"$")+parseFloat(e||0).toLocaleString(y,{minimumFractionDigits:2,maximumFractionDigits:2});async function g(t,n={}){n.headers={"Content-Type":"application/json",Authorization:"Bearer "+e};const o=await fetch(t,n),a=await o.json();if(!o.ok)throw new Error(a.error||`HTTP ${o.status}`);return a}function f(e){const t=document.getElementById("toastContainer");if(!t)return;const n=document.createElement("div");n.className="toast",n.textContent=e,t.appendChild(n),setTimeout((()=>n.remove()),2100)}function h(e){const n=t.find((t=>t.inventory_id===e.inventory_id));if(n)return n.quantity+=e.quantity||1,void E();t.push({inventory_id:e.inventory_id||e.id,part_number:e.part_number||"",name:e.name||"",quantity:e.quantity||1,unit_price:parseFloat(e.unit_price||e.price_1||0),unit_cost:parseFloat(e.cost||0),discount_pct:parseFloat(e.discount_pct||0),tax_rate:parseFloat(e.tax_rate||.16),stock:e.stock||0}),E(),f(`${e.name||"Articulo"} agregado`)}function _(e){t.splice(e,1),n>=t.length&&(n=t.length-1),E()}function E(){const e=document.getElementById("cartBody"),o=document.getElementById("cartTable"),a=document.getElementById("cartEmpty");if(0===t.length)return o.style.display="none",a.style.display="flex",void b();o.style.display="",a.style.display="none";let i="";t.forEach(((e,t)=>{const o=e.unit_price*e.quantity,a=o-o*e.discount_pct/100,c=r?`<td class="num" style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);color:var(--color-text-muted);font-size:var(--text-caption);">${v(e.unit_cost)}</td>`:"";let s="";if(r){const t=e.unit_price*(1-e.discount_pct/100),n=t>0?((t-e.unit_cost)/t*100).toFixed(1):"0.0",o=parseFloat(n);s=`<td style="text-align:center;padding:var(--space-2);"><span style="font-family:var(--font-mono);font-size:var(--text-caption);font-weight:var(--font-weight-bold);color:${o>30?"var(--color-success)":o>15?"var(--color-warning)":"var(--color-error)"};">${n}%</span></td>`}i+=`<tr style="border-bottom:1px solid var(--color-border);cursor:pointer;${t===n?"background:var(--color-primary-muted);":""}" onclick="POS.selectRow(${t})">\n <td style="padding:var(--space-2);color:var(--color-text-muted);">${t+1}</td>\n <td style="padding:var(--space-2);">\n <div style="font-weight:var(--font-weight-semibold);color:var(--color-text-primary);">${e.name}</div>\n <div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${e.part_number} | Stock: ${e.stock}</div>\n </td>\n <td style="text-align:center;padding:var(--space-2);"><input type="number" style="width:50px;text-align:center;font-family:var(--font-mono);background:var(--color-bg-base);color:var(--color-text-primary);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:2px 4px;" value="${e.quantity}" min="1"\n onchange="POS.updateQty(${t}, this.value)" onclick="event.stopPropagation()"></td>\n <td style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);">${v(e.unit_price)}</td>\n <td style="text-align:center;padding:var(--space-2);"><input type="number" style="width:45px;text-align:center;font-family:var(--font-mono);background:var(--color-bg-base);color:var(--color-text-primary);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:2px 4px;" value="${e.discount_pct}" min="0" max="100" step="0.5"\n onchange="POS.updateDiscount(${t}, this.value)" onclick="event.stopPropagation()">%</td>\n <td style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);font-weight:var(--font-weight-bold);">${v(a)}</td>\n ${c}\n ${s}\n <td style="text-align:center;"><button style="background:transparent;border:1px solid var(--color-border);border-radius:var(--radius-sm);width:24px;height:24px;cursor:pointer;color:var(--color-text-muted);font-size:14px;" onclick="event.stopPropagation(); POS.removeFromCart(${t})">&times;</button></td>\n </tr>`})),e.innerHTML=i,b(),function(){const e=document.getElementById("avgMargin");if(!e||!r)return;let n=0,o=0;if(t.forEach((e=>{if(e.unit_cost>0){const t=e.unit_price*(1-e.discount_pct/100);n+=t*e.quantity,o+=e.unit_cost*e.quantity}})),n>0){const t=(n-o)/n*100;e.textContent=t.toFixed(1)+"%",e.style.color=t>30?"var(--color-success)":t>15?"var(--color-warning)":"var(--color-error)"}else e.textContent="--"}()}function I(e,n){let o=Math.max(0,Math.min(100,parseFloat(n)||0));o>c&&(alert(`Descuento maximo permitido: ${c}%`),o=c),t[e].discount_pct=o,E()}function b(){let e=0,n=0,o=0;t.forEach((t=>{const a=t.unit_price*t.quantity,i=a*t.discount_pct/100,r=a-i,c=r*t.tax_rate;e+=r,n+=i,o+=c}));const a=e+o;document.getElementById("dispSubtotal").textContent=v(e),document.getElementById("dispTax").textContent=v(o),document.getElementById("dispTotal").textContent=v(a),n>0?(document.getElementById("discountRow").style.display="",document.getElementById("dispDiscount").textContent="-"+v(n)):document.getElementById("discountRow").style.display="none"}function x(){let e=0,n=0;return t.forEach((t=>{const o=t.unit_price*t.quantity,a=o-o*t.discount_pct/100,i=a*t.tax_rate;e+=a,n+=i})),Math.round(100*(e+n))/100}async function B(e){if(!e||e.length<2)$();else try{const t=await g(`/pos/api/inventory/items?q=${encodeURIComponent(e)}&per_page=20`),n=document.getElementById("searchResults"),a=document.getElementById("totalsPanel");if(0===t.data.length)n.innerHTML='<div style="padding:20px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';else{let e="";t.data.forEach((t=>{let n=t.price_1;if(o){const e=o.price_tier||1;n=3===e?t.price_3:2===e?t.price_2:t.price_1}e+=`<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border);cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:var(--transition-fast);" onmouseover="this.style.background='var(--color-primary-muted)'" onmouseout="this.style.background=''" onclick='POS.addFromSearch(${JSON.stringify(t).replace(/'/g,"&#39;")}, ${n})'>\n <div>\n <div style="font-weight:var(--font-weight-semibold);">${t.name}</div>\n <div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${t.part_number} | ${t.brand||""}</div>\n <div style="font-size:var(--text-caption);color:${t.stock<=0?"var(--color-error)":"var(--color-text-muted)"};">Stock: ${t.stock}</div>\n </div>\n <div style="font-family:var(--font-mono);font-weight:var(--font-weight-bold);color:var(--color-primary);">${v(n)}</div>\n </div>`})),n.innerHTML=e}n.style.display="",a.style.display="none"}catch(e){console.error("Search error:",e)}}function $(){const e=document.getElementById("searchResults"),t=document.getElementById("totalsPanel");e&&(e.style.display="none"),t&&(t.style.display="")}async function C(e){o=e,document.getElementById("customerAutocomplete").style.display="none",document.getElementById("customerSearchWrap").querySelector("input").style.display="none";if(document.getElementById("customerName").textContent=e.name,document.getElementById("customerTier").textContent={1:"P1 Mostrador",2:"P2 Taller",3:"P3 Mayoreo"}[e.price_tier]||"P1",document.getElementById("customerCredit").textContent=`Credito: ${v(e.credit_balance)} / ${v(e.credit_limit)}`,document.getElementById("customerSelected").style.display="",e.vehicle_info&&e.vehicle_info.length>0){const t=e.vehicle_info[0];document.getElementById("vehicleInfo").textContent=`${t.make||""} ${t.model||""} ${t.year||""} ${t.plates?"("+t.plates+")":""}`,document.getElementById("vehicleBanner").classList.add("visible")}try{const t=await g(`/pos/api/customers/${e.id}`);if(t.recent_purchases&&t.recent_purchases.length>0){const e=t.recent_purchases[0],n=Math.floor((Date.now()-new Date(e.created_at).getTime())/864e5),o=0===n?"hoy":1===n?"hace 1 dia":`hace ${n} dias`;document.getElementById("lastPurchaseInfo").textContent=`${v(e.total)} ${o}`,document.getElementById("vehicleBanner").classList.add("visible")}}catch(e){console.warn("Could not fetch customer detail:",e)}const t=document.getElementById("cfdiHint");t&&e.rfc&&(t.textContent=`RFC: ${e.rfc}`,document.getElementById("cfdiCheck").checked=!!e.rfc),E()}function w(){o=null,document.getElementById("customerSelected").style.display="none";const e=document.getElementById("customerSearch");e&&(e.style.display="",e.value=""),document.getElementById("vehicleBanner").classList.remove("visible");const t=document.getElementById("cfdiHint");t&&(t.textContent=""),E()}function k(){document.getElementById("newCustomerModal").classList.remove("open")}function S(){if(0===t.length)return void f("Carrito vacio");if(!a)return void alert("No hay caja abierta. Abra una caja primero.");i="efectivo";const e=x();document.getElementById("modalTotal").textContent=v(e),document.getElementById("modalItemCount").textContent=`${t.reduce(((e,t)=>e+t.quantity),0)} productos`,document.getElementById("modalCustomerName").textContent=o?o.name:"Publico General",document.getElementById("cashReceived").value="",document.getElementById("changeDisplay").textContent="$0.00",document.getElementById("changeDisplay").className="cambio-amount positive";const n=document.getElementById("paymentRef");n&&(n.value="");const r=document.getElementById("quickAmounts");if(r){const t=[Math.ceil(e),100*Math.ceil(e/100),500*Math.ceil(e/500),1e3*Math.ceil(e/1e3)],n=[...new Set(t)].slice(0,4);r.innerHTML=n.map((e=>`<button class="quick-btn" onclick="document.getElementById('cashReceived').value=${e};POS.updateChange();">${v(e)}</button>`)).join("")}const c=document.getElementById("refAmount");c&&(c.value=v(e)),document.querySelectorAll(".pago-tab").forEach((e=>e.classList.remove("active"))),document.querySelector('.pago-tab[data-method="efectivo"]').classList.add("active"),document.getElementById("cashPayment").classList.add("active"),document.getElementById("cashPayment").style.display="",document.getElementById("refPayment").classList.remove("active"),document.getElementById("refPayment").style.display="none",document.getElementById("mixedPayment").classList.remove("active"),document.getElementById("mixedPayment").style.display="none";const s=document.getElementById("btnConfirmPayment");s.disabled=!1,s.textContent=`Confirmar Pago — ${v(e)}`,document.getElementById("paymentModal").classList.add("open"),setTimeout((()=>document.getElementById("cashReceived").focus()),100)}function P(){document.getElementById("paymentModal").classList.remove("open")}async function T(){const e=x();let r=0,c=[],d="";if("efectivo"===i){if(r=parseFloat(document.getElementById("cashReceived").value)||0,r<e)return void alert(`Monto insuficiente. Total: ${v(e)}`)}else if("transferencia"===i||"tarjeta"===i)r=e,d=document.getElementById("paymentRef").value.trim();else if("mixto"===i){if(document.querySelectorAll(".mixed-row").forEach((e=>{const t=e.querySelector("select").value,n=parseFloat(e.querySelector(".mixed-amount").value)||0,o=e.querySelectorAll("input")[1]?.value||"";n>0&&(c.push({method:t,amount:n,reference:o}),r+=n)})),r<e)return void alert(`Monto total insuficiente. Falta: ${v(e-r)}`)}const m={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o?o.id:null,payment_method:i,sale_type:"cash",register_id:a?a.id:null,amount_paid:r,payment_details:c,reference:d,generate_cfdi:document.getElementById("cfdiCheck").checked},u=document.getElementById("btnConfirmPayment");u.disabled=!0,u.textContent="Procesando...";try{const e=await g("/pos/api/sales",{method:"POST",body:JSON.stringify(m)});s=e.id,l=e,P(),L(e),t=[],n=-1,w(),E(),f(`Venta #${e.id} completada`)}catch(e){alert("Error al procesar venta: "+e.message)}finally{u.disabled=!1,u.textContent="Confirmar Pago"}}async function M(){if(0===t.length)return void f("Carrito vacio");const e={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o?o.id:null};try{const t=await g("/pos/api/quotations",{method:"POST",body:JSON.stringify(e)});f(`Cotizacion #${t.id} guardada. Total: ${v(t.total)}`)}catch(e){alert("Error: "+e.message)}}function L(e){const t=new Date(e.created_at).toLocaleString("es-MX",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),n=o?o.name:"Publico General",r=o&&o.rfc?o.rfc:"";let c="";(e.items||[]).forEach((e=>{const t=e.unit_price*e.quantity*(1-(e.discount_pct||0)/100);c+=`\n <div class="item-line-wide">\n <span class="qty">${e.quantity}</span>\n <span class="name">${e.name||""}</span>\n <span class="price">${v(e.unit_price)}</span>\n <span class="subtotal">${v(e.subtotal||t)}</span>\n </div>`}));const s=`\n <div class="store-name">NEXUS AUTOPARTS</div>\n <div class="store-tagline">Tu conexion con las refacciones</div>\n <div class="store-info">\n Sucursal: ${a&&a.branch_name||""}<br>\n RFC: NAU210315XX1\n </div>\n <hr class="divider-double">\n <div class="folio-line">\n <span>VENTA: V-${e.id}</span>\n <span>${t}</span>\n </div>\n <div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">\n <span>Cliente: ${n}</span>\n ${r?`<span>RFC: ${r}</span>`:""}\n </div>\n <hr class="divider">\n <div class="item-line-wide" style="font-weight: bold; font-size: 9px; color: #555; text-transform: uppercase;">\n <span class="qty">Cant</span>\n <span class="name">Descripcion</span>\n <span class="price">P. Unit</span>\n <span class="subtotal">Importe</span>\n </div>\n <hr class="divider" style="margin: 2px 0;">\n ${c}\n <hr class="divider-double">\n <div class="total-section">\n <div class="total-line">\n <span>Subtotal:</span><span>${v(e.subtotal)}</span>\n </div>\n ${e.discount_total>0?`<div class="total-line"><span>Descuento:</span><span>-${v(e.discount_total)}</span></div>`:""}\n <div class="total-line">\n <span>IVA 16%:</span><span>${v(e.tax_total)}</span>\n </div>\n <div class="total-line grand">\n <span>TOTAL:</span><span>${v(e.total)}</span>\n </div>\n </div>\n <hr class="divider">\n <div class="payment-section">\n <div class="ticket-row">\n <span>Forma de pago:</span><span>${e.payment_method||i}</span>\n </div>\n ${"efectivo"===e.payment_method?`\n <div class="ticket-row">\n <span>Recibido:</span><span>${v(e.amount_paid)}</span>\n </div>\n <div class="ticket-row" style="font-weight: bold;">\n <span>Cambio:</span><span>${v(e.change_given||0)}</span>\n </div>`:""}\n </div>\n <hr class="divider">\n <div class="footer-section">\n <div class="thanks">Gracias por su compra!</div>\n <div>Conserve su ticket como comprobante.</div>\n </div>\n `,l=document.getElementById("ticketContent");l&&(l.innerHTML=s);const d=document.getElementById("ticketPreviewContent");d&&(d.innerHTML=s),document.getElementById("ticketModal").classList.add("open")}function q(){document.getElementById("ticketModal").classList.remove("open")}async function N(){if(s)try{L(await g(`/pos/api/sales/${s}`))}catch(e){alert("Error: "+e.message)}else f("No hay venta reciente")}function D(){f("Comando enviado al cajon de efectivo.")}return async function(){try{const t=JSON.parse(atob(e.split(".")[1]));if(document.getElementById("employeeName").textContent=t.name||"Empleado",document.getElementById("branchName").textContent=t.branch_name||"",r=(t.permissions||[]).includes("pos.view_cost"),c=t.max_discount_pct||100,r){document.getElementById("thCost").style.display="",document.getElementById("thMargin").style.display="";const e=document.getElementById("costToggle");e&&(e.style.display="")}const n=document.querySelector(".status-bar__user-avatar");if(n&&t.name){const e=t.name.split(" ");n.textContent=e.map((e=>e[0])).join("").substring(0,2).toUpperCase()}}catch(e){console.warn("Could not parse token:",e)}const o=localStorage.getItem("pos_cart");if(o)try{const e=JSON.parse(o);for(const t of e)h(t);localStorage.removeItem("pos_cart")}catch(e){console.warn("Could not load catalog cart:",e)}await async function(){try{const e=await g("/pos/api/register/current");e.register?(a=e.register,document.getElementById("registerInfo").innerHTML=`<span>Caja #${e.register.register_number}</span>`):document.getElementById("registerInfo").innerHTML="<span>Sin caja abierta</span>"}catch(e){console.warn("Register check failed:",e)}}(),document.addEventListener("keydown",(e=>{const o=e.target.tagName,a="INPUT"===o||"TEXTAREA"===o||"SELECT"===o;switch(e.key){case"F1":e.preventDefault(),document.getElementById("itemSearch").focus();break;case"F2":e.preventDefault(),document.getElementById("customerSearch").focus(),document.getElementById("customerSearch").style.display="",document.getElementById("customerSelected").style.display="none";break;case"F3":e.preventDefault(),S();break;case"F4":e.preventDefault(),M();break;case"F5":e.preventDefault(),N();break;case"F6":e.preventDefault(),D();break;case"Escape":if(e.preventDefault(),document.getElementById("paymentModal").classList.contains("open"))P();else if(document.getElementById("newCustomerModal").classList.contains("open"))k();else if(document.getElementById("ticketModal").classList.contains("open"))q();else if(document.querySelector(".confirm-overlay.active")){const e=document.getElementById("overlay-cancelar-venta"),t=document.getElementById("modal-cancelar-venta");e&&e.classList.contains("active")&&(e.classList.remove("active"),t.classList.remove("active"))}else $();break;case"Delete":!a&&n>=0&&n<t.length&&(e.preventDefault(),_(n));break;case"+":!a&&n>=0&&n<t.length&&(e.preventDefault(),t[n].quantity++,E());break;case"-":!a&&n>=0&&n<t.length&&(e.preventDefault(),t[n].quantity>1&&(t[n].quantity--,E()));break;case"*":if(!a&&n>=0&&n<t.length){e.preventDefault();const o=prompt("Descuento %:",t[n].discount_pct);null!==o&&I(n,o)}break;case"ArrowUp":!a&&t.length>0&&(e.preventDefault(),n=Math.max(0,n-1),E());break;case"ArrowDown":!a&&t.length>0&&(e.preventDefault(),n=Math.min(t.length-1,n+1),E());break;case"Enter":"cashReceived"===e.target.id&&(e.preventDefault(),T())}})),function(){const e=document.getElementById("itemSearch");if(!e)return;e.addEventListener("input",(()=>{clearTimeout(d),d=setTimeout((()=>B(e.value.trim())),300)})),e.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.preventDefault(),B(e.value.trim())),"Escape"===t.key&&(e.value="",$())}))}(),function(){const e=document.getElementById("customerSearch");if(!e)return;e.addEventListener("input",(()=>{clearTimeout(m),m=setTimeout((()=>async function(e){if(!e||e.length<2)return void(document.getElementById("customerAutocomplete").style.display="none");try{const t=await g(`/pos/api/customers?q=${encodeURIComponent(e)}&per_page=10`),n=document.getElementById("customerAutocomplete");if(0===t.data.length)n.innerHTML='<div style="padding:var(--space-3);color:var(--color-text-muted);">Sin resultados</div>';else{let e="";t.data.forEach((t=>{const n={1:"Mostrador",2:"Taller",3:"Mayoreo"};e+=`<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border);cursor:pointer;transition:var(--transition-fast);" onmouseover="this.style.background='var(--color-primary-muted)'" onmouseout="this.style.background=''" onclick='POS.selectCustomer(${JSON.stringify(t).replace(/'/g,"&#39;")})'>\n <div style="font-weight:var(--font-weight-semibold);">${t.name}</div>\n <div style="font-size:var(--text-caption);color:var(--color-text-muted);">${t.rfc||""} | ${t.phone||""} | ${n[t.price_tier]||"P1"} | Credito: ${v(t.credit_balance)}/${v(t.credit_limit)}</div>\n </div>`})),n.innerHTML=e}n.style.display="block"}catch(e){console.error("Customer search error:",e)}}(e.value.trim())),300)})),e.addEventListener("keydown",(t=>{"Escape"===t.key&&(e.value="",document.getElementById("customerAutocomplete").style.display="none")}))}()}(),{addToCart:h,removeFromCart:_,selectRow:function(e){n=e,E()},updateQty:function(e,n){const o=Math.max(1,parseInt(n)||1);t[e].quantity=o,E()},updateDiscount:I,addFromSearch:function(e,t){h({inventory_id:e.id,part_number:e.part_number,name:e.name,unit_price:t,cost:e.cost,tax_rate:e.tax_rate,stock:e.stock}),$(),document.getElementById("itemSearch").value="",document.getElementById("itemSearch").focus()},hideSearchResults:$,selectCustomer:C,clearCustomer:w,showNewCustomerModal:function(){document.getElementById("newCustomerModal").classList.add("open"),setTimeout((()=>document.getElementById("ncName").focus()),100)},closeNewCustomerModal:k,saveNewCustomer:async function(){const e=document.getElementById("ncName").value.trim();if(!e)return void alert("Nombre es requerido");const t=[],n=document.getElementById("ncVehMake").value.trim();n&&t.push({make:n,model:document.getElementById("ncVehModel").value.trim(),year:document.getElementById("ncVehYear").value.trim(),plates:document.getElementById("ncVehPlates").value.trim()});const o={name:e,rfc:document.getElementById("ncRfc").value.trim()||null,razon_social:document.getElementById("ncRazonSocial").value.trim()||null,regimen_fiscal:document.getElementById("ncRegimenFiscal").value||null,uso_cfdi:document.getElementById("ncUsoCfdi").value||"G03",phone:document.getElementById("ncPhone").value.trim()||null,email:document.getElementById("ncEmail").value.trim()||null,price_tier:parseInt(document.getElementById("ncPriceTier").value)||1,credit_limit:parseFloat(document.getElementById("ncCreditLimit").value)||0,vehicle_info:t.length>0?t:null};try{C({id:(await g("/pos/api/customers",{method:"POST",body:JSON.stringify(o)})).id,name:o.name,rfc:o.rfc,phone:o.phone,price_tier:o.price_tier,credit_limit:o.credit_limit,credit_balance:0,vehicle_info:o.vehicle_info}),k(),f("Cliente creado")}catch(e){alert("Error al crear cliente: "+e.message)}},checkout:S,confirmPayment:T,closePaymentModal:P,selectPaymentMethod:function(e,t){i=e,document.querySelectorAll(".pago-tab").forEach((e=>e.classList.remove("active"))),t&&t.classList.add("active");const n={efectivo:"cashPayment",transferencia:"refPayment",tarjeta:"refPayment",mixto:"mixedPayment"};if(["cashPayment","refPayment","mixedPayment"].forEach((t=>{const o=document.getElementById(t);if(o){const t=o.id===n[e];o.classList.toggle("active",t),o.style.display=t?"":"none"}})),"efectivo"===e&&document.getElementById("cashReceived").focus(),"transferencia"===e||"tarjeta"===e){const e=document.getElementById("paymentRef");e&&e.focus()}},updateChange:function(){const e=x(),t=(parseFloat(document.getElementById("cashReceived").value)||0)-e,n=document.getElementById("changeDisplay");t>=0?(n.textContent=v(t),n.className="cambio-amount positive"):(n.textContent="-"+v(Math.abs(t)),n.className="cambio-amount negative")},updateMixedTotal:function(){const e=x();let t=0;document.querySelectorAll(".mixed-amount").forEach((e=>{t+=parseFloat(e.value)||0}));const n=e-t,o=document.getElementById("mixedRemaining");o.textContent=n>0?`Faltante: ${v(n)}`:`Cubierto (${v(t)})`,o.style.color=n>0?"var(--color-error)":"var(--color-success)"},creditSale:async function(){if(0===t.length)return void alert("Carrito vacio");if(!o)return void alert("Seleccione un cliente para venta a credito");if(!a)return void alert("No hay caja abierta.");const e=x(),i=(o.credit_limit||0)-(o.credit_balance||0);if(o.credit_limit>0&&e>i&&!confirm(`Credito insuficiente. Disponible: ${v(i)}, Total: ${v(e)}. Continuar?`))return;const r={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o.id,payment_method:"credito",sale_type:"credit",register_id:a?a.id:null,amount_paid:0};try{const e=await g("/pos/api/sales",{method:"POST",body:JSON.stringify(r)});s=e.id,l=e,L(e),t=[],n=-1,w(),E()}catch(e){alert("Error: "+e.message)}},saveQuotation:M,createLayaway:async function(){if(0===t.length)return void alert("Carrito vacio");if(!o)return void alert("Seleccione un cliente para apartado");const e=x(),i=prompt(`Total: ${v(e)}\nIngrese monto del anticipo:`);if(!i)return;const r=parseFloat(i);if(isNaN(r)||r<=0)return void alert("Monto invalido");if(r>e)return void alert("El anticipo no puede exceder el total");const c={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o.id,initial_payment:r,payment_method:"efectivo",register_id:a?a.id:null};try{const e=await g("/pos/api/layaways",{method:"POST",body:JSON.stringify(c)});f(`Apartado #${e.id} creado. Restante: ${v(e.remaining)}`),t=[],n=-1,w(),E()}catch(e){alert("Error: "+e.message)}},showLastSale:N,openDrawer:D,showTicket:L,closeTicketModal:q,printTicket:function(){const e=document.getElementById("ticketPrintArea");e&&(e.style.display="block"),window.print(),setTimeout((()=>{e&&(e.style.display="none")}),500)},connectThermal:async function(){if(!window.NexusPrinter)return void f("Printer module not loaded");const e=await NexusPrinter.connect();e.ok?(f("Impresora conectada: "+(e.name||e.type)),function(){const e=document.getElementById("btnConnectPrinter"),t=document.getElementById("btnThermalPrint");window.NexusPrinter&&NexusPrinter.isConnected()?(e&&(e.style.display="none"),t&&(t.style.display="")):(e&&(e.style.display=""),t&&(t.style.display="none"))}()):f(e.error||"No se pudo conectar la impresora")},thermalPrint:async function(){if(!window.NexusPrinter||!NexusPrinter.isConnected())return void f("Conecte una impresora termica primero");if(!s)return void f("No hay venta para imprimir");f(await NexusPrinter.printSale(s)?"Ticket enviado a impresora termica":"Error al imprimir. Reconecte la impresora.")}}})();