feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
This commit is contained in:
@@ -732,6 +732,20 @@
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.btn--meli {
|
||||
background: #FFE600;
|
||||
color: #2D3277;
|
||||
border-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn--meli:hover {
|
||||
background: #e6cf00;
|
||||
color: #1a1f5c;
|
||||
}
|
||||
.btn--meli svg {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
DATA TABLE
|
||||
========================================================================= */
|
||||
@@ -1261,7 +1275,7 @@
|
||||
.inv-field label {
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1282,6 +1296,23 @@
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.inv-field select {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inv-field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.count-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
@@ -1301,3 +1332,48 @@
|
||||
|
||||
/* History table inside modal */
|
||||
.inv-modal .data-table { width: 100%; }
|
||||
|
||||
/* ─── MercadoLibre Category Autocomplete ─────────────────────────────── */
|
||||
.meli-cat-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.meli-cat-item {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: background var(--transition-fast);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.meli-cat-item:last-child { border-bottom: none; }
|
||||
.meli-cat-item:hover,
|
||||
.meli-cat-item.is-active {
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.meli-cat-item .cat-id {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
margin-left: 8px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.meli-cat-loading,
|
||||
.meli-cat-empty {
|
||||
padding: 12px 14px;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,22 @@ body {
|
||||
SIDEBAR — Glass treatment
|
||||
========================================================================== */
|
||||
|
||||
/* Prevent flash/stun while sidebar.js replaces static sidebar markup */
|
||||
.sidebar,
|
||||
.pos-sidebar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
background: var(--glass-bg-strong) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--glass-border) !important;
|
||||
}
|
||||
|
||||
body.sidebar-ready .sidebar,
|
||||
body.sidebar-ready .pos-sidebar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
1
pos/static/js/app-init.min.js
vendored
1
pos/static/js/app-init.min.js
vendored
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var e=localStorage.getItem("pos_token");if(e){try{var t=JSON.parse(atob(e.split(".")[1]));if(1e3*t.exp<Date.now())return localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),void(window.location.href="/pos/login")}catch(e){return localStorage.removeItem("pos_token"),void(window.location.href="/pos/login")}var o={};try{o=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var n=o.name||t.name||"Usuario",a=o.role||t.role||"",r="function"==typeof window.t?window.t:function(e){return e},i={owner:r("role_owner"),admin:r("role_admin"),cashier:r("role_cashier"),warehouse:r("role_warehouse"),accountant:r("role_accountant")}[a]||a,c=n.split(" ").map((function(e){return e[0]})).join("").toUpperCase().substring(0,2);document.querySelectorAll(".sidebar__user-name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".sidebar__user-role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".sidebar__user-avatar, .sidebar__avatar").forEach((function(e){e.textContent=c})),document.querySelectorAll(".profile-info__name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".profile-info__role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".theme-bar__label").forEach((function(e){-1===e.textContent.indexOf("Usuario:")&&-1===e.textContent.indexOf("Sucursal")||(e.textContent="Sucursal Principal — "+n)})),document.querySelectorAll(".status-bar .user-name, .status-info span").forEach((function(e){var t=e.textContent;["Hugo M.","Hugo García","J. Ramírez","José Ramírez","Carlos M.","Admin"].forEach((function(o){-1!==t.indexOf(o)&&(e.textContent=t.replace(o,n))}))}));var l=window.location.pathname;document.querySelectorAll(".nav-item, .nav-link").forEach((function(e){e.classList.remove("is-active","active"),(e.getAttribute("href")||"")===l&&(e.classList.add("is-active"),e.classList.add("active"))})),window.posLogout=function(){localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),localStorage.removeItem("pos_tenant_id"),localStorage.removeItem("pos_cart"),window.location.href="/pos/login"},document.querySelectorAll('[data-action="logout"], .btn-logout, .logout-btn').forEach((function(e){e.addEventListener("click",(function(e){e.preventDefault(),posLogout()}))}));var s=localStorage.getItem("pos_theme");s||(s=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"industrial":"modern"),document.documentElement.setAttribute("data-theme",s),document.querySelectorAll(".theme-bar").forEach((function(e){e.style.display="none"})),window.posSetTheme=function(e){document.documentElement.setAttribute("data-theme",e),localStorage.setItem("pos_theme",e)},window.setTheme=window.posSetTheme,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",(function(e){if(!localStorage.getItem("pos_theme")){var t=e.matches?"industrial":"modern";document.documentElement.setAttribute("data-theme",t)}})),setTimeout((function(){document.documentElement.setAttribute("data-theme",s)}),100),window.POS_USER={name:n,role:a,roleLabel:i,initials:c,token:e,tenantId:t.tenant_id,employeeId:t.employee_id,branchId:t.branch_id,permissions:t.permissions||[]}}else window.location.href="/pos/login"}();
|
||||
@@ -6,6 +6,7 @@
|
||||
_offset: 0,
|
||||
_limit: 50,
|
||||
_total: 0,
|
||||
_allowedBrands: [],
|
||||
|
||||
// Navigation state
|
||||
nav: {
|
||||
@@ -71,7 +72,9 @@
|
||||
},
|
||||
|
||||
loading: function(on) {
|
||||
this.el('brandCatalogLoading').style.display = on ? 'block' : 'none';
|
||||
var el = this.el('brandCatalogLoading');
|
||||
if (on) el.classList.add('is-visible');
|
||||
else el.classList.remove('is-visible');
|
||||
},
|
||||
|
||||
setContent: function(html) {
|
||||
@@ -88,23 +91,23 @@
|
||||
|
||||
buildBreadcrumb: function() {
|
||||
var parts = [];
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.loadBrands()" style="color:var(--color-primary);text-decoration:none;">Marcas</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick="BrandCatalog.loadBrands()">Marcas</a>');
|
||||
if (this.nav.brand) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.brand) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')\'>' + escapeHtml(this.nav.brand) + '</a>');
|
||||
}
|
||||
if (this.nav.model) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.model) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')\'>' + escapeHtml(this.nav.model) + '</a>');
|
||||
}
|
||||
if (this.nav.year) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')" style="color:var(--color-primary);text-decoration:none;">' + this.nav.year + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')\'>' + this.nav.year + '</a>');
|
||||
}
|
||||
if (this.nav.engine) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectEngine(' + this.nav.myeId + ',' + JSON.stringify(this.nav.engine) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.engine) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectEngine(' + this.nav.myeId + ',' + JSON.stringify(this.nav.engine) + ')\'>' + escapeHtml(this.nav.engine) + '</a>');
|
||||
}
|
||||
if (this.nav.category) {
|
||||
parts.push('<strong>' + escapeHtml(this.nav.category) + '</strong>');
|
||||
parts.push('<span class="breadcrumb__current">' + escapeHtml(this.nav.category) + '</span>');
|
||||
}
|
||||
this.setBreadcrumb(parts.join(' › '));
|
||||
this.setBreadcrumb('<nav class="breadcrumb">' + parts.join('<span class="breadcrumb__sep">›</span>') + '</nav>');
|
||||
},
|
||||
|
||||
// ---------- BRANDS ----------
|
||||
@@ -112,12 +115,10 @@
|
||||
this.loading(true);
|
||||
this.state = 'brands';
|
||||
this.reset();
|
||||
this.setBreadcrumb('<strong>Marcas de vehiculo</strong>');
|
||||
this.setBreadcrumb('<nav class="breadcrumb"><span class="breadcrumb__current">Marcas de vehiculo</span></nav>');
|
||||
this.setSearch(
|
||||
'<input type="text" id="brandSearchInput" placeholder="Buscar marca..." ' +
|
||||
'style="width:100%;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
||||
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);' +
|
||||
'outline:none;" oninput="BrandCatalog.filterBrands(this.value)">'
|
||||
'class="level-filter" oninput="BrandCatalog.filterBrands(this.value)">'
|
||||
);
|
||||
var self = this;
|
||||
fetch('/pos/api/catalog/vehicle-brands', { headers: this._headers() })
|
||||
@@ -130,24 +131,25 @@
|
||||
self.loading(false);
|
||||
self._allBrands = data.brands || [];
|
||||
if (!self._allBrands.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron marcas.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron marcas.</div></div>');
|
||||
return;
|
||||
}
|
||||
self.renderBrandList(self._allBrands);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar marcas: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar marcas</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderBrandList: function(brands) {
|
||||
var html = '';
|
||||
var html = '<div class="nav-grid">';
|
||||
brands.forEach(function(b) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ',' + b.id + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(b.name) + '</div>' +
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ',' + b.id + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(b.name) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
@@ -186,23 +188,28 @@
|
||||
self.loading(false);
|
||||
var models = data.data || [];
|
||||
if (!models.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron modelos.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron modelos.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
models.forEach(function(m) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderModelList(models);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar modelos: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar modelos</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderModelList: function(models) {
|
||||
var html = '<div class="nav-grid">';
|
||||
models.forEach(function(m) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectModel: function(modelId, modelName) {
|
||||
this.nav.model = modelName;
|
||||
this.nav.modelId = modelId;
|
||||
@@ -226,23 +233,28 @@
|
||||
self.loading(false);
|
||||
var years = data.data || [];
|
||||
if (!years.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron años.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron años.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
years.forEach(function(y) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectYear(' + y.id_year + ',' + y.year_car + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + y.year_car + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderYearList(years);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar años: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar años</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderYearList: function(years) {
|
||||
var html = '<div class="nav-grid nav-grid--years">';
|
||||
years.forEach(function(y) {
|
||||
html += '<div class="nav-card nav-card--year" onclick=\'BrandCatalog.selectYear(' + y.id_year + ',' + y.year_car + ')\'>' +
|
||||
'<div class="nav-card__name">' + y.year_car + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectYear: function(yearId, yearCar) {
|
||||
this.nav.year = yearCar;
|
||||
this.nav.yearId = yearId;
|
||||
@@ -266,24 +278,29 @@
|
||||
self.loading(false);
|
||||
var engines = data.data || [];
|
||||
if (!engines.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron motores.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron motores.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
engines.forEach(function(e) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(e.name_engine) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + escapeHtml(e.trim_level || '') + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderEngineList(engines);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar motores: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar motores</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderEngineList: function(engines) {
|
||||
var html = '<div class="nav-grid">';
|
||||
engines.forEach(function(e) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(e.name_engine) + '</div>' +
|
||||
'<div class="nav-card__sub">' + escapeHtml(e.trim_level || '') + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectEngine: function(myeId, engineName) {
|
||||
this.nav.engine = engineName;
|
||||
this.nav.myeId = myeId;
|
||||
@@ -305,26 +322,36 @@
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
self.loading(false);
|
||||
self._allowedBrands = data.allowed_brands || [];
|
||||
var categories = data.data || [];
|
||||
if (!categories.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron categorias.</p>');
|
||||
var msg = 'No se encontraron categorias.';
|
||||
if (self._allowedBrands.length) {
|
||||
msg = 'Este vehiculo no tiene cobertura de ' + self._allowedBrands.join(', ') + '.';
|
||||
}
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">' + msg + '</div><div class="empty-state__subtitle">Prueba con otro vehiculo o contacta a soporte para ampliar el catalogo.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
categories.forEach(function(c) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectCategory(' + c.id_part_category + ',' + JSON.stringify(c.name) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(c.name) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (c.part_count || 0) + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderCategoryList(categories);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar categorias: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar categorias</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderCategoryList: function(categories) {
|
||||
var html = '<div class="nav-grid">';
|
||||
categories.forEach(function(c) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectCategory(' + c.id_part_category + ',' + JSON.stringify(c.name) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(c.name) + '</div>' +
|
||||
'<div class="nav-card__sub">' + (c.part_count || 0) + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectCategory: function(catId, catName) {
|
||||
this.nav.category = catName;
|
||||
this.nav.categoryId = catId;
|
||||
@@ -340,11 +367,10 @@
|
||||
this.setSearch(
|
||||
'<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center;">' +
|
||||
'<input type="text" id="partsSearchInput" placeholder="Buscar refaccion..." value="' + escapeHtml(searchTerm || '') + '" ' +
|
||||
'style="flex:1;min-width:200px;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
||||
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);outline:none;" ' +
|
||||
'class="level-filter" ' +
|
||||
'onkeydown="if(event.key===\'Enter\')BrandCatalog.searchParts(this.value)">' +
|
||||
'<button class="btn btn--primary btn--sm" onclick="BrandCatalog.searchParts(document.getElementById(\'partsSearchInput\').value)">Buscar</button>' +
|
||||
'<button class="btn btn--secondary btn--sm" onclick="BrandCatalog.clearPartsSearch()">Limpiar</button>' +
|
||||
'<button class="btn btn-primary" onclick="BrandCatalog.searchParts(document.getElementById(\'partsSearchInput\').value)">Buscar</button>' +
|
||||
'<button class="btn btn-ghost" onclick="BrandCatalog.clearPartsSearch()">Limpiar</button>' +
|
||||
'</div>'
|
||||
);
|
||||
var url = '/pos/api/catalog/mye-parts?mye_id=' + encodeURIComponent(myeId) + '&category_id=' + encodeURIComponent(categoryId) +
|
||||
@@ -361,6 +387,7 @@
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
self.loading(false);
|
||||
self._allowedBrands = data.allowed_brands || [];
|
||||
self._lastItems = data.items || [];
|
||||
self._total = data.total || 0;
|
||||
self._offset = data.offset || 0;
|
||||
@@ -368,16 +395,20 @@
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar refacciones: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar refacciones</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderPartsList: function(items, searchTerm) {
|
||||
var html = '';
|
||||
if (!items.length) {
|
||||
html += '<div style="grid-column:1/-1;text-align:center;padding:var(--space-8);">' +
|
||||
'<p style="color:var(--color-text-muted);font-size:var(--text-body-lg);">No se encontraron refacciones.</p>' +
|
||||
'<button class="btn btn--primary" style="margin-top:var(--space-3);" onclick="BrandCatalog.loadCategories(' + this.nav.myeId + ')">Volver a categorias</button>' +
|
||||
var msg = 'No se encontraron refacciones.';
|
||||
if (this._allowedBrands.length) {
|
||||
msg = 'No hay refacciones de ' + this._allowedBrands.join(', ') + ' en esta categoria.';
|
||||
}
|
||||
html += '<div class="empty-state is-visible">' +
|
||||
'<div class="empty-state__title">' + msg + '</div>' +
|
||||
'<div class="empty-state__subtitle"><button class="btn btn-primary" onclick="BrandCatalog.loadCategories(' + this.nav.myeId + ')">Volver a categorias</button></div>' +
|
||||
'</div>';
|
||||
this.setContent(html);
|
||||
return;
|
||||
@@ -389,40 +420,55 @@
|
||||
'Mostrando ' + startIdx + '-' + endIdx + ' de ' + this._total + ' refacciones' +
|
||||
'</div>';
|
||||
|
||||
html += '<div style="grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3);">';
|
||||
html += '<div class="nav-grid nav-grid--parts">';
|
||||
items.forEach(function(p) {
|
||||
var price = p.local_price ? '$' + Number(p.local_price).toFixed(2) : 'Consultar precio';
|
||||
var img = '/pos/static/images/placeholder-part.png';
|
||||
var hasAm = !!p.manufacturer;
|
||||
var price = p.local_price
|
||||
? '$' + Number(p.local_price).toFixed(2)
|
||||
: (p.price_usd ? '$' + Number(p.price_usd).toFixed(2) : 'Consultar precio');
|
||||
var stockBadge = p.local_stock > 0
|
||||
? '<span style="display:inline-block;background:var(--color-success);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">' + p.local_stock + ' en stock</span>'
|
||||
: '<span style="display:inline-block;background:var(--color-text-muted);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">Sin stock local</span>';
|
||||
html += '<div class="catalog-category-card" style="padding:0;overflow:hidden;display:flex;flex-direction:column;">' +
|
||||
'<div style="height:160px;background:#f5f5f5;display:flex;align-items:center;justify-content:center;">' +
|
||||
'<img src="' + escapeHtml(img) + '" alt="" style="max-width:100%;max-height:100%;object-fit:contain;">' +
|
||||
? '<span class="stock-badge stock-badge--local">En stock</span>'
|
||||
: '<span class="stock-badge stock-badge--none">Sin stock local</span>';
|
||||
var imgHtml = p.image_url
|
||||
? '<img src="' + escapeHtml(p.image_url) + '" alt="">'
|
||||
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
|
||||
var brandLine = hasAm
|
||||
? '<div style="font-size:var(--text-caption);color:var(--color-accent);font-weight:600;">' + escapeHtml(p.manufacturer) + '</div>'
|
||||
: '';
|
||||
html += '<div class="part-card">' +
|
||||
'<div class="part-card__image">' + imgHtml + '</div>' +
|
||||
'<div class="part-card__body">' +
|
||||
brandLine +
|
||||
'<div class="part-card__oem">' + escapeHtml(p.oem_part_number || 'N/A') + '</div>' +
|
||||
'<div class="part-card__name">' + escapeHtml(p.name || '') + '</div>' +
|
||||
'</div>' +
|
||||
'<div style="padding:var(--space-3);flex:1;display:flex;flex-direction:column;">' +
|
||||
'<div style="font-weight:600;font-size:var(--text-body);margin-bottom:4px;">' + escapeHtml(p.oem_part_number || 'N/A') + stockBadge + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);margin-bottom:8px;flex:1;">' + escapeHtml(p.name || '') + '</div>' +
|
||||
'<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-primary);margin-bottom:8px;">' + price + '</div>' +
|
||||
'<button class="btn btn--primary btn--sm" style="width:100%;" onclick="BrandCatalog.addToCart(' + p.id + ', event)">Agregar</button>' +
|
||||
'<div class="part-card__footer">' +
|
||||
stockBadge +
|
||||
'<span class="part-card__price">' + price + '</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-primary" onclick="BrandCatalog.addToCart(' + p.id + ', event)">Agregar</button>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
html += this.renderPagination();
|
||||
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
renderPagination: function() {
|
||||
var hasPrev = this._offset > 0;
|
||||
var hasNext = (this._offset + this._limit) < this._total;
|
||||
var pageNum = Math.floor(this._offset / this._limit) + 1;
|
||||
var totalPages = Math.ceil(this._total / this._limit) || 1;
|
||||
html += '<div style="grid-column:1/-1;display:flex;justify-content:center;align-items:center;gap:var(--space-3);padding:var(--space-4) 0;">' +
|
||||
'<button class="btn btn--secondary" ' + (hasPrev ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
||||
var html = '<div class="pagination">' +
|
||||
'<button class="page-item" ' + (hasPrev ? '' : 'disabled') +
|
||||
' onclick="BrandCatalog.goToPage(' + (this._offset - this._limit) + ')">← Anterior</button>' +
|
||||
'<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">Pagina ' + pageNum + ' de ' + totalPages + '</span>' +
|
||||
'<button class="btn btn--secondary" ' + (hasNext ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
||||
'<button class="page-item" ' + (hasNext ? '' : 'disabled') +
|
||||
' onclick="BrandCatalog.goToPage(' + (this._offset + this._limit) + ')">Siguiente →</button>' +
|
||||
'</div>';
|
||||
|
||||
this.setContent(html);
|
||||
return html;
|
||||
},
|
||||
|
||||
searchParts: function(term) {
|
||||
@@ -451,16 +497,17 @@
|
||||
return;
|
||||
}
|
||||
if (window.CatalogApp && CatalogApp.addToCart) {
|
||||
var isAftermarket = !!part.manufacturer;
|
||||
CatalogApp.addToCart({
|
||||
id: part.id,
|
||||
id: part.oem_id || part.id,
|
||||
part_number: part.oem_part_number || 'N/A',
|
||||
name: part.name || 'Refaccion',
|
||||
brand: '',
|
||||
price: part.local_price || 0,
|
||||
brand: part.manufacturer || '',
|
||||
price: part.local_price || part.price_usd || 0,
|
||||
tax_rate: 0.16,
|
||||
unit: 'PZA',
|
||||
stock: part.local_stock || 0,
|
||||
source: 'oem-brand',
|
||||
source: isAftermarket ? 'aftermarket' : 'oem-brand',
|
||||
inventory_id: null
|
||||
}, 1);
|
||||
var btn = event.target;
|
||||
|
||||
@@ -349,10 +349,14 @@
|
||||
var cacheKey = 'nexus:brands:' + catalogMode;
|
||||
var cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
hideLoading();
|
||||
var data = JSON.parse(cached);
|
||||
renderBrands(data);
|
||||
return;
|
||||
try {
|
||||
hideLoading();
|
||||
var data = JSON.parse(cached);
|
||||
renderBrands(data);
|
||||
return;
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
apiFetch(API + '/brands?mode=' + catalogMode).then(function (data) {
|
||||
@@ -1631,8 +1635,13 @@
|
||||
var cacheKey = 'nexus:years-all';
|
||||
var cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
var data = JSON.parse(cached);
|
||||
var years = data.data || data || [];
|
||||
try {
|
||||
var data = JSON.parse(cached);
|
||||
var years = data.data || data || [];
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(cacheKey);
|
||||
var years = [];
|
||||
}
|
||||
if (!years.length) {
|
||||
years = [];
|
||||
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
|
||||
|
||||
1
pos/static/js/catalog.min.js
vendored
1
pos/static/js/catalog.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/chat.min.js
vendored
1
pos/static/js/chat.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -19,6 +19,11 @@ const Config = (() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
|
||||
}
|
||||
@@ -623,6 +628,67 @@ const Config = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Allowed Part Brands
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadAllowedBrands() {
|
||||
var container = document.getElementById('allowed-brands-container');
|
||||
if (!container) return;
|
||||
try {
|
||||
var res = await fetch(API + '/available-brands', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load brands');
|
||||
var d = await res.json();
|
||||
var allBrands = d.brands || [];
|
||||
|
||||
var res2 = await fetch(API + '/allowed-brands', { headers: headers() });
|
||||
if (!res2.ok) throw new Error('Failed to load allowed brands');
|
||||
var d2 = await res2.json();
|
||||
var allowed = d2.brands || [];
|
||||
|
||||
if (!allBrands.length) {
|
||||
container.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-body-sm);">No hay marcas disponibles.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
allBrands.forEach(function(b) {
|
||||
var checked = allowed.indexOf(b) !== -1 ? 'checked' : '';
|
||||
html += '<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;font-size:var(--text-body-sm);color:var(--color-text-primary);padding:var(--space-1);">' +
|
||||
'<input type="checkbox" value="' + escapeHtml(b) + '" data-brand-checkbox ' + checked + ' style="width:16px;height:16px;cursor:pointer;">' +
|
||||
escapeHtml(b) + '</label>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
} catch (e) {
|
||||
console.error('Config.loadAllowedBrands:', e);
|
||||
if (container) container.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-body-sm);">Error al cargar marcas.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllowedBrands() {
|
||||
var btn = document.getElementById('btn-save-allowed-brands');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
try {
|
||||
var checked = [];
|
||||
document.querySelectorAll('[data-brand-checkbox]').forEach(function(cb) {
|
||||
if (cb.checked) checked.push(cb.value);
|
||||
});
|
||||
var res = await fetch(API + '/allowed-brands', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ brands: checked })
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
toast('Marcas permitidas actualizadas');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Guardar Marcas'; }
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -650,6 +716,12 @@ const Config = (() => {
|
||||
btnCompat.addEventListener('click', saveVehicleCompatSource);
|
||||
}
|
||||
|
||||
// Allowed brands save button
|
||||
var btnBrands = document.getElementById('btn-save-allowed-brands');
|
||||
if (btnBrands) {
|
||||
btnBrands.addEventListener('click', saveAllowedBrands);
|
||||
}
|
||||
|
||||
// Kiosk mode toggle
|
||||
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
||||
if (kioskToggle && window.NexusKiosk) {
|
||||
@@ -671,12 +743,13 @@ const Config = (() => {
|
||||
loadBusiness();
|
||||
loadCurrency();
|
||||
loadVehicleCompatSource();
|
||||
loadAllowedBrands();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return {
|
||||
init, setTheme, selectThemeOption,
|
||||
init, setTheme, selectThemeOption, loadAllowedBrands, saveAllowedBrands,
|
||||
loadBranches, loadEmployees, saveBranch, saveEmployee, editEmployee,
|
||||
loadBusiness, saveBusiness, saveTaxParams,
|
||||
loadCurrency, saveCurrency,
|
||||
|
||||
1
pos/static/js/config.min.js
vendored
1
pos/static/js/config.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -104,7 +104,7 @@ const Customers = (() => {
|
||||
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
|
||||
const num = String(c.id).padStart(5, '0');
|
||||
const selClass = (currentCustomer && currentCustomer.id === c.id) ? 'selected' : '';
|
||||
return '<tr class="' + selClass + '" onclick="selectCustomer(' + c.id + ')">' +
|
||||
return '<tr class="' + selClass + '" onclick="Customers.selectCustomer(' + c.id + ')">' +
|
||||
'<td class="cell-num">' + num + '</td>' +
|
||||
'<td>' +
|
||||
'<div class="cell-name">' + (c.name || '') + '</div>' +
|
||||
@@ -263,6 +263,13 @@ const Customers = (() => {
|
||||
bar.className = `progress-bar__fill ${pct < 40 ? 'low' : pct > 75 ? 'high' : ''}`;
|
||||
}
|
||||
|
||||
// Discount
|
||||
const discountEl = document.getElementById('detailMaxDiscount');
|
||||
if (discountEl) discountEl.textContent = (c.max_discount_pct || 0) + '%';
|
||||
|
||||
// Re-wire action buttons after detail panel is visible
|
||||
wireActionButtons();
|
||||
|
||||
// Purchase History
|
||||
const hbody = document.getElementById('historyBody');
|
||||
if (hbody) {
|
||||
@@ -363,7 +370,7 @@ const Customers = (() => {
|
||||
const btns = document.querySelectorAll('.quick-actions .action-btn');
|
||||
// Order: Nueva Venta, Editar, Estado de Cuenta, Historial
|
||||
if (btns.length >= 1) btns[0].onclick = () => {
|
||||
if (currentCustomer) window.location.href = '/pos/?customer=' + currentCustomer.id;
|
||||
if (currentCustomer) window.location.href = '/pos/sale?customer=' + currentCustomer.id;
|
||||
};
|
||||
if (btns.length >= 2) btns[1].onclick = () => editCurrent();
|
||||
if (btns.length >= 3) btns[2].onclick = () => showStatement();
|
||||
@@ -378,17 +385,19 @@ const Customers = (() => {
|
||||
if (!modal) return;
|
||||
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';
|
||||
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
|
||||
safeSet('fName', '');
|
||||
safeSet('fRfc', '');
|
||||
safeSet('fRazonSocial', '');
|
||||
safeSet('fRegimenFiscal', '');
|
||||
safeSet('fUsoCfdi', 'G03');
|
||||
safeSet('fCp', '');
|
||||
safeSet('fPhone', '');
|
||||
safeSet('fEmail', '');
|
||||
safeSet('fAddress', '');
|
||||
safeSet('fPriceTier', '1');
|
||||
safeSet('fCreditLimit', '0');
|
||||
safeSet('fMaxDiscountPct', '0');
|
||||
modal.classList.add('active');
|
||||
document.getElementById('fName').focus();
|
||||
}
|
||||
@@ -400,17 +409,19 @@ const Customers = (() => {
|
||||
if (!modal) return;
|
||||
document.getElementById('modalTitle').textContent = 'Editar Cliente';
|
||||
document.getElementById('editId').value = c.id;
|
||||
document.getElementById('fName').value = c.name || '';
|
||||
document.getElementById('fRfc').value = c.rfc || '';
|
||||
document.getElementById('fRazonSocial').value = c.razon_social || '';
|
||||
document.getElementById('fRegimenFiscal').value = c.regimen_fiscal || '';
|
||||
document.getElementById('fUsoCfdi').value = c.uso_cfdi || 'G03';
|
||||
document.getElementById('fCp').value = c.cp || '';
|
||||
document.getElementById('fPhone').value = c.phone || '';
|
||||
document.getElementById('fEmail').value = c.email || '';
|
||||
document.getElementById('fAddress').value = c.address || '';
|
||||
document.getElementById('fPriceTier').value = c.price_tier || '1';
|
||||
document.getElementById('fCreditLimit').value = c.credit_limit || 0;
|
||||
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
|
||||
safeSet('fName', c.name || '');
|
||||
safeSet('fRfc', c.rfc || '');
|
||||
safeSet('fRazonSocial', c.razon_social || '');
|
||||
safeSet('fRegimenFiscal', c.regimen_fiscal || '');
|
||||
safeSet('fUsoCfdi', c.uso_cfdi || 'G03');
|
||||
safeSet('fCp', c.cp || '');
|
||||
safeSet('fPhone', c.phone || '');
|
||||
safeSet('fEmail', c.email || '');
|
||||
safeSet('fAddress', c.address || '');
|
||||
safeSet('fPriceTier', c.price_tier || '1');
|
||||
safeSet('fCreditLimit', c.credit_limit || 0);
|
||||
safeSet('fMaxDiscountPct', c.max_discount_pct || 0);
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
@@ -438,6 +449,7 @@ const Customers = (() => {
|
||||
address: val('fAddress') || null,
|
||||
price_tier: parseInt(val('fPriceTier')) || 1,
|
||||
credit_limit: parseFloat(val('fCreditLimit')) || 0,
|
||||
max_discount_pct: parseFloat(val('fMaxDiscountPct')) || 0,
|
||||
};
|
||||
|
||||
const editId = val('editId');
|
||||
@@ -586,7 +598,9 @@ const Customers = (() => {
|
||||
|
||||
function injectModals() {
|
||||
// Customer Create/Edit Modal
|
||||
if (!document.getElementById('customerModal')) {
|
||||
// Always remove and re-inject to ensure latest fields are present
|
||||
const existingModal = document.getElementById('customerModal');
|
||||
if (existingModal) existingModal.remove();
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div id="customerModal" class="modal-overlay" style="display:none;">
|
||||
@@ -646,6 +660,7 @@ const Customers = (() => {
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Limite de Credito</label><input type="number" id="fCreditLimit" class="form-input" value="0" min="0" step="1000" /></div>
|
||||
<div class="form-group"><label>Descuento Max (%)</label><input type="number" id="fMaxDiscountPct" class="form-input" value="0" min="0" max="100" step="0.5" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -655,9 +670,10 @@ const Customers = (() => {
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
// Statement Modal
|
||||
const existingStatement = document.getElementById('statementModal');
|
||||
if (existingStatement) existingStatement.remove();
|
||||
if (!document.getElementById('statementModal')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
@@ -772,11 +788,15 @@ const Customers = (() => {
|
||||
// Run init
|
||||
init();
|
||||
|
||||
return {
|
||||
const publicApi = {
|
||||
search, goToPage, loadCustomers,
|
||||
showDetail, selectCustomer, closeDetail,
|
||||
showCreateModal, editCurrent, closeModal, save,
|
||||
showStatement, closeStatement,
|
||||
showPaymentModal, closePayment, recordPayment,
|
||||
};
|
||||
|
||||
// Expose globally for inline HTML onclick handlers
|
||||
window.Customers = publicApi;
|
||||
return publicApi;
|
||||
})();
|
||||
|
||||
1
pos/static/js/customers.min.js
vendored
1
pos/static/js/customers.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/dashboard.min.js
vendored
1
pos/static/js/dashboard.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/diagrams.min.js
vendored
1
pos/static/js/diagrams.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/fleet.min.js
vendored
1
pos/static/js/fleet.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -47,6 +47,79 @@
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// --- Dashboard summary badges ---
|
||||
function loadSummary() {
|
||||
apiFetch(API + '/summary').then(function(data) {
|
||||
if (!data) return;
|
||||
var totalSkusEl = document.getElementById('inv-total-skus');
|
||||
var totalValueEl = document.getElementById('inv-total-value');
|
||||
var lowStockEl = document.getElementById('inv-low-stock');
|
||||
var noMovementEl = document.getElementById('inv-no-movement');
|
||||
if (totalSkusEl) totalSkusEl.textContent = (data.total_skus || 0).toLocaleString('es-MX');
|
||||
if (totalValueEl) totalValueEl.textContent = '$' + (data.total_value || 0).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
if (lowStockEl) lowStockEl.textContent = (data.low_stock || 0).toLocaleString('es-MX');
|
||||
if (noMovementEl) noMovementEl.textContent = (data.no_movement || 0).toLocaleString('es-MX');
|
||||
}).catch(function(err) {
|
||||
console.error('Inventory summary load failed:', err);
|
||||
});
|
||||
}
|
||||
loadSummary();
|
||||
|
||||
// --- Global tier discounts ---
|
||||
var globalDiscounts = { 2: 15, 3: 25 };
|
||||
function loadTierDiscounts() {
|
||||
apiFetch(API + '/tier-discounts').then(function(data) {
|
||||
if (data && data.data) {
|
||||
data.data.forEach(function(d) {
|
||||
globalDiscounts[d.tier_id] = d.discount_pct;
|
||||
});
|
||||
}
|
||||
var discEl = document.getElementById('tierDiscountBadge');
|
||||
if (discEl) {
|
||||
discEl.textContent = 'Taller -' + globalDiscounts[2] + '% · Mayoreo -' + globalDiscounts[3] + '%';
|
||||
}
|
||||
});
|
||||
}
|
||||
loadTierDiscounts();
|
||||
|
||||
function showTierDiscountModal() {
|
||||
document.getElementById('tierDisc2').value = globalDiscounts[2];
|
||||
document.getElementById('tierDisc3').value = globalDiscounts[3];
|
||||
document.getElementById('tierDiscountModal').classList.add('is-open');
|
||||
}
|
||||
function closeTierDiscountModal() {
|
||||
document.getElementById('tierDiscountModal').classList.remove('is-open');
|
||||
}
|
||||
function saveTierDiscounts() {
|
||||
var d2 = parseFloat(document.getElementById('tierDisc2').value) || 0;
|
||||
var d3 = parseFloat(document.getElementById('tierDisc3').value) || 0;
|
||||
fetch(API + '/tier-discounts', {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ discount_pct_2: d2, discount_pct_3: d3 })
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
showToast(res.message || 'Guardado', 'ok');
|
||||
globalDiscounts[2] = d2;
|
||||
globalDiscounts[3] = d3;
|
||||
var discEl = document.getElementById('tierDiscountBadge');
|
||||
if (discEl) {
|
||||
discEl.textContent = 'Taller -' + d2 + '% · Mayoreo -' + d3 + '%';
|
||||
}
|
||||
closeTierDiscountModal();
|
||||
}).catch(function() {
|
||||
showToast('Error al guardar descuentos', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hash-based tab switching (e.g. /pos/inventory#alertas)
|
||||
(function handleHashTab() {
|
||||
var hash = window.location.hash.replace('#', '');
|
||||
if (hash && ['stock', 'entradas', 'salidas', 'traspasos', 'ajustes', 'conteos', 'alertas'].indexOf(hash) !== -1) {
|
||||
setTimeout(function() { switchTab(hash); }, 100);
|
||||
}
|
||||
})();
|
||||
|
||||
// =====================================================================
|
||||
// TAB SWITCHING — uses design-system switchTab() already in the HTML.
|
||||
// We hook into it to trigger data loads when tabs are activated.
|
||||
@@ -63,8 +136,12 @@
|
||||
// STOCK / PRODUCTS (panel-stock)
|
||||
// =====================================================================
|
||||
|
||||
var selectedItems = new Set();
|
||||
|
||||
function renderInventoryRow(it) {
|
||||
var isChecked = selectedItems.has(it.id) ? 'checked' : '';
|
||||
return '<tr style="cursor:pointer;" onclick="viewProductDetail(' + it.id + ')">' +
|
||||
'<td onclick="event.stopPropagation();"><input type="checkbox" class="item-checkbox" data-id="' + it.id + '" ' + isChecked + ' onclick="event.stopPropagation();toggleItemSelection(' + it.id + ')"></td>' +
|
||||
'<td class="td--mono" style="font-size:var(--text-caption);color:var(--color-text-muted);">' + it.id + '</td>' +
|
||||
'<td class="td--mono">' + esc(it.barcode) + '</td>' +
|
||||
'<td class="td--mono">' + esc(it.part_number) + '</td>' +
|
||||
@@ -79,10 +156,50 @@
|
||||
'<td>' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();viewHistory(' + it.id + ')">Historial</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button>' +
|
||||
'<button class="btn btn--sm btn--meli" onclick="event.stopPropagation();publishToMeli(' + it.id + ')">ML</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="event.stopPropagation();deleteItem(' + it.id + ')">Eliminar</button>' +
|
||||
'</td></tr>';
|
||||
}
|
||||
|
||||
window.toggleItemSelection = function(id) {
|
||||
if (selectedItems.has(id)) {
|
||||
selectedItems.delete(id);
|
||||
} else {
|
||||
selectedItems.add(id);
|
||||
}
|
||||
updateSelectionUI();
|
||||
};
|
||||
|
||||
window.toggleSelectAllItems = function() {
|
||||
var cb = document.getElementById('selectAllItems');
|
||||
var allChecked = cb.checked;
|
||||
// We need to get all visible items from inventoryVS
|
||||
if (inventoryVS && inventoryVS.data) {
|
||||
inventoryVS.data.forEach(function(it) {
|
||||
if (allChecked) selectedItems.add(it.id);
|
||||
else selectedItems.delete(it.id);
|
||||
});
|
||||
inventoryVS.refresh();
|
||||
}
|
||||
updateSelectionUI();
|
||||
};
|
||||
|
||||
function updateSelectionUI() {
|
||||
var count = selectedItems.size;
|
||||
var btn = document.getElementById('btnPublishML');
|
||||
var badge = document.getElementById('meliSelectedCountBadge');
|
||||
if (btn) btn.style.display = count > 0 ? 'inline-flex' : 'none';
|
||||
if (badge) badge.textContent = count;
|
||||
// Update select-all checkbox state
|
||||
var selectAll = document.getElementById('selectAllItems');
|
||||
if (selectAll && inventoryVS && inventoryVS.data) {
|
||||
var visibleIds = inventoryVS.data.map(function(it) { return it.id; });
|
||||
var allSelected = visibleIds.length > 0 && visibleIds.every(function(id) { return selectedItems.has(id); });
|
||||
selectAll.checked = allSelected;
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems(page, search) {
|
||||
currentPage = page || 1;
|
||||
currentSearch = search !== undefined ? search : currentSearch;
|
||||
@@ -220,7 +337,7 @@
|
||||
loadItems(currentPage);
|
||||
// Close modal, clear form, refresh badges
|
||||
closeCreateModal();
|
||||
['newPartNumber','newName','newBrand','newBarcode','newCost','newPrice1','newPrice2','newPrice3','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
|
||||
['newPartNumber','newName','newBrand','newBarcode','newCost','newPrice1','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
@@ -634,6 +751,195 @@
|
||||
document.getElementById('historyModal').classList.remove('is-open');
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// DELETE ITEM
|
||||
// =====================================================================
|
||||
|
||||
function deleteItem(itemId) {
|
||||
if (!confirm('¿Eliminar este artículo del inventario? Se mantendrán los registros históricos.')) return;
|
||||
var token = localStorage.getItem('pos_token') || '';
|
||||
fetch(API + '/items/' + itemId, {
|
||||
method: 'DELETE',
|
||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
showToast('Artículo eliminado');
|
||||
loadItems(currentPage);
|
||||
if (window.loadInventoryStats) window.loadInventoryStats();
|
||||
}).catch(function() { alert('Error al eliminar artículo'); });
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// MERCADOLIBRE PUBLISH
|
||||
// =====================================================================
|
||||
|
||||
function publishToMeli(itemId) {
|
||||
selectedItems.clear();
|
||||
selectedItems.add(itemId);
|
||||
updateSelectionUI();
|
||||
openMeliPublishModal();
|
||||
}
|
||||
window.publishToMeli = publishToMeli;
|
||||
|
||||
// ─── MercadoLibre Bulk Publish Modal ───────────────────────────────────
|
||||
|
||||
window.openMeliPublishModal = function() {
|
||||
if (selectedItems.size === 0) { showToast('Selecciona al menos un producto', 'warn'); return; }
|
||||
document.getElementById('meliPublishModal').classList.add('is-open');
|
||||
document.getElementById('meliPublishResult').innerHTML = '';
|
||||
document.getElementById('meliCategoryId').value = '';
|
||||
document.getElementById('meliCategorySearch').value = '';
|
||||
document.getElementById('meliCategoryResults').innerHTML = '';
|
||||
refreshMeliPublishPreview();
|
||||
};
|
||||
|
||||
window.closeMeliPublishModal = function() {
|
||||
document.getElementById('meliPublishModal').classList.remove('is-open');
|
||||
};
|
||||
|
||||
function refreshMeliPublishPreview() {
|
||||
var container = document.getElementById('meliPublishItemsPreview');
|
||||
var countEl = document.getElementById('meliPublishSelectedCount');
|
||||
countEl.textContent = selectedItems.size + ' producto(s) seleccionado(s)';
|
||||
if (!inventoryVS || !inventoryVS.data) { container.innerHTML = '<p style="color:var(--color-text-muted);">Sin datos</p>'; return; }
|
||||
var items = inventoryVS.data.filter(function(it) { return selectedItems.has(it.id); });
|
||||
if (!items.length) { container.innerHTML = '<p style="color:var(--color-text-muted);">Ninguno</p>'; return; }
|
||||
var html = '<table class="data-table" style="font-size:var(--text-caption);"><thead><tr><th>ID</th><th>No. Parte</th><th>Nombre</th><th>Stock</th><th style="text-align:right">Precio</th></tr></thead><tbody>';
|
||||
items.forEach(function(it) {
|
||||
html += '<tr><td>' + it.id + '</td><td class="td--mono">' + esc(it.part_number) + '</td><td>' + esc(it.name) + '</td><td>' + it.stock + '</td><td style="text-align:right">$' + fmt(it.price_1) + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
var meliCategorySearchTimeout;
|
||||
var meliCatItems = [];
|
||||
var meliCatActiveIndex = -1;
|
||||
|
||||
window.searchMeliCategories = function() {
|
||||
var q = document.getElementById('meliCategorySearch').value.trim();
|
||||
var resultsDiv = document.getElementById('meliCategoryResults');
|
||||
if (q.length < 2) { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; return; }
|
||||
clearTimeout(meliCategorySearchTimeout);
|
||||
resultsDiv.innerHTML = '<div class="meli-cat-dropdown"><div class="meli-cat-loading">Buscando...</div></div>';
|
||||
meliCategorySearchTimeout = setTimeout(function() {
|
||||
fetch('/pos/api/marketplace-ext/categories?q=' + encodeURIComponent(q), { headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var cats = data.categories || [];
|
||||
meliCatItems = cats.slice(0, 10);
|
||||
meliCatActiveIndex = -1;
|
||||
if (!meliCatItems.length) { resultsDiv.innerHTML = '<div class="meli-cat-dropdown"><div class="meli-cat-empty">Sin resultados</div></div>'; return; }
|
||||
var html = '<div class="meli-cat-dropdown">';
|
||||
meliCatItems.forEach(function(c, idx) {
|
||||
html += '<div class="meli-cat-item" data-idx="' + idx + '" onmouseenter="highlightMeliCat(' + idx + ')" onmousedown="selectMeliCategoryIdx(' + idx + ')">' +
|
||||
'<span>' + esc(c.category_name || c.category_id) + '</span>' +
|
||||
'<span class="cat-id">' + esc(c.category_id) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
resultsDiv.innerHTML = html;
|
||||
})
|
||||
.catch(function() { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; });
|
||||
}, 300);
|
||||
};
|
||||
|
||||
window.highlightMeliCat = function(idx) {
|
||||
meliCatActiveIndex = idx;
|
||||
var items = document.querySelectorAll('.meli-cat-item');
|
||||
items.forEach(function(el, i) { el.classList.toggle('is-active', i === idx); });
|
||||
};
|
||||
|
||||
window.selectMeliCategoryIdx = function(idx) {
|
||||
var c = meliCatItems[idx];
|
||||
if (!c) return;
|
||||
selectMeliCategory(c.category_id, c.category_name || c.category_id);
|
||||
};
|
||||
|
||||
window.selectMeliCategory = function(id, name) {
|
||||
document.getElementById('meliCategoryId').value = id;
|
||||
document.getElementById('meliCategorySearch').value = name;
|
||||
document.getElementById('meliCategoryResults').innerHTML = '';
|
||||
meliCatItems = [];
|
||||
meliCatActiveIndex = -1;
|
||||
};
|
||||
|
||||
window.handleMeliCatKeydown = function(e) {
|
||||
if (!meliCatItems.length) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
meliCatActiveIndex = Math.min(meliCatActiveIndex + 1, meliCatItems.length - 1);
|
||||
highlightMeliCat(meliCatActiveIndex);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
meliCatActiveIndex = Math.max(meliCatActiveIndex - 1, -1);
|
||||
highlightMeliCat(meliCatActiveIndex);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (meliCatActiveIndex >= 0) selectMeliCategoryIdx(meliCatActiveIndex);
|
||||
} else if (e.key === 'Escape') {
|
||||
document.getElementById('meliCategoryResults').innerHTML = '';
|
||||
meliCatItems = [];
|
||||
meliCatActiveIndex = -1;
|
||||
}
|
||||
};
|
||||
|
||||
/* Cerrar dropdown al hacer click fuera */
|
||||
document.addEventListener('click', function(e) {
|
||||
var field = document.getElementById('meliCategorySearch');
|
||||
var results = document.getElementById('meliCategoryResults');
|
||||
if (field && results && !field.contains(e.target) && !results.contains(e.target)) {
|
||||
results.innerHTML = '';
|
||||
meliCatItems = [];
|
||||
meliCatActiveIndex = -1;
|
||||
}
|
||||
});
|
||||
|
||||
window.executeMeliPublish = function() {
|
||||
var categoryId = document.getElementById('meliCategoryId').value.trim();
|
||||
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = '<span style="color:var(--color-error);">Selecciona una categoría de MercadoLibre</span>'; return; }
|
||||
var listingType = document.getElementById('meliListingType').value;
|
||||
var shippingMode = document.getElementById('meliShippingMode').value;
|
||||
var ids = Array.from(selectedItems);
|
||||
var resultEl = document.getElementById('meliPublishResult');
|
||||
var btn = document.getElementById('meliPublishBtn');
|
||||
btn.disabled = true;
|
||||
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Publicando ' + ids.length + ' producto(s)...</span>';
|
||||
fetch('/pos/api/marketplace-ext/listings', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
inventory_ids: ids,
|
||||
category_id: categoryId,
|
||||
listing_type: listingType,
|
||||
shipping_mode: shippingMode
|
||||
})
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
btn.disabled = false;
|
||||
if (data.error) { resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>'; return; }
|
||||
var success = (data.success || []).length;
|
||||
var failedList = data.failed || [];
|
||||
var failed = failedList.length;
|
||||
var html = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + success + ' publicado(s)</span> · <span style="color:var(--color-error);">❌ ' + failed + ' fallo(s)</span></div>';
|
||||
if (failedList.length) {
|
||||
html += '<ul style="margin:0;padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
|
||||
failedList.forEach(function(f) {
|
||||
html += '<li>Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
resultEl.innerHTML = html;
|
||||
if (success > 0) {
|
||||
selectedItems.clear();
|
||||
updateSelectionUI();
|
||||
if (inventoryVS) inventoryVS.refresh();
|
||||
setTimeout(function() { closeMeliPublishModal(); }, 2500);
|
||||
}
|
||||
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(e.message) + '</span>'; });
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// BARCODE LABEL PRINT
|
||||
// =====================================================================
|
||||
@@ -747,9 +1053,9 @@
|
||||
// Prices
|
||||
html += '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Costo</span><span class="td--amount">$' + fmt(data.cost) + '</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 1</span><span class="td--amount">$' + fmt(data.price_1) + '</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 2</span><span class="td--amount">$' + fmt(data.price_2) + '</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 3</span><span class="td--amount">$' + fmt(data.price_3) + '</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Mostrador</span><span class="td--amount">$' + fmt(data.price_1) + '</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Taller</span><span class="td--amount">$' + fmt(data.price_2) + '</span><span style="font-size:var(--text-caption);color:var(--color-text-muted);"> (-' + globalDiscounts[2] + '%)</span></div>';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Mayoreo</span><span class="td--amount">$' + fmt(data.price_3) + '</span><span style="font-size:var(--text-caption);color:var(--color-text-muted);"> (-' + globalDiscounts[3] + '%)</span></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Cross-references section
|
||||
@@ -950,6 +1256,7 @@
|
||||
window.cancelDraft = cancelDraft;
|
||||
window.loadAlerts = loadAlerts;
|
||||
window.printBarcode = printBarcode;
|
||||
window.deleteItem = deleteItem;
|
||||
window.autoMatchCompat = autoMatchCompat;
|
||||
window.removeCompat = removeCompat;
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Expose for other scripts
|
||||
window.isKioskEnabled = isKioskEnabled;
|
||||
|
||||
// ─── Init ───
|
||||
if (isKioskEnabled()) {
|
||||
activate();
|
||||
|
||||
367
pos/static/js/marketplace_external.js
Normal file
367
pos/static/js/marketplace_external.js
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* marketplace_external.js — MercadoLibre integration UI
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var API = '/pos/api/marketplace-ext';
|
||||
var TOKEN = localStorage.getItem('pos_token') || '';
|
||||
|
||||
function headers() {
|
||||
return {
|
||||
'Authorization': 'Bearer ' + TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Device-Id': localStorage.getItem('pos_device_id') || 'web',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Tabs ──────────────────────────────────────────────────────────────
|
||||
|
||||
window.switchTab = function(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
||||
b.classList.toggle('is-active', b.dataset.tab === tab);
|
||||
b.setAttribute('aria-selected', b.dataset.tab === tab ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(function(p) {
|
||||
p.classList.toggle('is-active', p.id === 'panel-' + tab);
|
||||
});
|
||||
if (tab === 'listings') loadListings();
|
||||
if (tab === 'orders') loadOrders();
|
||||
};
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove('is-open');
|
||||
}
|
||||
window.closeModal = closeModal;
|
||||
|
||||
// ─── Config / Connection ───────────────────────────────────────────────
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
var res = await fetch(API + '/config', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load config');
|
||||
var cfg = await res.json();
|
||||
|
||||
var statusDiv = document.getElementById('configStatus');
|
||||
var formDiv = document.getElementById('configForm');
|
||||
var connectedDiv = document.getElementById('configConnected');
|
||||
|
||||
if (cfg.connected) {
|
||||
statusDiv.innerHTML = '<span class="meli-status meli-status--active">● Conectado</span>';
|
||||
formDiv.style.display = 'none';
|
||||
connectedDiv.style.display = 'block';
|
||||
document.getElementById('connectedNickname').textContent = cfg.meli_user_id || 'Usuario ML';
|
||||
document.getElementById('connectedSite').textContent = cfg.meli_site_id || 'MLM';
|
||||
} else {
|
||||
statusDiv.innerHTML = '<span class="meli-status meli-status--pending">● No conectado</span>';
|
||||
formDiv.style.display = 'block';
|
||||
connectedDiv.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
document.getElementById('configStatus').innerHTML = '<p style="color:var(--color-danger);">Error cargando configuración</p>';
|
||||
}
|
||||
}
|
||||
|
||||
window.startOAuth = function() {
|
||||
var clientId = document.getElementById('cfgClientId').value.trim();
|
||||
var clientSecret = document.getElementById('cfgClientSecret').value.trim();
|
||||
var category = document.getElementById('cfgCategory').value.trim();
|
||||
var shipping = document.getElementById('cfgShipping').value;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
alert('Client ID y Client Secret son requeridos');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save config locally for the callback
|
||||
localStorage.setItem('meli_client_id', clientId);
|
||||
localStorage.setItem('meli_client_secret', clientSecret);
|
||||
localStorage.setItem('meli_category', category);
|
||||
localStorage.setItem('meli_shipping', shipping);
|
||||
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri);
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
window.disconnectMeli = async function() {
|
||||
if (!confirm('¿Desconectar MercadoLibre? Las publicaciones existentes no se eliminarán de ML.')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/connect', { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) {
|
||||
loadConfig();
|
||||
} else {
|
||||
alert('Error desconectando');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Listings ──────────────────────────────────────────────────────────
|
||||
|
||||
var listingsData = [];
|
||||
|
||||
window.loadListings = async function() {
|
||||
var container = document.getElementById('listingsContainer');
|
||||
container.innerHTML = '<p>Cargando...</p>';
|
||||
try {
|
||||
var res = await fetch(API + '/listings?page=1&per_page=50', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load listings');
|
||||
var data = await res.json();
|
||||
listingsData = data.items || [];
|
||||
renderListings();
|
||||
} catch (e) {
|
||||
container.innerHTML = '<p style="color:var(--color-danger);">Error cargando publicaciones</p>';
|
||||
}
|
||||
};
|
||||
|
||||
function renderListings() {
|
||||
var container = document.getElementById('listingsContainer');
|
||||
var statusFilter = document.getElementById('listingStatusFilter').value;
|
||||
var search = document.getElementById('listingSearch').value.toLowerCase();
|
||||
|
||||
var filtered = listingsData.filter(function(l) {
|
||||
if (statusFilter && l.external_status !== statusFilter) return false;
|
||||
if (search && !((l.title || '').toLowerCase().includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
container.innerHTML = '<p style="color:var(--color-text-muted);padding:var(--space-4);">No hay publicaciones. Ve a Inventario y publica un producto.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(function(l) {
|
||||
var statusClass = 'meli-status--' + (l.external_status || 'pending');
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + '</div>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + (l.external_status || '—') + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: ' + escapeHtml(l.external_item_id || '—')
|
||||
+ '</div>'
|
||||
+ '<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);">'
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="syncListing(' + l.id + ')">Sync</button>'
|
||||
+ (l.external_status === 'active' ? '<button class="btn btn--ghost btn--xs" onclick="pauseListing(' + l.id + ')">Pausar</button>' : '<button class="btn btn--ghost btn--xs" onclick="activateListing(' + l.id + ')">Activar</button>')
|
||||
+ '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterListings = renderListings;
|
||||
|
||||
window.syncListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/sync', { method: 'POST', headers: headers() });
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('Sincronizado: $' + data.price + ' · Stock: ' + data.stock);
|
||||
loadListings();
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
window.pauseListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/pause', { method: 'POST', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.activateListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/activate', { method: 'POST', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.deleteListing = async function(id) {
|
||||
if (!confirm('¿Cerrar esta publicación en MercadoLibre?')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id, { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Orders ────────────────────────────────────────────────────────────
|
||||
|
||||
var ordersData = [];
|
||||
|
||||
window.loadOrders = async function() {
|
||||
var tbody = document.getElementById('ordersTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="6">Cargando...</td></tr>';
|
||||
try {
|
||||
var res = await fetch(API + '/orders?page=1&per_page=50', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load orders');
|
||||
var data = await res.json();
|
||||
ordersData = data.items || [];
|
||||
renderOrders();
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--color-danger)">Error cargando órdenes</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
function renderOrders() {
|
||||
var tbody = document.getElementById('ordersTableBody');
|
||||
var statusFilter = document.getElementById('orderStatusFilter').value;
|
||||
var search = document.getElementById('orderSearch').value.toLowerCase();
|
||||
|
||||
var filtered = ordersData.filter(function(o) {
|
||||
if (statusFilter && o.status !== statusFilter) return false;
|
||||
if (search && !((o.buyer_name || '').toLowerCase().includes(search) || (o.external_order_id || '').includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--color-text-muted)">No hay órdenes.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filtered.map(function(o) {
|
||||
var statusClass = 'meli-status--' + (o.status || 'pending');
|
||||
return '<tr>'
|
||||
+ '<td><a href="#" onclick="showOrderDetail(' + o.id + ');return false;">' + escapeHtml(o.external_order_id) + '</a></td>'
|
||||
+ '<td>' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '</td>'
|
||||
+ '<td style="text-align:right">$' + (o.total_amount || 0).toFixed(2) + '</td>'
|
||||
+ '<td><span class="meli-status ' + statusClass + '">' + (o.status || '—') + '</span></td>'
|
||||
+ '<td>' + (o.created_at ? o.created_at.split('T')[0] : '—') + '</td>'
|
||||
+ '<td>'
|
||||
+ (o.status === 'pending' ? '<button class="btn btn--primary btn--xs" onclick="convertOrder(' + o.id + ')">Convertir a Venta</button> ' : '')
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="showOrderDetail(' + o.id + ')">Ver</button>'
|
||||
+ '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterOrders = renderOrders;
|
||||
|
||||
window.showOrderDetail = async function(id) {
|
||||
var modal = document.getElementById('orderModal');
|
||||
var body = document.getElementById('orderModalBody');
|
||||
var footer = document.getElementById('orderModalFooter');
|
||||
body.innerHTML = 'Cargando...';
|
||||
footer.innerHTML = '';
|
||||
modal.classList.add('is-open');
|
||||
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id, { headers: headers() });
|
||||
var o = await res.json();
|
||||
if (!res.ok) throw new Error(o.error || 'Error');
|
||||
|
||||
var itemsHtml = (o.items || []).map(function(it) {
|
||||
return '<tr><td>' + escapeHtml(it.title || '—') + '</td><td>' + it.quantity + '</td><td style="text-align:right">$' + (it.unit_price || 0).toFixed(2) + '</td></tr>';
|
||||
}).join('');
|
||||
|
||||
body.innerHTML = '<div style="margin-bottom:var(--space-4);">'
|
||||
+ '<p><strong>Comprador:</strong> ' + escapeHtml(o.buyer_name || '—') + ' (' + escapeHtml(o.buyer_nickname || '—') + ')</p>'
|
||||
+ '<p><strong>Email:</strong> ' + escapeHtml(o.buyer_email || '—') + '</p>'
|
||||
+ '<p><strong>Teléfono:</strong> ' + escapeHtml(o.buyer_phone || '—') + '</p>'
|
||||
+ '<p><strong>Total:</strong> $' + (o.total_amount || 0).toFixed(2) + '</p>'
|
||||
+ '<p><strong>Estado ML:</strong> ' + escapeHtml(o.external_status || '—') + '</p>'
|
||||
+ '<p><strong>Estado Nexus:</strong> ' + escapeHtml(o.status || '—') + '</p>'
|
||||
+ '</div>'
|
||||
+ '<h4 style="margin:var(--space-3) 0;">Items</h4>'
|
||||
+ '<table class="data-table"><thead><tr><th>Producto</th><th>Cantidad</th><th style="text-align:right">Precio</th></tr></thead><tbody>' + itemsHtml + '</tbody></table>';
|
||||
|
||||
footer.innerHTML = '';
|
||||
if (o.status === 'pending') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="convertOrder(' + o.id + ');closeModal(\'orderModal\')">Convertir a Venta</button> ';
|
||||
}
|
||||
if (o.status === 'confirmed') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="updateOrderStatus(' + o.id + ', \'packed\')">Marcar Empacada</button> ';
|
||||
}
|
||||
if (o.status === 'packed') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="updateOrderStatus(' + o.id + ', \'shipped\')">Marcar Enviada</button> ';
|
||||
}
|
||||
footer.innerHTML += '<button class="btn btn--ghost" onclick="closeModal(\'orderModal\')">Cerrar</button>';
|
||||
} catch (e) {
|
||||
body.innerHTML = '<p style="color:var(--color-danger)">Error: ' + escapeHtml(e.message) + '</p>';
|
||||
}
|
||||
};
|
||||
|
||||
window.convertOrder = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id + '/convert', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('Orden convertida a venta #' + data.sale_id);
|
||||
loadOrders();
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
window.updateOrderStatus = async function(id, status) {
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id + '/status', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ status: status })
|
||||
});
|
||||
if (res.ok) { loadOrders(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Utils ─────────────────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ─── Init ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Handle OAuth callback
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var authCode = urlParams.get('code');
|
||||
if (authCode && window.location.pathname.includes('marketplace-external')) {
|
||||
(async function() {
|
||||
var clientId = localStorage.getItem('meli_client_id');
|
||||
var clientSecret = localStorage.getItem('meli_client_secret');
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
try {
|
||||
var res = await fetch(API + '/connect', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({
|
||||
code: authCode,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
})
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('¡Conectado exitosamente con MercadoLibre!');
|
||||
window.history.replaceState({}, document.title, '/pos/marketplace-external');
|
||||
loadConfig();
|
||||
} else {
|
||||
alert('Error conectando: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadConfig();
|
||||
});
|
||||
})();
|
||||
@@ -123,6 +123,8 @@ const POS = (() => {
|
||||
currentRegister = null;
|
||||
document.getElementById('registerInfo').innerHTML =
|
||||
'<span style="color:var(--color-error);cursor:pointer;" onclick="POS.showOpenRegisterModal()" title="Clic para abrir caja">⚠ Sin caja abierta — Clic para abrir</span>';
|
||||
// Force open register modal on first load
|
||||
showOpenRegisterModal();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Register check failed:', e);
|
||||
@@ -253,6 +255,9 @@ const POS = (() => {
|
||||
discount_pct: parseFloat(item.discount_pct || 0),
|
||||
tax_rate: parseFloat(item.tax_rate || 0.16),
|
||||
stock: item.stock || 0,
|
||||
price_1: parseFloat(item.price_1 || 0),
|
||||
price_2: parseFloat(item.price_2 || 0),
|
||||
price_3: parseFloat(item.price_3 || 0),
|
||||
});
|
||||
|
||||
renderCart();
|
||||
@@ -265,6 +270,67 @@ const POS = (() => {
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
cart.length = 0;
|
||||
selectedRow = -1;
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function openCancelModal() {
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
const dialog = document.getElementById('modal-cancelar-venta');
|
||||
if (overlay) overlay.classList.add('active');
|
||||
if (dialog) dialog.classList.add('active');
|
||||
}
|
||||
|
||||
function closeCancelModal() {
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
const dialog = document.getElementById('modal-cancelar-venta');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
if (dialog) dialog.classList.remove('active');
|
||||
}
|
||||
|
||||
function changeQuantity() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const q = prompt('Nueva cantidad:', cart[selectedRow].quantity);
|
||||
if (q !== null) {
|
||||
const n = parseInt(q);
|
||||
if (n > 0) {
|
||||
cart[selectedRow].quantity = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDiscount() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const d = prompt('Descuento %:', cart[selectedRow].discount_pct);
|
||||
if (d !== null) {
|
||||
const n = parseFloat(d);
|
||||
if (n >= 0 && n <= 100) {
|
||||
cart[selectedRow].discount_pct = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire confirm-cancel button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('btnConfirmCancel');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
clearCart();
|
||||
closeCancelModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function renderCart() {
|
||||
const tbody = document.getElementById('cartBody');
|
||||
const table = document.getElementById('cartTable');
|
||||
@@ -472,6 +538,9 @@ const POS = (() => {
|
||||
cost: item.cost,
|
||||
tax_rate: item.tax_rate,
|
||||
stock: item.stock,
|
||||
price_1: item.price_1,
|
||||
price_2: item.price_2,
|
||||
price_3: item.price_3,
|
||||
});
|
||||
hideSearchResults();
|
||||
document.getElementById('itemSearch').value = '';
|
||||
@@ -530,11 +599,22 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function recalcCartPrices() {
|
||||
const tier = currentCustomer ? (currentCustomer.price_tier || 1) : 1;
|
||||
cart.forEach(item => {
|
||||
if (item.price_1 > 0) {
|
||||
item.unit_price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function selectCustomer(customer) {
|
||||
currentCustomer = customer;
|
||||
document.getElementById('customerAutocomplete').style.display = 'none';
|
||||
document.getElementById('customerSearchWrap').querySelector('input').style.display = 'none';
|
||||
|
||||
recalcCartPrices();
|
||||
|
||||
const tiers = { 1: 'P1 Mostrador', 2: 'P2 Taller', 3: 'P3 Mayoreo' };
|
||||
document.getElementById('customerName').textContent = customer.name;
|
||||
document.getElementById('customerTier').textContent = tiers[customer.price_tier] || 'P1';
|
||||
@@ -1255,7 +1335,7 @@ const POS = (() => {
|
||||
init();
|
||||
|
||||
return {
|
||||
addToCart, removeFromCart, selectRow,
|
||||
addToCart, removeFromCart, clearCart, selectRow,
|
||||
updateQty, updateDiscount,
|
||||
addFromSearch, hideSearchResults,
|
||||
selectCustomer, clearCustomer,
|
||||
@@ -1268,5 +1348,6 @@ const POS = (() => {
|
||||
connectThermal, thermalPrint,
|
||||
showOpenRegisterModal, closeOpenRegisterModal, openRegister,
|
||||
showCutZModal, closeCutZModal, loadCutX, confirmCutZ,
|
||||
openCancelModal, closeCancelModal, changeQuantity, applyDiscount,
|
||||
};
|
||||
})();
|
||||
|
||||
2
pos/static/js/pos.min.js
vendored
2
pos/static/js/pos.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@
|
||||
{ name: _t('customers'), href: '/pos/customers', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>' },
|
||||
{ name: 'Cotizaciones', href: '/pos/quotations', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="15" x2="15" y2="15"/><line x1="12" y1="12" x2="12" y2="18"/>' },
|
||||
{ name: 'Marketplace', href: '/pos/marketplace', icon: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>' },
|
||||
{ name: 'MercadoLibre', href: '/pos/marketplace-external', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' },
|
||||
{ name: _t('invoicing'), href: '/pos/invoicing', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>' },
|
||||
{ name: _t('accounting'), href: '/pos/accounting', icon: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>' },
|
||||
{ name: _t('reports'), href: '/pos/reports', icon: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' },
|
||||
@@ -180,4 +181,7 @@
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.closeSidebar = closeSidebar;
|
||||
|
||||
// Reveal sidebar smoothly after replacement to avoid flash/stun
|
||||
document.body.classList.add('sidebar-ready');
|
||||
|
||||
})();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
var activePhone = null;
|
||||
var pollTimer = null;
|
||||
var statusPollTimer = null;
|
||||
var qrPollTimer = null;
|
||||
var connectionState = 'unknown'; // 'open', 'close', 'connecting', 'unknown'
|
||||
|
||||
// -- Helpers ---------------------------------------------------------------
|
||||
@@ -88,6 +89,10 @@
|
||||
api('GET', '/status').then(function (data) {
|
||||
var state = (data.instance || data).state || data.state || 'close';
|
||||
updateConnectionUI(state);
|
||||
// If bridge already has a QR ready, show it immediately
|
||||
if (state === 'qr' || state === 'connecting') {
|
||||
fetchQR();
|
||||
}
|
||||
}).catch(function () {
|
||||
updateConnectionUI('close');
|
||||
});
|
||||
@@ -106,7 +111,8 @@
|
||||
// Load conversations + start polling on page load / reconnect
|
||||
loadConversations();
|
||||
startPolling();
|
||||
} else if (state === 'connecting') {
|
||||
stopQRPolling();
|
||||
} else if (state === 'connecting' || state === 'qr') {
|
||||
statusDot.className = 'status-dot status-dot--warn';
|
||||
statusText.textContent = 'Escaneando QR...';
|
||||
connectSection.style.display = 'flex';
|
||||
@@ -125,6 +131,7 @@
|
||||
refreshQrBtn.style.display = 'none';
|
||||
qrImg.style.display = 'none';
|
||||
qrPlaceholder.style.display = '';
|
||||
stopQRPolling();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +148,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Instance created, now fetch QR
|
||||
fetchQR();
|
||||
// Switch UI to connecting state immediately
|
||||
updateConnectionUI('connecting');
|
||||
qrPlaceholder.textContent = 'Iniciando conexion con WhatsApp, generando QR...';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
|
||||
// Start polling for QR; the first fetchQR may not have QR ready yet
|
||||
startStatusPolling();
|
||||
startQRPolling();
|
||||
}).catch(function () {
|
||||
connectBtn.disabled = false;
|
||||
connectBtn.textContent = 'Conectar WhatsApp';
|
||||
@@ -151,7 +165,10 @@
|
||||
}
|
||||
|
||||
function fetchQR() {
|
||||
qrPlaceholder.textContent = 'Generando QR...';
|
||||
// Only update placeholder text if we don't already have a QR image showing
|
||||
if (qrImg.style.display !== 'block') {
|
||||
qrPlaceholder.textContent = 'Generando codigo QR, espera unos segundos...';
|
||||
}
|
||||
|
||||
api('GET', '/qr').then(function (data) {
|
||||
var base64 = data.qr || data.base64 || data.qrcode || '';
|
||||
@@ -164,14 +181,18 @@
|
||||
|
||||
// Start polling for connection state while QR is shown
|
||||
startStatusPolling();
|
||||
startQRPolling();
|
||||
} else if ((data.instance && data.instance.state === 'open') || data.state === 'open') {
|
||||
// Already connected
|
||||
updateConnectionUI('open');
|
||||
loadConversations();
|
||||
} else {
|
||||
qrPlaceholder.textContent = 'No se pudo generar el QR. Intenta de nuevo.';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
// QR not ready yet — this is normal right after pressing Connect
|
||||
if (qrImg.style.display !== 'block') {
|
||||
qrPlaceholder.textContent = 'Generando codigo QR, por favor espera... (el codigo cambia cada pocos segundos, escanealo en cuanto aparezca)';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}).catch(function () {
|
||||
qrPlaceholder.textContent = 'Error al obtener QR';
|
||||
@@ -208,6 +229,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startQRPolling() {
|
||||
stopQRPolling();
|
||||
qrPollTimer = setInterval(function () {
|
||||
if (connectionState === 'connecting' || connectionState === 'qr') {
|
||||
fetchQR();
|
||||
} else {
|
||||
stopQRPolling();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopQRPolling() {
|
||||
if (qrPollTimer) {
|
||||
clearInterval(qrPollTimer);
|
||||
qrPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener('click', doConnect);
|
||||
disconnectBtn.addEventListener('click', doDisconnect);
|
||||
refreshQrBtn.addEventListener('click', fetchQR);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// /home/Autopartes/pos/static/pwa/sw.js
|
||||
// Nexus POS — Service Worker v6
|
||||
// Nexus POS — Service Worker v9
|
||||
// Self-contained vanilla JS. No external imports.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v6';
|
||||
const CACHE_NAME = 'nexus-pos-v11';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
|
||||
Reference in New Issue
Block a user