Files
Autoparts-DB/pos/static/js/invoicing.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
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Invoicing=(()=>{const t="/pos/api/invoicing";function e(){return localStorage.getItem("pos_token")||""}async function a(a,n={}){const o=await fetch(`${t}${a}`,{headers:{Authorization:`Bearer ${e()}`,"Content-Type":"application/json"},...n});if(!o.ok){const t=await o.json().catch((()=>({error:o.statusText})));throw new Error(t.error||"Request failed")}return o.json()}function n(t){return parseFloat(t||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function o(t){document.querySelectorAll(".tab-btn").forEach((t=>{t.classList.remove("is-active"),t.setAttribute("aria-selected","false")})),document.querySelectorAll(".tab-panel").forEach((t=>t.classList.remove("is-active")));const e=document.querySelector(`.tab-btn[onclick*="'${t}'"]`)||document.getElementById(`tab-${t}`);e&&(e.classList.add("is-active"),e.setAttribute("aria-selected","true"));const a=document.getElementById(`panel-${t}`);a&&a.classList.add("is-active"),"facturas"===t&&r(),"notas"===t&&s(),"complementos"===t&&l(),"cancelaciones"===t&&i()}function c(t){const e={pending:{css:"badge--pendiente",label:"Pendiente"},pendiente:{css:"badge--pendiente",label:"Pendiente"},sending:{css:"badge--proceso",label:"Enviando"},stamped:{css:"badge--timbrada",label:"Timbrada"},timbrada:{css:"badge--timbrada",label:"Timbrada"},failed:{css:"badge--rechazada",label:"Fallido"},cancelled:{css:"badge--cancelada",label:"Cancelada"},cancelada:{css:"badge--cancelada",label:"Cancelada"},ppd:{css:"badge--ppd",label:"PPD"},proceso:{css:"badge--proceso",label:"En proceso"},aceptada:{css:"badge--aceptada",label:"Aceptada SAT"},rechazada:{css:"badge--rechazada",label:"Rechazada SAT"}}[t]||{css:"",label:t||""};return`<span class="badge ${e.css}">${e.label}</span>`}async function r(){const e=document.getElementById("panel-facturas");if(!e)return;const o=e.querySelector(".data-table tbody");if(o)try{const r=await a("/queue?per_page=50&type=Ingreso"),s=r.data||[];if(!s.length)return void(o.innerHTML='<tr><td colspan="10" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay facturas en este periodo.</td></tr>');o.innerHTML=s.map((e=>`<tr>\n <td class="td--mono">${e.provisional_folio||e.id||"-"}</td>\n <td class="td--primary">${e.serie||"-"}</td>\n <td class="td--primary">${e.customer_name||"-"}</td>\n <td class="td--mono">${e.rfc||"-"}</td>\n <td class="td--amount">$${n(e.subtotal)}</td>\n <td class="td--amount">$${n(e.tax)}</td>\n <td class="td--amount">$${n(e.total)}</td>\n <td style="font-size:var(--text-caption);">${e.uso_cfdi||"-"}</td>\n <td>${c(e.status)}</td>\n <td>\n <div style="display:flex;gap:4px;">\n <button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${e.id})">Ver</button>\n ${e.sale_id?`<a href="${t}/${e.sale_id}/pdf" target="_blank" class="btn btn--ghost btn--sm">PDF</a>`:""}\n ${"stamped"===e.status?`<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${e.id})">XML</button>`:""}\n </div>\n </td>\n </tr>`)).join("");const l=e.querySelector(".table-footer span");l&&(l.textContent=`Mostrando 1${s.length} de ${r.pagination?.total||s.length} facturas`)}catch(t){o.innerHTML=`<tr><td colspan="10" style="color:var(--color-error);padding:var(--space-4);">Error: ${t.message}</td></tr>`}}async function s(){const e=document.getElementById("panel-notas");if(!e)return;const o=e.querySelector(".data-table tbody");if(o)try{const e=(await a("/queue?per_page=50&type=Egreso")).data||[];if(!e.length)return void(o.innerHTML='<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay notas de credito.</td></tr>');o.innerHTML=e.map((e=>`<tr>\n <td class="td--mono">${e.provisional_folio||"-"}</td>\n <td class="td--mono" style="color:var(--color-text-accent);">${e.related_folio||"-"}</td>\n <td class="td--primary">${e.customer_name||"-"}</td>\n <td>${e.description||"-"}</td>\n <td class="td--amount">$${n(e.total)}</td>\n <td>${c(e.status)}</td>\n <td>\n <div style="display:flex;gap:4px;">\n <button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${e.id})">Ver</button>\n ${e.sale_id?`<a href="${t}/${e.sale_id}/pdf" target="_blank" class="btn btn--ghost btn--sm">PDF</a>`:""}\n </div>\n </td>\n </tr>`)).join("")}catch(t){o.innerHTML=`<tr><td colspan="7" style="color:var(--color-error);padding:var(--space-4);">Error: ${t.message}</td></tr>`}}async function l(){const t=document.getElementById("panel-complementos");if(!t)return;const e=t.querySelector(".data-table tbody");if(e)try{const t=(await a("/queue?per_page=50&type=Pago")).data||[];if(!t.length)return void(e.innerHTML='<tr><td colspan="8" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay complementos de pago.</td></tr>');e.innerHTML=t.map((t=>`<tr>\n <td class="td--mono">${t.provisional_folio||"-"}</td>\n <td class="td--mono" style="color:var(--color-text-accent);">${t.related_folio||"-"}</td>\n <td class="td--primary">${t.customer_name||"-"}</td>\n <td class="td--amount">$${n(t.total)}</td>\n <td style="font-size:var(--text-caption);">${t.payment_method||"-"}</td>\n <td>${t.created_at?new Date(t.created_at).toLocaleDateString("es-MX"):"-"}</td>\n <td>${c(t.status)}</td>\n <td>\n <div style="display:flex;gap:4px;">\n <button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${t.id})">Ver</button>\n ${"stamped"===t.status?`<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${t.id})">XML</button>`:""}\n </div>\n </td>\n </tr>`)).join("")}catch(t){e.innerHTML=`<tr><td colspan="8" style="color:var(--color-error);padding:var(--space-4);">Error: ${t.message}</td></tr>`}}async function i(){const t=document.getElementById("panel-cancelaciones");if(!t)return;const e=t.querySelector(".cancel-grid");if(e)try{const t=(await a("/queue?per_page=50&status=cancelled")).data||[];let o=[];try{o=(await a("/queue?per_page=50&status=cancelling")).data||[]}catch(t){}const r=[...o,...t];if(!r.length)return void(e.innerHTML='<p style="padding:var(--space-6);color:var(--color-text-muted);text-align:center;">No hay solicitudes de cancelacion.</p>');e.innerHTML=r.map((t=>{const e="cancelled"===t.status?"cancel-card--aceptada":"cancelling"===t.status?"cancel-card--proceso":!1===t.cancel_accepted?"cancel-card--rechazada":"cancel-card--proceso",a="cancelled"===t.status?c("aceptada"):!1===t.cancel_accepted?c("rechazada"):c("proceso");return`<div class="cancel-card ${e}">\n <div class="cancel-card__header">\n <span class="cancel-card__folio">${t.provisional_folio||`CFDI-${t.id}`}</span>\n ${a}\n </div>\n <div class="cancel-card__body">\n <div class="cancel-card__row">\n <span class="cancel-card__row-label">Cliente</span>\n <span class="cancel-card__row-value">${t.customer_name||"-"}</span>\n </div>\n <div class="cancel-card__row">\n <span class="cancel-card__row-label">RFC</span>\n <span class="cancel-card__row-value" style="font-family:var(--font-mono);font-size:0.8rem;">${t.rfc||"-"}</span>\n </div>\n <div class="cancel-card__row">\n <span class="cancel-card__row-label">Motivo</span>\n <span class="cancel-card__row-value">${t.cancel_motive||"-"}</span>\n </div>\n <div class="cancel-card__row">\n <span class="cancel-card__row-label">Monto</span>\n <span class="cancel-card__row-value" style="font-family:var(--font-mono);font-weight:600;color:var(--color-text-primary);">$${n(t.total)} MXN</span>\n </div>\n </div>\n <div class="cancel-card__footer">\n <span style="font-size:var(--text-caption);color:var(--color-text-muted);">${t.cancelled_at?"Cancelada: "+new Date(t.cancelled_at).toLocaleDateString("es-MX"):t.created_at?"Solicitada: "+new Date(t.created_at).toLocaleDateString("es-MX"):""}</span>\n <button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${t.id})">Ver detalle</button>\n </div>\n </div>`})).join("")}catch(t){e.innerHTML=`<p style="color:var(--color-error);padding:var(--space-4);">Error: ${t.message}</p>`}}function d(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}let p=null;function u(t){p=t;const e=document.getElementById("modalCancelOverlay");e&&(e.style.display="flex")}async function m(){if(!p)return;const t=document.getElementById("modalCancelOverlay");if(!t)return;const e=t.querySelector('input[name="motivo-sat"]:checked');if(!e)return void alert("Selecciona un motivo de cancelacion.");const n=e.value,o=t.querySelector('input[type="text"]'),c=o?o.value.trim():"";if("01"!==n||c){if(confirm("Confirmar cancelacion ante el SAT?"))try{const e={motive:n};c&&(e.replacement_uuid=c),await a(`/cancel/${p}`,{method:"POST",body:JSON.stringify(e)}),t.style.display="none",p=null,r(),alert("CFDI cancelado exitosamente.")}catch(t){alert("Error: "+t.message)}}else alert("UUID sustituto requerido para motivo 01.")}function v(){const t=document.getElementById("newInvoiceModalOverlay");if(!t)return;const e=t.querySelector("#invoiceSaleId");e&&(e.value=""),document.getElementById("invoiceResult").innerHTML="",t.style.display="flex"}function y(){const t=document.getElementById("newInvoiceModalOverlay");t&&(t.style.display="none")}async function g(){const t=parseInt(document.getElementById("invoiceSaleId").value),e=document.getElementById("invoiceResult");if(t)try{const n=await a("/invoice",{method:"POST",body:JSON.stringify({sale_id:t})});e.innerHTML='<span style="color:var(--color-success);">Factura generada: '+(n.provisional_folio||"CFDI-"+(n.id||""))+"</span>",r(),setTimeout((()=>y()),1500)}catch(t){e.innerHTML='<span style="color:var(--color-error);">Error: '+t.message+"</span>"}else e.innerHTML='<span style="color:var(--color-error);">Ingrese un ID de venta valido.</span>'}function f(){alert("Nota de credito: proximamente")}return document.addEventListener("DOMContentLoaded",(function(){(e()||(window.location.href="/pos/login",0))&&(!function(){const t=document.getElementById("live-clock");if(!t)return;const e=()=>{const e=new Date;t.textContent=e.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit",second:"2-digit"})};e(),setInterval(e,1e3)}(),function(){const t=document.getElementById("modalDetalleOverlay");if(!t)return;const e=t.querySelector('button[onclick*="modalDetalleOverlay"]');e&&(e.onclick=()=>{t.style.display="none"})}(),function(){const t=document.getElementById("modalCancelOverlay");if(!t)return;const e=t.querySelectorAll("div:last-child button");e.length>=2&&(e[e.length-1].onclick=()=>m(),e[e.length-2].onclick=()=>{t.style.display="none",p=null})}(),r())})),window.switchTab=o,window.showNewInvoiceModal=v,window.closeNewInvoiceModal=y,window.submitNewInvoice=g,window.notaCreditoPlaceholder=f,{switchTab:o,loadFacturas:r,loadNotas:s,loadComplementos:l,loadCancelaciones:i,showDetail:async function(t){const e=document.getElementById("modalDetalleOverlay");if(e)try{const o=await a(`/queue/${t}`),c=e.querySelector(".modal-card");if(!c)return;const r=c.querySelector("div > div:first-child > div:first-child"),s=c.querySelector("div > div:first-child > div:nth-child(2)");r&&(r.textContent="Detalle de Factura"),s&&(s.textContent=`${o.provisional_folio||"CFDI-"+o.id}${"stamped"===o.status?"Timbrada":"cancelled"===o.status?"Cancelada":"pending"===o.status?"Pendiente":o.status||""}`);const l=c.querySelector("div:nth-child(2)");l&&(l.innerHTML=`\n <div style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-4); margin-bottom:var(--space-6);">\n <div>\n <div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Emisor</div>\n <div style="font-weight:var(--font-weight-semibold);">${o.emisor_name||"Nexus Autoparts SA de CV"}</div>\n <div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">${o.emisor_rfc||""}</div>\n </div>\n <div>\n <div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Receptor</div>\n <div style="font-weight:var(--font-weight-semibold);">${o.customer_name||"-"}</div>\n <div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">${o.rfc||""}</div>\n </div>\n <div>\n <div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">UUID</div>\n <div style="font-family:var(--font-mono); font-size:var(--text-caption); color:var(--color-text-accent); word-break:break-all;">${o.uuid_fiscal||"Sin timbrar"}</div>\n </div>\n <div>\n <div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Total</div>\n <div style="font-family:var(--font-mono); font-size:var(--text-h4); font-weight:var(--font-weight-bold); color:var(--color-text-primary);">$${n(o.total)}</div>\n </div>\n </div>\n ${o.error_message?`<p style="color:var(--color-error);margin-bottom:var(--space-3);"><strong>Error:</strong> ${d(o.error_message)}</p>`:""}\n ${o.xml_signed||o.xml_unsigned?`\n <div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>\n <pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${d(o.xml_signed||o.xml_unsigned)}</pre>\n `:""}`);const i=c.querySelector("div:last-child button:last-child");i&&"stamped"===o.status?(i.style.display="",i.onclick=()=>{e.style.display="none",u(t)}):i&&(i.style.display="none"),e.dataset.cfdiId=t,e.dataset.saleId=o.sale_id||"",e.style.display="flex"}catch(t){alert("Error al cargar detalle: "+t.message)}},showCancelModal:u,confirmCancel:m,processQueue:async function(){if(confirm("Procesar todos los CFDIs pendientes?"))try{const t=await a("/queue/process",{method:"POST"});alert(`Procesados: ${t.processed}, Timbrados: ${t.stamped}, Fallidos: ${t.failed}`),r()}catch(t){alert("Error: "+t.message)}},showNewInvoiceModal:v,closeNewInvoiceModal:y,submitNewInvoice:g,notaCreditoPlaceholder:f}})();