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
16 KiB
JavaScript
1 line
16 KiB
JavaScript
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}})(); |