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:
2026-05-26 04:24:07 +00:00
parent 50c0dbe7d4
commit a236187f3a
66 changed files with 7335 additions and 498 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"}();

View File

@@ -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(' &rsaquo; '));
this.setBreadcrumb('<nav class="breadcrumb">' + parts.join('<span class="breadcrumb__sep">&rsaquo;</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) + ')">&larr; 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 &rarr;</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;

View File

@@ -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 });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,11 @@ const Config = (() => {
return true;
}
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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,

File diff suppressed because one or more lines are too long

View File

@@ -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;
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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;

View File

@@ -154,6 +154,9 @@
}
};
// Expose for other scripts
window.isKioskEnabled = isKioskEnabled;
// ─── Init ───
if (isKioskEnabled()) {
activate();

View 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();
});
})();

View File

@@ -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">&#x26A0; 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,
};
})();

File diff suppressed because one or more lines are too long

View File

@@ -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');
})();

View File

@@ -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);

View File

@@ -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',