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 Accounting=(()=>{function a(){return localStorage.getItem("pos_token")||""}async function e(e,n={}){const t=await fetch(`/pos/api/accounting${e}`,{headers:{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"},...n});if(!t.ok){const a=await t.json().catch((()=>({error:t.statusText})));throw new Error(a.error||"Request failed")}return t.json()}function n(a){return parseFloat(a||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function t(a){document.querySelectorAll(".tab-btn").forEach((a=>{a.classList.remove("is-active"),a.setAttribute("aria-selected","false")})),document.querySelectorAll(".tab-panel").forEach((a=>a.classList.remove("is-active")));const e=document.querySelector(`.tab-btn[onclick*="'${a}'"]`);e&&(e.classList.add("is-active"),e.setAttribute("aria-selected","true"));const n=document.getElementById(`panel-${a}`);n&&n.classList.add("is-active"),"cxc"===a&&o(),"cxp"===a&&c(),"balance"===a&&s(),"resultados"===a&&i(),"flujo"===a&&l(),"conciliacion"===a&&d(),"cierre"===a&&p()}function r(a,e){return`<span class="badge ${{pending:"badge--pending",vigente:"badge--pending",overdue:"badge--overdue",vencida:"badge--overdue",partial:"badge--partial",parcial:"badge--partial",ok:"badge--ok",pagada:"badge--ok",open:"badge--pending",closed:"badge--ok"}[a]||""}">${e||a}</span>`}async function o(){const a=document.getElementById("panel-cxc");if(!a)return;const t=a.querySelector(".data-table tbody");if(t)try{const o=await e("/aging"),c=o.data||[];if(!c.length)return void(t.innerHTML='<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por cobrar.</td></tr>');t.innerHTML=c.map((a=>{const e=a.days_overdue>0?"overdue":a.paid>0&&a.balance>0?"partial":a.balance<=0?"ok":"pending",t="overdue"===e?"Vencida":"partial"===e?"Parcial":"ok"===e?"Pagada":"Vigente";return`<tr>\n <td class="td--mono">${a.invoice||a.folio||"-"}</td>\n <td class="td--primary">${a.name||a.customer_name||"-"}</td>\n <td>${a.issue_date?new Date(a.issue_date).toLocaleDateString("es-MX"):"-"}</td>\n <td>${a.due_date?new Date(a.due_date).toLocaleDateString("es-MX"):"-"}</td>\n <td class="td--amount">$${n(a.total)}</td>\n <td class="td--amount">$${n(a.paid||0)}</td>\n <td class="td--amount">$${n(a.balance||a.total)}</td>\n <td>${r(e,t)}</td>\n <td><button class="btn btn--ghost btn--sm">${a.balance>0?"Cobrar":"Ver"}</button></td>\n </tr>`})).join("");const s=a.querySelector(".pagination span");s&&(s.textContent=`Mostrando 1-${c.length} de ${o.totals?.count||c.length} registros`),function(a,e){const n=document.querySelector(`.tab-btn[onclick*="'${a}'"]`);if(!n)return;const t=n.querySelector(".tab-btn__badge");t&&(t.textContent=e)}("cxc",c.length)}catch(a){t.innerHTML=`<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${a.message}</td></tr>`}}async function c(){const a=document.getElementById("panel-cxp");if(!a)return;const t=a.querySelector(".data-table tbody");if(t)try{const o=await e("/aging?type=payable"),c=o.data||[];if(!c.length)return void(t.innerHTML='<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por pagar.</td></tr>');t.innerHTML=c.map((a=>{const e=a.days_overdue>0?"overdue":a.paid>0&&a.balance>0?"partial":a.balance<=0?"ok":"pending",t="overdue"===e?"Vencida":"partial"===e?"Parcial":"ok"===e?"Pagada":"Vigente";return`<tr>\n <td class="td--mono">${a.invoice||a.folio||"-"}</td>\n <td class="td--primary">${a.name||a.vendor_name||"-"}</td>\n <td>${a.receipt_date?new Date(a.receipt_date).toLocaleDateString("es-MX"):"-"}</td>\n <td>${a.due_date?new Date(a.due_date).toLocaleDateString("es-MX"):"-"}</td>\n <td class="td--amount">$${n(a.total)}</td>\n <td class="td--amount">$${n(a.paid||0)}</td>\n <td class="td--amount">$${n(a.balance||a.total)}</td>\n <td>${r(e,t)}</td>\n <td><button class="btn btn--ghost btn--sm">${a.balance>0?"Pagar":"Ver"}</button></td>\n </tr>`})).join("");const s=a.querySelector(".pagination span");s&&(s.textContent=`Mostrando 1-${c.length} de ${o.totals?.count||c.length} registros`)}catch(a){t.innerHTML=`<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${a.message}</td></tr>`}}async function s(){const a=document.getElementById("panel-balance");if(!a)return;const t=a.querySelector(".finance-grid");if(t)try{const r=new Date,o=await e(`/balance-sheet?date=${r.toISOString().slice(0,10)}`),c=t.querySelector(".finance-card:first-child"),s=t.querySelector(".finance-card:last-child");if(c){let a='<div class="finance-card__header"><div class="finance-card__title">Activos</div></div>';if(o.activo&&o.activo.items)for(const e of o.activo.items){const t=e.balance<0;a+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${e.code?e.code+" ":""}${e.name}</span>\n <span class="finance-card__row-value ${t?"finance-card__row-value--negative":""}">$${n(e.balance)}</span>\n </div>`}a+=`<div class="finance-card__row finance-card__row--total">\n <span class="finance-card__row-label">Total Activos</span>\n <span class="finance-card__row-value">$${n(o.activo?.total||0)}</span>\n </div>`,c.innerHTML=a}if(s){let a='<div class="finance-card__header"><div class="finance-card__title">Pasivo + Capital</div></div>';if(o.pasivo&&o.pasivo.items){a+='<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Pasivo</span><span></span></div>';for(const e of o.pasivo.items)a+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${e.code?e.code+" ":""}${e.name}</span>\n <span class="finance-card__row-value">$${n(e.balance)}</span>\n </div>`;a+=`<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Pasivo</span><span class="finance-card__row-value">$${n(o.pasivo.total)}</span></div>`}if(o.capital&&o.capital.items){a+='<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Capital Contable</span><span></span></div>';for(const e of o.capital.items){const t=e.balance>0;a+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${e.code?e.code+" ":""}${e.name}</span>\n <span class="finance-card__row-value ${t?"finance-card__row-value--positive":""}">$${n(e.balance)}</span>\n </div>`}a+=`<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Capital</span><span class="finance-card__row-value">$${n(o.capital.total)}</span></div>`}a+=`<div class="finance-card__row finance-card__row--total">\n <span class="finance-card__row-label">Total Pasivo + Capital</span>\n <span class="finance-card__row-value">$${n((o.pasivo?.total||0)+(o.capital?.total||0))}</span>\n </div>`,s.innerHTML=a}const i=a.querySelector(".select-filter");i&&o.as_of&&(i.innerHTML=`<option>Al ${o.as_of}</option>`)}catch(a){t.innerHTML=`<p style="color:var(--color-error);padding:var(--space-4);">Error: ${a.message}</p>`}}async function i(){const a=document.getElementById("panel-resultados");if(!a)return;const t=a.querySelector(".finance-grid");if(t)try{const a=new Date,r=await e(`/income-statement?year=${a.getFullYear()}&month=${a.getMonth()+1}`),o=t.querySelector(".finance-card");if(!o)return;let c='<div class="finance-card__header"><div class="finance-card__title">Estado de Resultados</div></div>';if(c+='<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Ingresos</span><span></span></div>',r.ingresos&&r.ingresos.items)for(const a of r.ingresos.items)c+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${a.name}</span>\n <span class="finance-card__row-value">$${n(a.amount)}</span>\n </div>`;if(c+=`<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Ingresos</span><span class="finance-card__row-value">$${n(r.ingresos?.total||0)}</span></div>`,c+='<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Costo de Ventas</span><span></span></div>',r.costos&&r.costos.items)for(const a of r.costos.items)c+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${a.name}</span>\n <span class="finance-card__row-value finance-card__row-value--negative">-$${n(Math.abs(a.amount))}</span>\n </div>`;if(c+=`<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Utilidad Bruta</span><span class="finance-card__row-value finance-card__row-value--positive">$${n(r.utilidad_bruta||0)}</span></div>`,c+='<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Gastos de Operacion</span><span></span></div>',r.gastos&&r.gastos.items)for(const a of r.gastos.items)c+=`<div class="finance-card__row finance-card__row--indent">\n <span class="finance-card__row-label">${a.name}</span>\n <span class="finance-card__row-value finance-card__row-value--negative">-$${n(Math.abs(a.amount))}</span>\n </div>`;c+=`<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Gastos Operacion</span><span class="finance-card__row-value finance-card__row-value--negative">-$${n(Math.abs(r.gastos?.total||0))}</span></div>`;c+=`<div class="finance-card__row finance-card__row--total">\n <span class="finance-card__row-label">Utilidad Neta</span>\n <span class="finance-card__row-value ${(r.utilidad_neta||0)>=0?"finance-card__row-value--positive":"finance-card__row-value--negative"}">$${n(r.utilidad_neta||0)}</span>\n </div>`,o.innerHTML=c}catch(a){t.innerHTML=`<p style="color:var(--color-error);padding:var(--space-4);">Error: ${a.message}</p>`}}async function l(){}async function d(){}async function p(){const a=document.getElementById("panel-cierre");if(!a)return;const n=a.querySelector(".btn--primary");n&&!n.dataset.wired&&(n.dataset.wired="true",n.addEventListener("click",(async()=>{const a=new Date,n=a.getFullYear(),t=a.getMonth()+1;if(confirm(`Cerrar periodo ${t}/${n}? Esta accion no se puede revertir.`))try{await e("/periods/close",{method:"POST",body:JSON.stringify({year:n,month:t})}),alert("Periodo cerrado exitosamente.")}catch(a){alert("Error: "+a.message)}})))}function u(){for(var a=document.querySelectorAll("table"),e=null,n=0;n<a.length;n++)if(null!==a[n].offsetParent&&a[n].querySelector("tbody tr")){e=a[n];break}if(e){var t=[],r=e.querySelectorAll("thead th");if(r.length&&t.push(Array.from(r).map((function(a){return'"'+a.textContent.trim().replace(/"/g,'""')+'"'})).join(",")),e.querySelectorAll("tbody tr").forEach((function(a){var e=a.querySelectorAll("td");t.push(Array.from(e).map((function(a){return'"'+a.textContent.trim().replace(/"/g,'""')+'"'})).join(","))})),t.length<=1)alert("Sin datos para exportar.");else{var o=t.join("\n"),c=new Blob(["\ufeff"+o],{type:"text/csv;charset=utf-8;"}),s=URL.createObjectURL(c),i=document.createElement("a");i.href=s,i.download="contabilidad_nexus_"+(new Date).toISOString().slice(0,10)+".csv",i.click(),URL.revokeObjectURL(s)}}else alert("No hay datos para exportar en la vista actual.")}function f(){const a=document.getElementById("newEntryModalOverlay");if(!a)return;const e=a.querySelector("#entryDate");e&&!e.value&&(e.value=(new Date).toISOString().slice(0,10)),document.getElementById("entryResult").innerHTML="",a.style.display="flex"}function v(){const a=document.getElementById("newEntryModalOverlay");a&&(a.style.display="none")}function _(){const a=document.getElementById("entryLines");if(!a)return;const e=document.createElement("div");e.className="entry-line",e.style.cssText="display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:var(--space-2);margin-bottom:var(--space-2);align-items:center;",e.innerHTML='<input type="text" placeholder="Cuenta contable" class="entry-account" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" /><input type="number" placeholder="Debe" class="entry-debit" step="0.01" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" /><input type="number" placeholder="Haber" class="entry-credit" step="0.01" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" /><button class="btn btn--ghost btn--sm" onclick="this.closest(\'.entry-line\').remove()">×</button>',a.appendChild(e)}async function m(){const a=document.getElementById("entryDate").value,n=document.getElementById("entryType").value,t=document.getElementById("entryDescription").value.trim(),r=document.getElementById("entryResult");if(!a||!t)return void(r.innerHTML='<span style="color:var(--color-error);">Fecha y descripcion son obligatorios.</span>');const o=[];if(document.querySelectorAll("#entryLines .entry-line").forEach((a=>{const e=a.querySelector(".entry-account").value.trim(),n=parseFloat(a.querySelector(".entry-debit").value)||0,t=parseFloat(a.querySelector(".entry-credit").value)||0;e&&(n||t)&&o.push({account:e,debit:n,credit:t})})),o.length)try{await e("/entries",{method:"POST",body:JSON.stringify({date:a,type:n,description:t,lines:o})}),r.innerHTML='<span style="color:var(--color-success);">Poliza creada exitosamente.</span>',setTimeout((()=>v()),1200)}catch(a){r.innerHTML='<span style="color:var(--color-error);">Error: '+a.message+"</span>"}else r.innerHTML='<span style="color:var(--color-error);">Agregue al menos una partida.</span>'}return document.addEventListener("DOMContentLoaded",(function(){(a()||(window.location.href="/pos/login",0))&&(!function(){const a=document.getElementById("live-clock");if(!a)return;const e=()=>{const e=new Date;a.textContent=e.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})};e(),setInterval(e,1e3)}(),async function(){if(!(document.querySelectorAll(".summary-card").length<4))try{const a=new Date;await e(`/trial-balance?year=${a.getFullYear()}&month=${a.getMonth()+1}`)}catch(a){}}(),o())})),window.switchTab=t,window.exportarContabilidad=u,window.showNewEntryModal=f,window.closeNewEntryModal=v,window.addEntryLine=_,window.submitNewEntry=m,{switchTab:t,loadAging:o,loadAccountsPayable:c,loadBalanceSheet:s,loadIncomeStatement:i,loadCashFlow:l,loadReconciliation:d,loadPeriodClose:p,exportarContabilidad:u,showNewEntryModal:f,closeNewEntryModal:v,addEntryLine:_,submitNewEntry:m}})(); |