Files
Autoparts-DB/pos/static/js/dashboard.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
14 KiB
JavaScript

const Dashboard=(()=>{function t(){return localStorage.getItem("pos_token")||""}function e(t){return"$"+parseFloat(t||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function n(t){return parseInt(t||0).toLocaleString("es-MX")}function a(){return(new Date).toISOString().slice(0,10)}function s(t){const e=new Date;return e.setDate(e.getDate()-t),e.toISOString().slice(0,10)}const o=["Dom","Lun","Mar","Mie","Jue","Vie","Sab"],r=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],i=["Domingo","Lunes","Martes","Miercoles","Jueves","Viernes","Sabado"];function l(t){document.documentElement.setAttribute("data-theme",t);try{localStorage.setItem("nexus-theme",t)}catch(t){}const e=document.getElementById("btn-industrial"),n=document.getElementById("btn-modern");e&&e.classList.toggle("active","industrial"===t),n&&n.classList.toggle("active","modern"===t)}function c(){const t=document.getElementById("sidebar"),e=document.getElementById("sidebar-overlay");t&&t.classList.remove("open"),e&&e.classList.remove("open"),document.body.style.overflow=""}async function d(e){try{const n=await fetch(e,{headers:{Authorization:`Bearer ${t()}`,"Content-Type":"application/json"}});return 401===n.status?(window.location.href="/pos/login",null):n.ok?await n.json():null}catch(t){return console.error("Dashboard fetch error:",e,t),null}}async function m(){const t=a(),s=await d(`/pos/api/register/daily-summary?date=${t}`);if(!s)return u("kpi-ventas-total","kpi-ventas-meta"),u("kpi-tickets-count","kpi-tickets-meta"),u("kpi-promedio-value","kpi-promedio-meta"),null;const o=document.getElementById("kpi-ventas-total"),r=document.getElementById("kpi-ventas-meta");if(o&&(o.textContent=e(s.total_sales)),r){const t=Object.entries(s.sales_by_method||{}).map((([t,n])=>`${p(t)}: ${e(n.amount)}`)).join(" | ");r.innerHTML=t?`<span class="kpi-meta-text">${t}</span>`:'<span class="kpi-meta-text">Sin ventas aun</span>'}const i=document.getElementById("kpi-tickets-count"),l=document.getElementById("kpi-tickets-meta");if(i&&(i.textContent=n(s.total_sales_count)),l){const t=s.cancelled_count||0;l.innerHTML=t>0?`<span class="kpi-tag kpi-tag--neutral">${t} cancelada${t>1?"s":""}</span><span class="kpi-meta-text">de ${s.total_sales_count+t} totales</span>`:`<span class="kpi-meta-text">${s.total_sales_count} completada${1!==s.total_sales_count?"s":""}</span>`}const c=document.getElementById("kpi-promedio-value"),m=document.getElementById("kpi-promedio-meta"),v=s.total_sales_count>0?s.total_sales/s.total_sales_count:0;c&&(c.textContent=e(v)),m&&(m.innerHTML=`<span class="kpi-meta-text">sobre ${n(s.total_sales_count)} ventas</span>`);const g=document.getElementById("kpi-cajas-count"),y=document.getElementById("kpi-cajas-meta"),f=s.registers||[],h=f.filter((t=>"open"===t.status)),k=f.filter((t=>"closed"===t.status));return g&&(g.textContent=f.length),y&&(y.innerHTML=`<span class="kpi-tag kpi-tag--up">${h.length} abierta${1!==h.length?"s":""}</span><span class="kpi-meta-text">${k.length} cerrada${1!==k.length?"s":""}</span>`),function(t){const n=document.getElementById("registers-list");if(!n)return;if(!t||0===t.length)return void(n.innerHTML='<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin cajas registradas hoy</div>');const a=Math.max(...t.map((t=>t.sale_total||0)),1);n.innerHTML=t.map(((t,n)=>{const s="open"===t.status?"kpi-tag--up":"kpi-tag--neutral",o="open"===t.status?"Abierta":"Cerrada",r=a>0?Math.round(t.sale_total/a*100):0;return`\n <div class="rank-item">\n <div class="rank-num ${0===n?"rank-num--1":1===n?"rank-num--2":""}">${n+1}</div>\n <div class="rank-item__info">\n <div class="rank-item__name">Caja ${t.register_number||t.id}</div>\n <div class="rank-item__sub">${t.employee_name||"Sin cajero"} &nbsp;&middot;&nbsp; ${t.sale_count||0} ventas &nbsp;<span class="kpi-tag ${s}" style="font-size:9px;padding:1px 6px;">${o}</span></div>\n <div class="rank-item__bar-bg">\n <div class="rank-item__bar-fill" style="width:${r}%"></div>\n </div>\n </div>\n <div class="rank-item__value">${e(t.sale_total)}</div>\n </div>`})).join("")}(f),s}function u(t,e){const n=document.getElementById(t),a=document.getElementById(e);n&&(n.textContent="--"),a&&(a.innerHTML='<span class="kpi-meta-text" style="color:var(--color-error)">Error al cargar</span>')}function p(t){return t?t.charAt(0).toUpperCase()+t.slice(1):""}function v(t,e,n,a,s){return`\n <div class="alert-item alert-item--${t}">\n <div class="alert-icon-wrap alert-icon-wrap--${t}">${s}</div>\n <div class="alert-content">\n <div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:3px;">\n <span class="alert-title">${e}</span>\n <span class="alert-badge alert-badge--${t}">${n}</span>\n </div>\n <div class="alert-desc">${a}</div>\n </div>\n <a href="/pos/inventory#alertas" class="alert-action" style="text-decoration:none;">Ver</a>\n </div>`}function g(t){const e=document.createElement("div");return e.textContent=t||"",e.innerHTML}async function y(){const t=a(),n=await d(`/pos/api/sales?date_from=${t}&date_to=${t}&per_page=10`),s=document.getElementById("recent-sales-tbody");if(!s)return;if(!n||!n.data||0===n.data.length)return void(s.innerHTML='<tr><td colspan="5" style="text-align:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</td></tr>');const o=n.data.slice(0,5),r=await Promise.all(o.map((t=>d(`/pos/api/sales/${t.id}`))));s.innerHTML=o.map(((t,n)=>{const a=r[n],s=t.created_at?t.created_at.slice(11,16):"--:--",o=t.customer_name||"Publico General",i=t.total||0,l=t.payment_method||"efectivo";let c="";a&&a.items&&a.items.length>0&&(c=a.items.slice(0,3).map((t=>`${g(t.name)}${t.quantity>1?" (x"+t.quantity+")":""}`)).join(", "),a.items.length>3&&(c+="..."));const d=function(t){const e=(t||"").toLowerCase();return e.includes("efectivo")||"cash"===e?"efectivo":e.includes("tarjeta")||"card"===e?"tarjeta":e.includes("transferencia")||"transfer"===e?"transferencia":e.includes("credito")||"credit"===e?"credito":"efectivo"}(l),m=p(l),u="cancelled"===t.status?' <span style="color:var(--color-error);font-size:10px;font-weight:700;">[CANCELADA]</span>':"";return`\n <tr>\n <td><span class="td-time">${s}</span></td>\n <td><span class="td-client">${g(o)}</span>${u}</td>\n <td class="td-products">${c}</td>\n <td><span class="td-mono">${e(i)}</span></td>\n <td><span class="pago-badge pago-badge--${d}">${m}</span></td>\n </tr>`})).join("")}function f(){if(t()||(window.location.href="/pos/login",0)){try{const t=localStorage.getItem("nexus-theme");"industrial"!==t&&"modern"!==t||l(t)}catch(t){}!function(){const e=new Date,n=e.getHours();let a="Buenos dias";n>=12&&n<19?a="Buenas tardes":n>=19&&(a="Buenas noches");let s="";try{const e=JSON.parse(atob(t().split(".")[1]));s=e.name||e.sub||""}catch(t){}const o=document.getElementById("header-greeting");o&&(o.textContent=s?`${a}, ${s}`:a);const l=document.getElementById("header-subtitle");if(l){const t=i[e.getDay()],n=e.getDate(),a=r[e.getMonth()],s=e.getFullYear();l.textContent=`${t}, ${n} de ${a} de ${s}`}}(),m(),async function(){const t=await d("/pos/api/inventory/alerts"),e=document.getElementById("alerts-grid");if(!e)return;if(!t||!t.data||0===t.data.length)return void(e.innerHTML='\n <div class="alert-item" style="border-left:4px solid var(--color-success);justify-content:center;">\n <div class="alert-icon-wrap" style="background-color:rgba(34,197,94,0.1);color:var(--color-success);">\n <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.5"/><path d="M6 9l2 2 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n <div class="alert-content">\n <span class="alert-title">Sin alertas</span>\n <div class="alert-desc">Todo el inventario dentro de parametros normales.</div>\n </div>\n </div>');const n=t.data,a=n.filter((t=>"zero"===t.type)),s=n.filter((t=>"low"===t.type)),o=n.filter((t=>"over"===t.type));let r="";if(a.length>0){const t=a.slice(0,5).map((t=>`${t.name}: <strong>${t.stock} uds</strong>`)).join(" &middot; ");r+=v("error","Stock Agotado",`${a.length} items`,t,'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 3L16.5 15H1.5L9 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9 8v3M9 12.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>')}if(s.length>0){const t=s.slice(0,5).map((t=>`${t.name}: <strong>${t.stock} uds</strong> (min ${t.min_stock})`)).join(" &middot; ");r+=v("warning","Stock Bajo",`${s.length} items`,t,'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 3L16.5 15H1.5L9 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9 8v3M9 12.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>')}if(o.length>0){const t=o.slice(0,5).map((t=>`${t.name}: <strong>${t.stock} uds</strong> (max ${t.max_stock})`)).join(" &middot; ");r+=v("orange","Sobrestock",`${o.length} items`,t,'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="2" y="2" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M9 6v3M9 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>')}e.innerHTML=r||'<div class="alert-item" style="border-left:none;justify-content:center;color:var(--color-text-muted);">Sin alertas</div>'}(),async function(){const t=a(),n=await d(`/pos/api/sales?date_from=${t}&date_to=${t}&status=completed&per_page=200`),s=document.getElementById("top-products-list");if(!s)return;if(!n||!n.data||0===n.data.length)return void(s.innerHTML='<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</div>');const o=n.data.slice(0,20),r=await Promise.all(o.map((t=>d(`/pos/api/sales/${t.id}`)))),i={};for(const t of r)if(t&&t.items)for(const e of t.items){const t=e.part_number||e.name;i[t]||(i[t]={name:e.name,part_number:e.part_number||"",qty:0,revenue:0}),i[t].qty+=e.quantity||0,i[t].revenue+=e.subtotal||0}const l=Object.values(i).sort(((t,e)=>e.revenue-t.revenue)).slice(0,5);if(0===l.length)return void(s.innerHTML='<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin productos vendidos</div>');const c=l[0].revenue||1;s.innerHTML=l.map(((t,n)=>{const a=Math.round(t.revenue/c*100);return`\n <div class="rank-item">\n <div class="rank-num ${0===n?"rank-num--1":1===n?"rank-num--2":""}">${n+1}</div>\n <div class="rank-item__info">\n <div class="rank-item__name">${g(t.name)}</div>\n <div class="rank-item__sub">${g(t.part_number)} &nbsp;&middot;&nbsp; ${t.qty} pzas vendidas</div>\n <div class="rank-item__bar-bg">\n <div class="rank-item__bar-fill" style="width:${a}%"></div>\n </div>\n </div>\n <div class="rank-item__value">${e(t.revenue)}</div>\n </div>`})).join("")}(),async function(){const t=document.getElementById("bar-chart"),n=document.getElementById("chart-week-total");if(!t)return;const r=[];for(let t=6;t>=0;t--)r.push(s(t));const i=await Promise.all(r.map((t=>d(`/pos/api/register/daily-summary?date=${t}`))));let l=0;const c=r.map(((t,e)=>{const n=i[e],s=n&&n.total_sales||0;l+=s;const r=new Date(t+"T12:00:00");return{label:o[r.getDay()],total:s,isToday:t===a()}}));n&&(n.innerHTML=`Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${e(l)}</strong>`);const m=Math.max(...c.map((t=>t.total)),1);t.innerHTML=c.map((t=>{const n=Math.max(Math.round(t.total/m*100),2),a=t.isToday?" today":"",s=t.isToday?' style="color:var(--color-primary);font-weight:700;"':"";return`\n <div class="bar-chart__col">\n <div class="bar-chart__bar-wrap">\n <div class="bar-chart__bar${a}" style="height:${n}%" data-value="${t.isToday?`${e(t.total)} ← Hoy`:e(t.total)}"></div>\n </div>\n <span class="bar-chart__label"${s}>${t.label}</span>\n </div>`})).join("")}(),y(),setInterval((()=>{m(),y()}),12e4)}}return window.setTheme=l,window.toggleSidebar=function(){const t=document.getElementById("sidebar"),e=document.getElementById("sidebar-overlay");if(!t)return;const n=t.classList.contains("open");t.classList.toggle("open",!n),e&&e.classList.toggle("open",!n),document.body.style.overflow=n?"":"hidden"},window.closeSidebar=c,window.addEventListener("resize",(function(){window.innerWidth>=768&&c()})),window.setPeriod=function(t){t.closest(".period-selector").querySelectorAll(".period-btn").forEach((function(t){t.classList.remove("active")})),t.classList.add("active")},document.addEventListener("DOMContentLoaded",f),{init:f,setTheme:l}})();