/** * 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 = '● Conectado'; 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 = '● No conectado'; formDiv.style.display = 'block'; connectedDiv.style.display = 'none'; } } catch (e) { console.error(e); document.getElementById('configStatus').innerHTML = '

Error cargando configuración

'; } } 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 = '
' + '
' + '
'; 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 = renderEmptyState({ icon: '', title: 'Error cargando publicaciones', subtitle: 'No se pudieron obtener las publicaciones de MercadoLibre. Intenta de nuevo.' }); } }; 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 = renderEmptyState({ icon: '', title: 'Sin publicaciones', subtitle: 'Aún no hay publicaciones en MercadoLibre. Ve a Inventario y publica un producto.', action: 'Ir a Inventario' }); return; } container.innerHTML = filtered.map(function(l) { var statusClass = 'meli-status--' + (l.external_status || 'pending'); return '
' + '
' + '
' + escapeHtml(l.title || l.inventory_name || 'Sin título') + '
' + '' + (l.external_status || '—') + '' + '
' + '
' + 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: ' + escapeHtml(l.external_item_id || '—') + '
' + '
' + '' + (l.external_status === 'active' ? '' : '') + '' + '
' + '
'; }).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) { showToast('Sincronizado: $' + data.price + ' · Stock: ' + data.stock, 'ok', { title: 'Publicación actualizada' }); loadListings(); } else { showToast(data.error || 'Error desconocido', 'error', { title: 'Error de sincronización' }); } } catch (e) { showToast(e.message, 'error', { title: 'Error de red' }); } }; 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 = renderSkeletonRows(6, 5); 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 = '' + renderEmptyState({ icon: '', title: 'Error cargando órdenes', subtitle: 'No se pudieron obtener las órdenes de MercadoLibre.' }) + ''; } }; 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 = '' + renderEmptyState({ icon: '', title: 'Sin órdenes', subtitle: 'No hay órdenes de MercadoLibre en este momento.' }) + ''; return; } tbody.innerHTML = filtered.map(function(o) { var statusClass = 'meli-status--' + (o.status || 'pending'); return '' + '' + escapeHtml(o.external_order_id) + '' + '' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '' + '$' + (o.total_amount || 0).toFixed(2) + '' + '' + (o.status || '—') + '' + '' + (o.created_at ? o.created_at.split('T')[0] : '—') + '' + '' + (o.status === 'pending' ? ' ' : '') + '' + '' + ''; }).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 '' + escapeHtml(it.title || '—') + '' + it.quantity + '$' + (it.unit_price || 0).toFixed(2) + ''; }).join(''); body.innerHTML = '
' + '

Comprador: ' + escapeHtml(o.buyer_name || '—') + ' (' + escapeHtml(o.buyer_nickname || '—') + ')

' + '

Email: ' + escapeHtml(o.buyer_email || '—') + '

' + '

Teléfono: ' + escapeHtml(o.buyer_phone || '—') + '

' + '

Total: $' + (o.total_amount || 0).toFixed(2) + '

' + '

Estado ML: ' + escapeHtml(o.external_status || '—') + '

' + '

Estado Nexus: ' + escapeHtml(o.status || '—') + '

' + '
' + '

Items

' + '' + itemsHtml + '
ProductoCantidadPrecio
'; footer.innerHTML = ''; if (o.status === 'pending') { footer.innerHTML += ' '; } if (o.status === 'confirmed') { footer.innerHTML += ' '; } if (o.status === 'packed') { footer.innerHTML += ' '; } footer.innerHTML += ''; } catch (e) { body.innerHTML = '

Error: ' + escapeHtml(e.message) + '

'; } }; 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) { showToast('Orden convertida a venta #' + data.sale_id, 'ok', { title: 'Venta creada' }); loadOrders(); } else { showToast(data.error || 'Error desconocido', 'error', { title: 'Error al convertir' }); } } catch (e) { showToast(e.message, 'error', { title: 'Error de red' }); } }; 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); } })(); } // ML Status Cards (sparkline simulation) window.loadMeliStats = async function() { var container = document.getElementById('meliStatsBar'); if (!container) return; try { var res = await fetch(API + '/listings?page=1&per_page=200', { headers: headers() }); if (!res.ok) throw new Error('Failed'); var data = await res.json(); var items = data.items || []; var active = items.filter(function(l) { return l.external_status === 'active'; }).length; var paused = items.filter(function(l) { return l.external_status === 'paused'; }).length; var closed = items.filter(function(l) { return l.external_status === 'closed'; }).length; var total = items.length; var html = '
' + '
Activas
' + active + '
' + '
Pausadas
' + paused + '
' + '
Cerradas
' + closed + '
' + '
Total
' + total + '
'; // Sparkline simulation html += '
Tendencia
'; html += '
'; container.innerHTML = html; if (typeof renderSparkline === 'function') { renderSparkline('#meliSparkline', [active, paused, closed, total % 50, active - 2, paused + 1, closed, active], { prefix: '' }); } } catch(e) { container.innerHTML = ''; } }; // Kanban Order View var _orderViewMode = 'table'; window.toggleOrderView = function() { _orderViewMode = _orderViewMode === 'table' ? 'kanban' : 'table'; document.getElementById('ordersTableView').style.display = _orderViewMode === 'table' ? '' : 'none'; document.getElementById('ordersKanbanView').style.display = _orderViewMode === 'kanban' ? '' : 'none'; document.getElementById('btnKanbanView').textContent = _orderViewMode === 'table' ? '📋 Kanban' : '📄 Tabla'; if (_orderViewMode === 'kanban') renderKanbanOrders(); }; function renderKanbanOrders() { var container = document.getElementById('ordersKanbanView'); if (!container) return; var columns = [ { key: 'pending', label: 'Pendientes', badge: 'badge--pending' }, { key: 'confirmed', label: 'Confirmadas', badge: 'badge--ok' }, { key: 'packed', label: 'Empacadas', badge: 'badge--transit' }, { key: 'shipped', label: 'Enviadas', badge: 'badge--transit' }, { key: 'delivered', label: 'Entregadas', badge: 'badge--complete' }, { key: 'cancelled', label: 'Canceladas', badge: 'badge--cancelled' }, ]; var html = '
'; columns.forEach(function(col) { var items = ordersData.filter(function(o) { return o.status === col.key; }); html += '
'; html += '
' + col.label + '' + items.length + '
'; html += '
'; items.slice(0, 20).forEach(function(o) { html += '
' + '
' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '
' + '
$' + (o.total_amount || 0).toFixed(2) + ' · ' + escapeHtml(o.external_order_id || '') + '
' + '
'; }); if (items.length > 20) html += '
+' + (items.length - 20) + ' más
'; html += '
'; }); html += '
'; container.innerHTML = html; } // Register Cmd+K items if (typeof registerCmdKItem === 'function') { registerCmdKItem({ group: 'MercadoLibre', label: 'Configuración ML', href: '/pos/marketplace-external', icon: '⚙️' }); registerCmdKItem({ group: 'MercadoLibre', label: 'Publicaciones ML', href: '/pos/marketplace-external#listings', icon: '📦' }); registerCmdKItem({ group: 'MercadoLibre', label: 'Órdenes ML', href: '/pos/marketplace-external#orders', icon: '🛒' }); } document.addEventListener('DOMContentLoaded', function() { loadConfig(); }); })();