- customers.min.js, fleet.min.js, inventory.min.js, pos-utils.min.js, sidebar.min.js, virtual-scroll.min.js
1 line
25 KiB
JavaScript
1 line
25 KiB
JavaScript
const Customers=(()=>{let e=localStorage.getItem("pos_token")||"",t=1,n=1,o=null,a=null;const i=e=>"$"+parseFloat(e||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2});async function l(t,n={}){n.headers={"Content-Type":"application/json",Authorization:"Bearer "+e};const o=await fetch(t,n);if(401===o.status)return void(window.location.href="/pos/login");const a=await o.json();if(!o.ok)throw new Error(a.error||`HTTP ${o.status}`);return a}const d={1:"Mostrador",2:"Taller",3:"Mayoreo"},r={1:"mostrador",2:"taller",3:"mayoreo"};function s(e){return e.credit_balance>0&&e.credit_limit>0&&e.credit_balance>e.credit_limit?'<span class="badge badge--warning"><span class="badge-dot"></span>Mora</span>':'<span class="badge badge--active"><span class="badge-dot"></span>Activo</span>'}function c(e){if(!e)return"--";const t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e.substring(0,2).toUpperCase()}function m(e){if(!e)return"-";try{return new Date(e).toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"})}catch(t){return e}}async function p(e,o){e=e||t;const a=document.getElementById("searchInput");o=void 0!==o?o:a&&a.value||"";try{const a=new URLSearchParams({page:e,per_page:50});o&&a.append("q",o);const i=await l(`/pos/api/customers?${a}`);!function(e){const t=document.getElementById("customersBody");if(!t)return;if(!e||0===e.length)return void(t.innerHTML='<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>');g||(g=new VirtualScroll({container:t,rowHeight:52,buffer:3,renderRow:u,emptyHtml:'<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>'}));g.setData(e)}(i.data||[]),function(e){const o=document.querySelector(".pagination"),a=document.getElementById("tableInfo");if(!e||!e.total_pages)return o&&(o.innerHTML=""),void(a&&(a.textContent=""));n=e.total_pages,t=e.page;const i=e.total||0,l=e.per_page||50,d=(e.page-1)*l+1,r=Math.min(e.page*l,i);a&&(a.textContent=`Mostrando ${d}–${r} de ${i.toLocaleString("es-MX")} clientes`);if(!o)return;if(n<=1)return void(o.innerHTML="");let s="";s+=`<button class="page-btn" ${e.page<=1?"disabled":""} onclick="Customers.goToPage(${e.page-1})">‹</button>`;const c=7;let m=Math.max(1,e.page-3),p=Math.min(n,m+c-1);p-m<c-1&&(m=Math.max(1,p-c+1));for(let t=m;t<=p;t++)s+=`<button class="page-btn ${t===e.page?"active":""}" onclick="Customers.goToPage(${t})">${t}</button>`;p<n&&(s+='<span style="color:var(--color-text-muted);font-size:var(--text-caption);padding:0 4px;">...</span>',s+=`<button class="page-btn" onclick="Customers.goToPage(${n})">${n}</button>`);s+=`<button class="page-btn" ${e.page>=n?"disabled":""} onclick="Customers.goToPage(${e.page+1})">›</button>`,o.innerHTML=s}(i.pagination||{})}catch(e){console.error("Load customers failed:",e)}}function u(e){const t=d[e.price_tier]||"Mostrador",n=r[e.price_tier]||"mostrador",a=parseFloat(e.credit_limit||0),l=parseFloat(e.credit_balance||0),c=Math.max(0,a-l),p=a>0?Math.round(l/a*100):0,u=p>=80?"none":p>=60?"low":"",g=String(e.id).padStart(5,"0");return'<tr class="'+(o&&o.id===e.id?"selected":"")+'" onclick="selectCustomer('+e.id+')"><td class="cell-num">'+g+'</td><td><div class="cell-name">'+(e.name||"")+'</div><div class="cell-name-sub hide-mobile">'+(e.email||"")+'</div></td><td class="cell-rfc hide-mobile">'+(e.rfc||"-")+'</td><td class="hide-mobile">'+(e.phone||"-")+'</td><td class="hide-mobile" style="font-size:var(--text-caption);color:var(--color-text-secondary);">'+(e.email||"-")+'</td><td><span class="tipo-chip tipo-chip--'+n+'">'+t+'</span></td><td class="cell-credit '+u+'">'+i(c)+'</td><td class="cell-date hide-mobile">'+m(e.last_purchase||e.created_at)+"</td><td>"+s(e)+"</td></tr>"}var g=null;function v(){clearTimeout(a),a=setTimeout((()=>{t=1,p(1)}),300)}async function f(e){try{const n=await l(`/pos/api/customers/${e}`);o=n;const a=document.getElementById("detailEmpty"),u=document.getElementById("detailContent");a&&(a.style.display="none"),u&&(u.style.display="flex");const g=document.getElementById("detailAvatar");g&&(g.textContent=c(n.name));const v=document.getElementById("detailName");v&&(v.textContent=(n.name||"").toUpperCase());const f=document.getElementById("detailRFC");f&&(f.textContent=n.rfc||"-");const y=document.getElementById("detailTipo");if(y){const e=d[n.price_tier]||"Mostrador",t=r[n.price_tier]||"mostrador";y.textContent=e,y.className=`tipo-chip tipo-chip--${t}`}const b=document.getElementById("detailStatus");b&&(b.innerHTML=s(n));const h=(e,t)=>{const n=document.getElementById(e);n&&(n.textContent=t||"-")};h("detailAddress",n.address),h("detailPhone",n.phone),h("detailEmail",n.email),h("detailSince",m(n.created_at)),h("detailLastPurchase",m(n.last_purchase));const x=parseFloat(n.credit_limit||0),C=parseFloat(n.credit_balance||0),E=Math.max(0,x-C),I=x>0?Math.round(C/x*100):0;h("detailCreditLimit",i(x)),h("detailCreditAvail",i(E)),h("detailCreditUsed",i(C)),h("detailCreditPct",I+"%");const B=document.getElementById("detailCreditBar");B&&(B.style.width=I+"%",B.className="progress-bar__fill "+(I<40?"low":I>75?"high":""));const w=document.getElementById("historyBody");if(w){const e=n.recent_purchases||[];0===e.length?w.innerHTML='<tr><td colspan="4" style="text-align:center;color:var(--color-text-muted);padding:var(--space-4);">Sin compras recientes</td></tr>':(w.innerHTML="",e.forEach((e=>{const t="paid"===e.status?"mbadge--paid":"overdue"===e.status?"mbadge--overdue":"mbadge--pending",n="paid"===e.status?"Pagado":"overdue"===e.status?"Vencido":"Pendiente",o=document.createElement("tr");o.innerHTML=`\n <td class="date">${m(e.created_at)}</td>\n <td class="folio">NX-${String(e.id).padStart(5,"0")}</td>\n <td class="total">${i(e.total)}</td>\n <td><span class="mbadge ${t}">${n}</span></td>\n `,w.appendChild(o)})))}!function(e){const t=(e,t)=>{const n=document.getElementById(e);n&&(n.textContent=t||"--")};t("panelAvatar",c(e.name)),t("panelName",e.name),t("panelRfc","RFC: "+(e.rfc||"-"));const n=parseFloat(e.credit_limit||0),o=parseFloat(e.credit_balance||0),a=Math.max(0,n-o),l=n>0?Math.round(o/n*100):0;t("panelCreditLimit",i(n)),t("panelCreditUsed",i(o)),t("panelCreditAvail",i(a));const d=document.getElementById("panelCreditBar");d&&(d.style.width=l+"%");const r=document.getElementById("panelVehicles");if(r){const t=e.vehicle_info||[];0===t.length?r.innerHTML='<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin vehiculos registrados</span>':r.innerHTML=t.map((e=>`<div style="padding:4px 0;border-bottom:1px solid var(--color-border);">\n <strong>${e.make||""} ${e.model||""} ${e.year||""}</strong>\n ${e.plates?` <span style="color:var(--color-text-muted);">Placas: ${e.plates}</span>`:""}\n </div>`)).join("")}const s=document.getElementById("panelPurchases");if(s){const t=e.recent_purchases||[];0===t.length?s.innerHTML='<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin compras recientes</span>':s.innerHTML=t.slice(0,5).map((e=>`<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);font-size:var(--text-body-sm);">\n <span>NX-${String(e.id).padStart(5,"0")} — ${m(e.created_at)}</span>\n <span>${i(e.total)}</span>\n </div>`)).join("")}}(n),p(t)}catch(e){console.error("Error loading customer:",e)}}function y(){const e=document.getElementById("customerModal");e&&(document.getElementById("modalTitle").textContent="Nuevo Cliente",document.getElementById("editId").value="",document.getElementById("fName").value="",document.getElementById("fRfc").value="",document.getElementById("fRazonSocial").value="",document.getElementById("fRegimenFiscal").value="",document.getElementById("fUsoCfdi").value="G03",document.getElementById("fCp").value="",document.getElementById("fPhone").value="",document.getElementById("fEmail").value="",document.getElementById("fAddress").value="",document.getElementById("fPriceTier").value="1",document.getElementById("fCreditLimit").value="0",e.classList.add("active"),document.getElementById("fName").focus())}function b(){if(!o)return;const e=o,t=document.getElementById("customerModal");t&&(document.getElementById("modalTitle").textContent="Editar Cliente",document.getElementById("editId").value=e.id,document.getElementById("fName").value=e.name||"",document.getElementById("fRfc").value=e.rfc||"",document.getElementById("fRazonSocial").value=e.razon_social||"",document.getElementById("fRegimenFiscal").value=e.regimen_fiscal||"",document.getElementById("fUsoCfdi").value=e.uso_cfdi||"G03",document.getElementById("fCp").value=e.cp||"",document.getElementById("fPhone").value=e.phone||"",document.getElementById("fEmail").value=e.email||"",document.getElementById("fAddress").value=e.address||"",document.getElementById("fPriceTier").value=e.price_tier||"1",document.getElementById("fCreditLimit").value=e.credit_limit||0,t.classList.add("active"))}function h(){const e=document.getElementById("customerModal");e&&e.classList.remove("active")}async function x(){if(!o)return;const e=document.getElementById("statementModal");if(!e)return;const t=document.getElementById("statementName");t&&(t.textContent=o.name);const n=document.getElementById("statementContent");n&&(n.innerHTML='<div style="text-align:center;padding:20px;color:var(--color-text-muted);">Cargando...</div>'),e.classList.add("active");try{const e=await l(`/pos/api/customers/${o.id}/statement`);let t=`<div style="margin-bottom:12px;font-size:14px;">\n <strong>Saldo actual: ${i(e.balance)}</strong> |\n Limite: ${i(e.customer?e.customer.credit_limit:0)}\n </div>`;e.entries&&0!==e.entries.length?(t+='<table style="width:100%;border-collapse:collapse;font-size:13px;">',t+='<tr style="background:var(--color-surface-2);"><th style="padding:8px;text-align:left;">Fecha</th><th style="text-align:left;">Concepto</th><th style="text-align:right;padding:8px;">Cargo</th><th style="text-align:right;padding:8px;">Abono</th><th style="text-align:right;padding:8px;">Saldo</th></tr>',e.entries.forEach((e=>{t+=`<tr style="border-bottom:1px solid var(--color-border);">\n <td style="padding:6px 8px;">${m(e.date)}</td>\n <td>${e.description||""}</td>\n <td style="text-align:right;padding:6px 8px;">${"charge"===e.type?i(e.amount):""}</td>\n <td style="text-align:right;padding:6px 8px;">${"payment"===e.type?i(e.amount):""}</td>\n <td style="text-align:right;padding:6px 8px;">${i(e.running_balance)}</td>\n </tr>`})),t+="</table>"):t+='<div style="color:var(--color-text-muted);padding:20px;text-align:center;">Sin movimientos</div>',n&&(n.innerHTML=t)}catch(e){n&&(n.innerHTML=`<div style="color:var(--color-error);padding:20px;text-align:center;">Error: ${e.message}</div>`)}}function C(){const e=document.getElementById("paymentModal");e&&e.classList.remove("active")}function E(e){f(e),"function"==typeof openSlidePanel&&openSlidePanel()}return window.filterCustomers=function(){v()},e?(window.openNewCustomerModal=y,function(){const e=document.querySelectorAll(".quick-actions .action-btn");e.length>=1&&(e[0].onclick=()=>{o&&(window.location.href="/pos/?customer="+o.id)}),e.length>=2&&(e[1].onclick=()=>b()),e.length>=3&&(e[2].onclick=()=>x()),e.length>=4&&(e[3].onclick=()=>{o&&f(o.id)})}(),window.viewCustomer=E,function(){if(!document.getElementById("customerModal")){const e=document.createElement("div");e.innerHTML='\n <div id="customerModal" class="modal-overlay" style="display:none;">\n <div class="modal-box">\n <div class="modal-header">\n <h3 id="modalTitle">Nuevo Cliente</h3>\n <button class="btn-close" onclick="Customers.closeModal()">×</button>\n </div>\n <div class="modal-body">\n <input type="hidden" id="editId" />\n <div class="form-row">\n <div class="form-group"><label>Nombre *</label><input type="text" id="fName" class="form-input" placeholder="Nombre del cliente" /></div>\n <div class="form-group"><label>RFC</label><input type="text" id="fRfc" class="form-input" placeholder="RFC" maxlength="13" /></div>\n </div>\n <div class="form-row">\n <div class="form-group"><label>Razon Social</label><input type="text" id="fRazonSocial" class="form-input" placeholder="Razon Social" /></div>\n <div class="form-group"><label>Regimen Fiscal</label>\n <select id="fRegimenFiscal" class="form-input">\n <option value="">-- Seleccionar --</option>\n <option value="601">601 - General de Ley PM</option>\n <option value="603">603 - Personas Morales Fines No Lucrativos</option>\n <option value="605">605 - Sueldos y Salarios</option>\n <option value="606">606 - Arrendamiento</option>\n <option value="608">608 - Demas Ingresos</option>\n <option value="612">612 - Personas Fisicas Empresariales</option>\n <option value="616">616 - Sin Obligaciones Fiscales</option>\n <option value="621">621 - Incorporacion Fiscal</option>\n <option value="625">625 - RESICO</option>\n <option value="626">626 - RESICO PM</option>\n </select>\n </div>\n </div>\n <div class="form-row">\n <div class="form-group"><label>Uso CFDI</label>\n <select id="fUsoCfdi" class="form-input">\n <option value="G01">G01 - Adquisicion de mercancias</option>\n <option value="G03" selected>G03 - Gastos en general</option>\n <option value="I01">I01 - Construcciones</option>\n <option value="I08">I08 - Otra maquinaria</option>\n <option value="P01">P01 - Por definir</option>\n <option value="S01">S01 - Sin efectos fiscales</option>\n </select>\n </div>\n <div class="form-group"><label>Codigo Postal</label><input type="text" id="fCp" class="form-input" placeholder="C.P." maxlength="5" /></div>\n </div>\n <div class="form-row">\n <div class="form-group"><label>Telefono</label><input type="text" id="fPhone" class="form-input" placeholder="Telefono" /></div>\n <div class="form-group"><label>Email</label><input type="email" id="fEmail" class="form-input" placeholder="correo@ejemplo.com" /></div>\n </div>\n <div class="form-group"><label>Direccion</label><input type="text" id="fAddress" class="form-input" placeholder="Direccion" /></div>\n <div class="form-row">\n <div class="form-group"><label>Tipo Precio</label>\n <select id="fPriceTier" class="form-input">\n <option value="1">Mostrador</option>\n <option value="2">Taller</option>\n <option value="3">Mayoreo</option>\n </select>\n </div>\n <div class="form-group"><label>Limite de Credito</label><input type="number" id="fCreditLimit" class="form-input" value="0" min="0" step="1000" /></div>\n </div>\n </div>\n <div class="modal-footer">\n <button class="btn btn-secondary" onclick="Customers.closeModal()">Cancelar</button>\n <button class="btn btn-primary" onclick="Customers.save()">Guardar</button>\n </div>\n </div>\n </div>',document.body.appendChild(e)}if(!document.getElementById("statementModal")){const e=document.createElement("div");e.innerHTML='\n <div id="statementModal" class="modal-overlay" style="display:none;">\n <div class="modal-box modal-box--wide">\n <div class="modal-header">\n <h3>Estado de Cuenta — <span id="statementName"></span></h3>\n <button class="btn-close" onclick="Customers.closeStatement()">×</button>\n </div>\n <div class="modal-body" id="statementContent" style="max-height:60vh;overflow-y:auto;">\n </div>\n <div class="modal-footer">\n <button class="btn btn-primary" onclick="Customers.showPaymentModal()">Registrar Abono</button>\n <button class="btn btn-secondary" onclick="Customers.closeStatement()">Cerrar</button>\n </div>\n </div>\n </div>',document.body.appendChild(e)}if(!document.getElementById("paymentModal")){const e=document.createElement("div");e.innerHTML='\n <div id="paymentModal" class="modal-overlay" style="display:none;">\n <div class="modal-box">\n <div class="modal-header">\n <h3>Registrar Abono</h3>\n <button class="btn-close" onclick="Customers.closePayment()">×</button>\n </div>\n <div class="modal-body">\n <p style="margin-bottom:var(--space-4);color:var(--color-text-secondary);">Cliente: <strong id="paymentCustomerName"></strong></p>\n <div class="form-row">\n <div class="form-group"><label>Monto</label><input type="number" id="paymentAmount" class="form-input" placeholder="0.00" min="0" step="0.01" /></div>\n <div class="form-group"><label>Metodo</label>\n <select id="paymentMethod" class="form-input">\n <option value="cash">Efectivo</option>\n <option value="transfer">Transferencia</option>\n <option value="card">Tarjeta</option>\n <option value="check">Cheque</option>\n </select>\n </div>\n </div>\n <div class="form-group"><label>Referencia</label><input type="text" id="paymentRef" class="form-input" placeholder="Num. referencia (opcional)" /></div>\n </div>\n <div class="modal-footer">\n <button class="btn btn-secondary" onclick="Customers.closePayment()">Cancelar</button>\n <button class="btn btn-primary" onclick="Customers.recordPayment()">Registrar</button>\n </div>\n </div>\n </div>',document.body.appendChild(e)}if(!document.getElementById("modalStyles")){const e=document.createElement("style");e.id="modalStyles",e.textContent="\n .modal-overlay {\n position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n background: rgba(0,0,0,0.5); z-index: 9000;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(2px);\n }\n .modal-overlay.active { display: flex !important; }\n .modal-box {\n background: var(--color-bg-elevated); border: 1px solid var(--color-border);\n border-radius: var(--radius-lg); width: 90%; max-width: 600px;\n max-height: 85vh; overflow-y: auto;\n box-shadow: var(--shadow-xl);\n }\n .modal-box--wide { max-width: 800px; }\n .modal-header {\n display: flex; align-items: center; justify-content: space-between;\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--color-border);\n }\n .modal-header h3 {\n font-family: var(--font-heading); font-size: var(--text-h6);\n font-weight: var(--heading-weight-primary); color: var(--color-text-primary);\n letter-spacing: var(--tracking-wide); text-transform: uppercase;\n }\n .modal-body { padding: var(--space-5); }\n .modal-footer {\n display: flex; justify-content: flex-end; gap: var(--space-3);\n padding: var(--space-4) var(--space-5);\n border-top: 1px solid var(--color-border);\n }\n .form-row { display: flex; gap: var(--space-4); margin-bottom: var(--space-4); }\n .form-row > .form-group { flex: 1; }\n .form-group { margin-bottom: var(--space-4); }\n .form-group label {\n display: block; font-size: var(--text-caption); font-weight: var(--font-weight-semibold);\n color: var(--color-text-muted); text-transform: uppercase; letter-spacing: var(--tracking-wider);\n margin-bottom: var(--space-1);\n }\n .form-input {\n width: 100%; height: 38px; padding: 0 var(--space-3);\n background: var(--color-bg-overlay); border: 1.5px solid var(--color-border);\n border-radius: var(--radius-md); font-family: var(--font-body);\n font-size: var(--text-body-sm); color: var(--color-text-primary); outline: none;\n transition: var(--transition-fast);\n }\n .form-input:focus { border-color: var(--color-border-focus); box-shadow: var(--shadow-focus); }\n select.form-input { cursor: pointer; }\n ",document.head.appendChild(e)}}(),p(),async function(){try{const e=await l("/pos/api/customers?page=1&per_page=1"),t=e.pagination?e.pagination.total:0,n=document.querySelectorAll(".summary-card__value");n.length>=1&&(n[0].textContent=t.toLocaleString("es-MX"))}catch(e){}}()):window.location.href="/pos/login",{search:v,goToPage:function(e){e<1||e>n||(t=e,p(e))},loadCustomers:p,showDetail:E,selectCustomer:f,closeDetail:function(){o=null;const e=document.getElementById("detailEmpty"),n=document.getElementById("detailContent");e&&(e.style.display="flex"),n&&(n.style.display="none"),p(t)},showCreateModal:y,editCurrent:b,closeModal:h,save:async function(){const e=document.getElementById("fName"),t=e?e.value.trim():"";if(!t)return void alert("Nombre es requerido");const n=e=>{const t=document.getElementById(e);return t?t.value.trim():""},a={name:t,rfc:n("fRfc")||null,razon_social:n("fRazonSocial")||null,regimen_fiscal:n("fRegimenFiscal")||null,uso_cfdi:n("fUsoCfdi")||"G03",cp:n("fCp")||null,phone:n("fPhone")||null,email:n("fEmail")||null,address:n("fAddress")||null,price_tier:parseInt(n("fPriceTier"))||1,credit_limit:parseFloat(n("fCreditLimit"))||0},i=n("editId");try{i?await l(`/pos/api/customers/${i}`,{method:"PUT",body:JSON.stringify(a)}):await l("/pos/api/customers",{method:"POST",body:JSON.stringify(a)}),h(),p(),i&&o&&f(i)}catch(e){alert("Error: "+e.message)}},showStatement:x,closeStatement:function(){const e=document.getElementById("statementModal");e&&e.classList.remove("active")},showPaymentModal:function(){if(!o)return;const e=document.getElementById("paymentModal");e&&(document.getElementById("paymentCustomerName").textContent=o.name,document.getElementById("paymentAmount").value="",document.getElementById("paymentMethod").value="cash",document.getElementById("paymentRef").value="",e.classList.add("active"),document.getElementById("paymentAmount").focus())},closePayment:C,recordPayment:async function(){if(!o)return;const e=parseFloat(document.getElementById("paymentAmount").value);if(!e||e<=0)return void alert("Ingresa un monto valido");const t=document.getElementById("paymentMethod").value,n=document.getElementById("paymentRef").value.trim();try{await l(`/pos/api/customers/${o.id}/payment`,{method:"POST",body:JSON.stringify({amount:e,method:t,reference:n})}),C(),f(o.id)}catch(e){alert("Error: "+e.message)}}}})(); |