- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
609 lines
34 KiB
JavaScript
609 lines
34 KiB
JavaScript
/**
|
|
* 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();
|
|
if (tab === 'questions') loadQuestions();
|
|
};
|
|
|
|
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) + '&scope=read+write+offline_access';
|
|
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 = '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:70%;"></div></div>'
|
|
+ '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:60%;"></div></div>'
|
|
+ '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:80%;"></div></div>';
|
|
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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
title: 'Sin publicaciones',
|
|
subtitle: 'Aún no hay publicaciones en MercadoLibre. Ve a Inventario y publica un producto.',
|
|
action: '<a href="/pos/inventory" class="btn btn--meli btn--sm">Ir a Inventario</a>'
|
|
});
|
|
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);">'
|
|
+ '<a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;color:var(--color-primary);text-decoration:none;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + ' ↗</a>'
|
|
+ '<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: <a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + escapeHtml(l.external_item_id || '—') + '</a>'
|
|
+ '</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>')
|
|
+ (l.external_status === 'closed' || !l.is_active
|
|
? '<button class="btn btn--danger btn--xs" onclick="deleteListingPermanently(' + l.id + ')">Eliminar</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) {
|
|
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); }
|
|
};
|
|
|
|
window.deleteListingPermanently = async function(id) {
|
|
if (!confirm('¿Eliminar permanentemente esta publicación del listado local? Esta acción no se puede deshacer.')) return;
|
|
try {
|
|
var res = await fetch(API + '/listings/' + id + '/permanent', { method: 'DELETE', headers: headers() });
|
|
if (res.ok) { loadListings(); } else { alert('Error al eliminar'); }
|
|
} 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 = '<tr><td colspan="6">' + renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
title: 'Error cargando órdenes',
|
|
subtitle: 'No se pudieron obtener las órdenes de MercadoLibre.'
|
|
}) + '</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">' + renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
title: 'Sin órdenes',
|
|
subtitle: 'No hay órdenes de MercadoLibre en este momento.'
|
|
}) + '</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) {
|
|
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 = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
|
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Activas</div><div class="kpi-card__value" style="color:var(--color-success);">' + active + '</div></div>' +
|
|
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Pausadas</div><div class="kpi-card__value" style="color:var(--color-warning);">' + paused + '</div></div>' +
|
|
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Cerradas</div><div class="kpi-card__value" style="color:var(--color-error);">' + closed + '</div></div>' +
|
|
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Total</div><div class="kpi-card__value">' + total + '</div></div>';
|
|
// Sparkline simulation
|
|
html += '<div class="kpi-card" style="flex:1;min-width:200px;"><div class="kpi-card__label">Tendencia</div><div id="meliSparkline"></div></div>';
|
|
html += '</div>';
|
|
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 = '<div class="kanban">';
|
|
columns.forEach(function(col) {
|
|
var items = ordersData.filter(function(o) { return o.status === col.key; });
|
|
html += '<div class="kanban__col" data-status="' + col.key + '">';
|
|
html += '<div class="kanban__col-header">' + col.label + '<span class="kanban__col-count ' + col.badge + '">' + items.length + '</span></div>';
|
|
html += '<div class="kanban__cards">';
|
|
items.slice(0, 20).forEach(function(o) {
|
|
html += '<div class="kanban__card" draggable="true" data-id="' + o.id + '">' +
|
|
'<div class="kanban__card-title">' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '</div>' +
|
|
'<div class="kanban__card-meta">$' + (o.total_amount || 0).toFixed(2) + ' · ' + escapeHtml(o.external_order_id || '') + '</div>' +
|
|
'</div>';
|
|
});
|
|
if (items.length > 20) html += '<div style="text-align:center;font-size:11px;color:var(--color-text-muted);padding:8px;">+' + (items.length - 20) + ' más</div>';
|
|
html += '</div></div>';
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ─── Questions ─────────────────────────────────────────────────────────
|
|
|
|
var questionsData = [];
|
|
|
|
window.loadQuestions = async function() {
|
|
var container = document.getElementById('questionsContainer');
|
|
container.innerHTML = '<div class="skeleton-grid">' + Array(6).fill('<div class="skeleton skeleton--card"></div>').join('') + '</div>';
|
|
try {
|
|
var res = await fetch(API + '/questions', { headers: headers() });
|
|
if (!res.ok) throw new Error('Failed to load questions');
|
|
var data = await res.json();
|
|
questionsData = data.items || [];
|
|
renderQuestions();
|
|
} catch (e) {
|
|
container.innerHTML = renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
title: 'Sin preguntas',
|
|
subtitle: 'No hay preguntas de compradores pendientes. Sincroniza con MercadoLibre para obtenerlas.',
|
|
action: '<button class="btn btn--meli btn--sm" onclick="syncQuestions()">Sincronizar con ML</button>'
|
|
});
|
|
}
|
|
};
|
|
|
|
function renderQuestions() {
|
|
var container = document.getElementById('questionsContainer');
|
|
var statusFilter = document.getElementById('questionStatusFilter').value;
|
|
var search = document.getElementById('questionSearch').value.toLowerCase();
|
|
|
|
var filtered = questionsData.filter(function(q) {
|
|
if (statusFilter && q.status !== statusFilter) return false;
|
|
if (search && !((q.question_text || '').toLowerCase().includes(search)) && !((q.listing_title || '').toLowerCase().includes(search))) return false;
|
|
return true;
|
|
});
|
|
|
|
// Stats bar
|
|
var unanswered = questionsData.filter(function(q) { return q.status === 'unanswered'; }).length;
|
|
var answered = questionsData.filter(function(q) { return q.status === 'answered'; }).length;
|
|
var total = questionsData.length;
|
|
var statsHtml = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
|
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-primary);">' + total + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Total preguntas</div></div>' +
|
|
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-error);">' + unanswered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Sin responder</div></div>' +
|
|
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-success);">' + answered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Respondidas</div></div>' +
|
|
'</div>';
|
|
document.getElementById('questionsStatsBar').innerHTML = statsHtml;
|
|
|
|
if (!filtered.length) {
|
|
container.innerHTML = renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
title: 'Sin preguntas',
|
|
subtitle: statusFilter ? 'No hay preguntas con el filtro seleccionado.' : 'No hay preguntas sincronizadas.',
|
|
action: ''
|
|
});
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filtered.map(function(q) {
|
|
var statusClass = 'meli-status--' + (q.status || 'pending');
|
|
var statusLabel = q.status === 'unanswered' ? 'Sin responder' : (q.status === 'answered' ? 'Respondida' : (q.status || '—'));
|
|
var answerHtml = '';
|
|
if (q.status === 'unanswered') {
|
|
answerHtml = '<div style="margin-top:var(--space-2);">' +
|
|
'<textarea class="meli-title-input" id="qAnswer-' + q.id + '" rows="2" placeholder="Escribe tu respuesta..."></textarea>' +
|
|
'<button class="btn btn--primary btn--xs" style="margin-top:6px;" onclick="submitAnswer(' + q.id + ')">Enviar respuesta</button>' +
|
|
'</div>';
|
|
} else if (q.answer_text) {
|
|
answerHtml = '<div style="margin-top:var(--space-2);padding:var(--space-2);background:var(--color-surface-0);border-radius:var(--radius-sm);font-size:var(--text-caption);color:var(--color-text-secondary);">' +
|
|
'<strong>Respuesta:</strong> ' + escapeHtml(q.answer_text) +
|
|
'</div>';
|
|
}
|
|
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(q.listing_title || 'Artículo sin título') + '</div>'
|
|
+ '<span class="meli-status ' + statusClass + '">' + statusLabel + '</span>'
|
|
+ '</div>'
|
|
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
|
+ 'Comprador: ' + escapeHtml(q.buyer_nickname || '—') + ' · ' + (q.question_date ? new Date(q.question_date).toLocaleString('es-MX') : '—')
|
|
+ '</div>'
|
|
+ '<div style="font-size:var(--text-body-sm);color:var(--color-text-primary);margin-bottom:var(--space-2);">'
|
|
+ '<strong>Pregunta:</strong> ' + escapeHtml(q.question_text)
|
|
+ '</div>'
|
|
+ answerHtml
|
|
+ '</div>';
|
|
}).join('');
|
|
}
|
|
|
|
window.filterQuestions = renderQuestions;
|
|
|
|
window.syncQuestions = async function() {
|
|
var btn = document.querySelector('#panel-questions .btn--primary');
|
|
if (btn) { btn.disabled = true; btn.textContent = 'Sincronizando...'; }
|
|
try {
|
|
var res = await fetch(API + '/questions/sync', { method: 'POST', headers: headers() });
|
|
var data = await res.json();
|
|
if (res.ok) {
|
|
showToast('Sincronizadas ' + (data.synced || 0) + ' preguntas', 'ok', { title: 'Sincronización' });
|
|
loadQuestions();
|
|
} else {
|
|
showToast(data.error || 'Error al sincronizar', 'error', { title: 'Error' });
|
|
}
|
|
} catch (e) {
|
|
showToast(e.message, 'error', { title: 'Error de red' });
|
|
} finally {
|
|
if (btn) { btn.disabled = false; btn.textContent = '🔄 Actualizar'; }
|
|
}
|
|
};
|
|
|
|
window.submitAnswer = async function(questionId) {
|
|
var textarea = document.getElementById('qAnswer-' + questionId);
|
|
if (!textarea) return;
|
|
var text = textarea.value.trim();
|
|
if (!text) {
|
|
showToast('Escribe una respuesta antes de enviar', 'error', { title: 'Respuesta vacía' });
|
|
return;
|
|
}
|
|
try {
|
|
var res = await fetch(API + '/questions/' + questionId + '/answer', {
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify({ text: text })
|
|
});
|
|
var data = await res.json();
|
|
if (res.ok) {
|
|
showToast('Respuesta enviada correctamente', 'ok', { title: 'Pregunta respondida' });
|
|
loadQuestions();
|
|
} else {
|
|
showToast(data.error || 'Error al enviar respuesta', 'error', { title: 'Error' });
|
|
}
|
|
} catch (e) {
|
|
showToast(e.message, 'error', { title: 'Error de red' });
|
|
}
|
|
};
|
|
|
|
// 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: '🛒' });
|
|
registerCmdKItem({ group: 'MercadoLibre', label: 'Preguntas ML', href: '/pos/marketplace-external#questions', icon: '❓' });
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadConfig();
|
|
});
|
|
})();
|