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

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