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:
@@ -65,7 +65,9 @@
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;font-family:var(--font-mono);font-weight:700;">$' + fmt(q.total) + '</td>';
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;">' + statusBadge + '</td>';
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;color:var(--color-text-muted);">' + dateStr + '</td>';
|
||||
html += '<td><button onclick="deleteQuote(' + q.id + ', event)" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:16px;padding:4px 8px;border-radius:4px;" onmouseover="this.style.color=\'#F85149\';this.style.background=\'rgba(248,81,73,0.1)\'" onmouseout="this.style.color=\'var(--color-text-muted)\';this.style.background=\'none\'">🗑️</button></td>';
|
||||
html += '<td><button onclick="deleteQuote(' + q.id + ', event)" style="background:var(--color-bg-overlay);border:1.5px solid var(--color-border);color:var(--color-text-muted);cursor:pointer;font-size:13px;padding:6px 10px;border-radius:var(--radius-md);display:inline-flex;align-items:center;gap:4px;transition:var(--transition-fast);" onmouseover="this.style.color=\'var(--color-error)\';this.style.borderColor=\'var(--color-error)\';this.style.background=\'rgba(248,81,73,0.08)\'" onmouseout="this.style.color=\'var(--color-text-muted)\';this.style.borderColor=\'var(--color-border)\';this.style.background=\'var(--color-bg-overlay)\'">';
|
||||
html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||
html += 'Eliminar</button></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
@@ -114,15 +116,33 @@
|
||||
html += '<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-text-accent);">Total: $' + fmt(q.total) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:var(--space-5);display:flex;gap:var(--space-3);justify-content:flex-end;flex-wrap:wrap;">';
|
||||
html += '<div style="margin-top:var(--space-5);">';
|
||||
// Primary actions
|
||||
if (q.status === 'active') {
|
||||
html += '<button class="btn btn--ghost" onclick="editQuote(' + q.id + ')" style="color:#4f46e5;">Editar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="convertQuote(' + q.id + ')" style="color:#059669;">Convertir a venta</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="shareQuote(' + q.id + ')">Compartir link</button>';
|
||||
html += '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3);">';
|
||||
html += '<button class="btn btn--primary" onclick="convertQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>';
|
||||
html += 'Convertir a Venta</button>';
|
||||
html += '<button class="btn btn--secondary" onclick="editQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
||||
html += 'Editar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="shareQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--color-border);">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>';
|
||||
html += 'Compartir</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<button class="btn btn--ghost" onclick="deleteQuote(' + q.id + ')" style="color:#F85149;">Eliminar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="exportVisibleTableCSV(\'cotizacion_' + q.id + '\')">Exportar CSV</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="window.print()">Imprimir</button>';
|
||||
// Secondary actions
|
||||
html += '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;justify-content:flex-end;">';
|
||||
html += '<button class="btn btn--ghost" onclick="window.print()" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>';
|
||||
html += 'Imprimir</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="exportVisibleTableCSV(\'cotizacion_' + q.id + '\')" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
html += 'Exportar CSV</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="deleteQuote(' + q.id + ')" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;color:var(--color-error);">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||
html += 'Eliminar</button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
document.getElementById('quoteDetail').innerHTML = html;
|
||||
@@ -142,69 +162,83 @@
|
||||
} else {
|
||||
alert('Error: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(err) { alert('Error de red al eliminar: ' + err.message); });
|
||||
};
|
||||
|
||||
window.editQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_edit_quote_id', id);
|
||||
localStorage.setItem('pos_edit_quote_customer_id', q.customer_id || '');
|
||||
localStorage.setItem('pos_edit_quote_notes', q.notes || '');
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos';
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_edit_quote_id', id);
|
||||
localStorage.setItem('pos_edit_quote_customer_id', q.customer_id || '');
|
||||
localStorage.setItem('pos_edit_quote_notes', q.notes || '');
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos/sale';
|
||||
})
|
||||
.catch(function(err) { alert('Error al cargar para editar: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en editQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
window.convertQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_convert_quote_id', id);
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos';
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_convert_quote_id', id);
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos/sale';
|
||||
})
|
||||
.catch(function(err) { alert('Error al cargar para convertir: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en convertQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
window.shareQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id + '/share', { method: 'POST', headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.url) {
|
||||
navigator.clipboard.writeText(d.url).then(function() {
|
||||
alert('Link copiado al portapapeles:\n' + d.url);
|
||||
}).catch(function() {
|
||||
prompt('Copia este link:', d.url);
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id + '/share', { method: 'POST', headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.url) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(d.url).then(function() {
|
||||
alert('Link copiado al portapapeles:\n' + d.url);
|
||||
}).catch(function() {
|
||||
prompt('Copia este link:', d.url);
|
||||
});
|
||||
} else {
|
||||
prompt('Copia este link:', d.url);
|
||||
}
|
||||
} else {
|
||||
alert('Error del servidor: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
})
|
||||
.catch(function(err) { alert('Error de red al compartir: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en shareQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
// Close modal on outside click
|
||||
|
||||
Reference in New Issue
Block a user