Phase 1 - Analytics y Reportes: - PostMetrics and AnalyticsReport models for tracking engagement - Analytics service with dashboard stats, top posts, optimal times - 8 API endpoints at /api/analytics/* - Interactive dashboard with Chart.js charts - Celery tasks for metrics fetch (15min) and weekly reports Phase 2 - Integración Odoo: - Lead and OdooSyncLog models for CRM integration - Odoo fields added to Product and Service models - XML-RPC service for bidirectional sync - Lead management API at /api/leads/* - Leads dashboard template - Celery tasks for product/service sync and lead export Phase 3 - A/B Testing y Recycling: - ABTest, ABTestVariant, RecycledPost models - Statistical winner analysis using chi-square test - Content recycling with engagement-based scoring - APIs at /api/ab-tests/* and /api/recycling/* - Automated test evaluation and content recycling tasks Phase 4 - Thread Series y Templates: - ThreadSeries and ThreadPost models for multi-post threads - AI-powered thread generation - Enhanced ImageTemplate with HTML template support - APIs at /api/threads/* and /api/templates/* - Thread scheduling with reply chain support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
558 lines
25 KiB
HTML
558 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Leads - Social Media Automation</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||
<style>
|
||
body { background-color: #1a1a2e; color: #eee; }
|
||
.card { background-color: #16213e; border-radius: 12px; }
|
||
.accent { color: #d4a574; }
|
||
.btn-primary { background-color: #d4a574; color: #1a1a2e; }
|
||
.btn-primary:hover { background-color: #c49564; }
|
||
.btn-secondary { background-color: #374151; color: #fff; }
|
||
.btn-secondary:hover { background-color: #4b5563; }
|
||
.status-new { background-color: #3b82f6; }
|
||
.status-contacted { background-color: #8b5cf6; }
|
||
.status-qualified { background-color: #f59e0b; }
|
||
.status-proposal { background-color: #ec4899; }
|
||
.status-won { background-color: #10b981; }
|
||
.status-lost { background-color: #ef4444; }
|
||
.priority-urgent { border-left: 4px solid #ef4444; }
|
||
.priority-high { border-left: 4px solid #f59e0b; }
|
||
.priority-medium { border-left: 4px solid #3b82f6; }
|
||
.priority-low { border-left: 4px solid #6b7280; }
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen">
|
||
<!-- Header -->
|
||
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4">
|
||
<div class="flex justify-between items-center">
|
||
<h1 class="text-2xl font-bold">
|
||
<span class="accent">Consultoría AS</span> - Leads
|
||
</h1>
|
||
<nav class="flex gap-4">
|
||
<a href="/dashboard" class="px-4 py-2 rounded hover:bg-gray-800">Home</a>
|
||
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
|
||
<a href="/dashboard/leads" class="px-4 py-2 rounded bg-gray-800">Leads</a>
|
||
<a href="/dashboard/analytics" class="px-4 py-2 rounded hover:bg-gray-800">Analytics</a>
|
||
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container mx-auto px-6 py-8">
|
||
<!-- Stats Cards -->
|
||
<div class="grid grid-cols-5 gap-4 mb-8">
|
||
<div class="card p-4 text-center">
|
||
<div class="text-3xl font-bold accent" id="totalLeads">-</div>
|
||
<div class="text-gray-400 text-sm">Total Leads</div>
|
||
</div>
|
||
<div class="card p-4 text-center">
|
||
<div class="text-3xl font-bold text-blue-400" id="newLeads">-</div>
|
||
<div class="text-gray-400 text-sm">Nuevos</div>
|
||
</div>
|
||
<div class="card p-4 text-center">
|
||
<div class="text-3xl font-bold text-purple-400" id="contactedLeads">-</div>
|
||
<div class="text-gray-400 text-sm">Contactados</div>
|
||
</div>
|
||
<div class="card p-4 text-center">
|
||
<div class="text-3xl font-bold text-green-400" id="qualifiedLeads">-</div>
|
||
<div class="text-gray-400 text-sm">Calificados</div>
|
||
</div>
|
||
<div class="card p-4 text-center">
|
||
<div class="text-3xl font-bold text-yellow-400" id="unsyncedLeads">-</div>
|
||
<div class="text-gray-400 text-sm">Sin Sincronizar</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filters and Actions -->
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div class="flex gap-2">
|
||
<select id="statusFilter" onchange="loadLeads()"
|
||
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
|
||
<option value="">Todos los estados</option>
|
||
<option value="new">Nuevo</option>
|
||
<option value="contacted">Contactado</option>
|
||
<option value="qualified">Calificado</option>
|
||
<option value="proposal">Propuesta</option>
|
||
<option value="won">Ganado</option>
|
||
<option value="lost">Perdido</option>
|
||
</select>
|
||
<select id="priorityFilter" onchange="loadLeads()"
|
||
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
|
||
<option value="">Todas las prioridades</option>
|
||
<option value="urgent">Urgente</option>
|
||
<option value="high">Alta</option>
|
||
<option value="medium">Media</option>
|
||
<option value="low">Baja</option>
|
||
</select>
|
||
<select id="platformFilter" onchange="loadLeads()"
|
||
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
|
||
<option value="">Todas las plataformas</option>
|
||
<option value="x">X (Twitter)</option>
|
||
<option value="threads">Threads</option>
|
||
<option value="instagram">Instagram</option>
|
||
<option value="facebook">Facebook</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button onclick="showNewLeadModal()" class="btn-primary px-4 py-2 rounded">
|
||
+ Nuevo Lead
|
||
</button>
|
||
<button onclick="syncLeadsToOdoo()" class="btn-secondary px-4 py-2 rounded">
|
||
Sincronizar a Odoo
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Leads List -->
|
||
<div class="card p-6">
|
||
<div id="leadsList" class="space-y-4">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<div class="flex justify-center mt-6 gap-2">
|
||
<button onclick="prevPage()" id="prevBtn" class="btn-secondary px-4 py-2 rounded" disabled>
|
||
Anterior
|
||
</button>
|
||
<span id="pageInfo" class="px-4 py-2">Página 1</span>
|
||
<button onclick="nextPage()" id="nextBtn" class="btn-secondary px-4 py-2 rounded">
|
||
Siguiente
|
||
</button>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- New Lead Modal -->
|
||
<div id="newLeadModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
|
||
<div class="card p-6 w-full max-w-lg">
|
||
<h3 class="text-xl font-semibold mb-4">Nuevo Lead</h3>
|
||
<form id="newLeadForm" onsubmit="createLead(event)">
|
||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Nombre</label>
|
||
<input type="text" name="name" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Email</label>
|
||
<input type="email" name="email" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Teléfono</label>
|
||
<input type="text" name="phone" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Empresa</label>
|
||
<input type="text" name="company" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Plataforma *</label>
|
||
<select name="platform" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
<option value="manual">Manual</option>
|
||
<option value="x">X (Twitter)</option>
|
||
<option value="threads">Threads</option>
|
||
<option value="instagram">Instagram</option>
|
||
<option value="facebook">Facebook</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Prioridad</label>
|
||
<select name="priority" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
<option value="medium">Media</option>
|
||
<option value="low">Baja</option>
|
||
<option value="high">Alta</option>
|
||
<option value="urgent">Urgente</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm text-gray-400 mb-1">Interés</label>
|
||
<textarea name="interest" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm text-gray-400 mb-1">Notas</label>
|
||
<textarea name="notes" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||
</div>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" onclick="closeNewLeadModal()" class="btn-secondary px-4 py-2 rounded">
|
||
Cancelar
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded">
|
||
Crear Lead
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Lead Modal -->
|
||
<div id="editLeadModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
|
||
<div class="card p-6 w-full max-w-lg">
|
||
<h3 class="text-xl font-semibold mb-4">Editar Lead</h3>
|
||
<form id="editLeadForm" onsubmit="updateLead(event)">
|
||
<input type="hidden" name="lead_id" id="editLeadId">
|
||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Nombre</label>
|
||
<input type="text" name="name" id="editName" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Email</label>
|
||
<input type="email" name="email" id="editEmail" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Teléfono</label>
|
||
<input type="text" name="phone" id="editPhone" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Empresa</label>
|
||
<input type="text" name="company" id="editCompany" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Estado</label>
|
||
<select name="status" id="editStatus" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
<option value="new">Nuevo</option>
|
||
<option value="contacted">Contactado</option>
|
||
<option value="qualified">Calificado</option>
|
||
<option value="proposal">Propuesta</option>
|
||
<option value="won">Ganado</option>
|
||
<option value="lost">Perdido</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-1">Prioridad</label>
|
||
<select name="priority" id="editPriority" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||
<option value="low">Baja</option>
|
||
<option value="medium">Media</option>
|
||
<option value="high">Alta</option>
|
||
<option value="urgent">Urgente</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm text-gray-400 mb-1">Interés</label>
|
||
<textarea name="interest" id="editInterest" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm text-gray-400 mb-1">Notas</label>
|
||
<textarea name="notes" id="editNotes" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||
</div>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" onclick="closeEditLeadModal()" class="btn-secondary px-4 py-2 rounded">
|
||
Cancelar
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded">
|
||
Guardar
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentPage = 0;
|
||
const pageSize = 20;
|
||
let totalLeads = 0;
|
||
|
||
async function loadLeads() {
|
||
const status = document.getElementById('statusFilter').value;
|
||
const priority = document.getElementById('priorityFilter').value;
|
||
const platform = document.getElementById('platformFilter').value;
|
||
|
||
const params = new URLSearchParams({
|
||
limit: pageSize,
|
||
offset: currentPage * pageSize
|
||
});
|
||
|
||
if (status) params.append('status', status);
|
||
if (priority) params.append('priority', priority);
|
||
if (platform) params.append('platform', platform);
|
||
|
||
try {
|
||
const res = await fetch(`/api/leads/?${params}`);
|
||
const data = await res.json();
|
||
|
||
totalLeads = data.total;
|
||
renderLeads(data.leads);
|
||
updatePagination();
|
||
|
||
// Also load stats
|
||
loadStats();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading leads:', error);
|
||
}
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const res = await fetch('/api/leads/stats/summary');
|
||
const stats = await res.json();
|
||
|
||
document.getElementById('totalLeads').textContent = stats.total;
|
||
document.getElementById('newLeads').textContent = stats.by_status.new || 0;
|
||
document.getElementById('contactedLeads').textContent = stats.by_status.contacted || 0;
|
||
document.getElementById('qualifiedLeads').textContent = stats.by_status.qualified || 0;
|
||
document.getElementById('unsyncedLeads').textContent = stats.unsynced_to_odoo || 0;
|
||
|
||
} catch (error) {
|
||
console.error('Error loading stats:', error);
|
||
}
|
||
}
|
||
|
||
function renderLeads(leads) {
|
||
const container = document.getElementById('leadsList');
|
||
container.innerHTML = '';
|
||
|
||
const statusLabels = {
|
||
new: 'Nuevo',
|
||
contacted: 'Contactado',
|
||
qualified: 'Calificado',
|
||
proposal: 'Propuesta',
|
||
won: 'Ganado',
|
||
lost: 'Perdido'
|
||
};
|
||
|
||
const platformIcons = {
|
||
x: '𝕏',
|
||
threads: '🧵',
|
||
instagram: '📷',
|
||
facebook: '📘',
|
||
manual: '✏️'
|
||
};
|
||
|
||
leads.forEach(lead => {
|
||
container.innerHTML += `
|
||
<div class="bg-gray-800 rounded-lg p-4 priority-${lead.priority}">
|
||
<div class="flex justify-between items-start mb-3">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-2xl">${platformIcons[lead.platform] || '📱'}</span>
|
||
<div>
|
||
<h4 class="font-semibold">${lead.name || lead.username || 'Sin nombre'}</h4>
|
||
<p class="text-sm text-gray-400">
|
||
${lead.email || ''} ${lead.phone ? '| ' + lead.phone : ''}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="status-${lead.status} px-2 py-1 rounded text-xs text-white">
|
||
${statusLabels[lead.status] || lead.status}
|
||
</span>
|
||
${lead.synced_to_odoo ? '<span class="text-green-400 text-xs">✓ Odoo</span>' : ''}
|
||
</div>
|
||
</div>
|
||
|
||
${lead.interest ? `<p class="text-sm text-gray-300 mb-2">${lead.interest}</p>` : ''}
|
||
${lead.company ? `<p class="text-xs text-gray-500 mb-2">Empresa: ${lead.company}</p>` : ''}
|
||
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-xs text-gray-500">
|
||
${new Date(lead.created_at).toLocaleDateString()}
|
||
</span>
|
||
<div class="flex gap-2">
|
||
<button onclick="editLead(${lead.id})" class="text-sm text-blue-400 hover:underline">
|
||
Editar
|
||
</button>
|
||
${!lead.synced_to_odoo ? `
|
||
<button onclick="syncSingleLead(${lead.id})" class="text-sm text-accent hover:underline">
|
||
Sincronizar
|
||
</button>
|
||
` : ''}
|
||
<button onclick="deleteLead(${lead.id})" class="text-sm text-red-400 hover:underline">
|
||
Eliminar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
if (leads.length === 0) {
|
||
container.innerHTML = '<p class="text-gray-500 text-center py-8">No hay leads que coincidan con los filtros</p>';
|
||
}
|
||
}
|
||
|
||
function updatePagination() {
|
||
const totalPages = Math.ceil(totalLeads / pageSize);
|
||
document.getElementById('pageInfo').textContent = `Página ${currentPage + 1} de ${totalPages || 1}`;
|
||
document.getElementById('prevBtn').disabled = currentPage === 0;
|
||
document.getElementById('nextBtn').disabled = currentPage >= totalPages - 1;
|
||
}
|
||
|
||
function prevPage() {
|
||
if (currentPage > 0) {
|
||
currentPage--;
|
||
loadLeads();
|
||
}
|
||
}
|
||
|
||
function nextPage() {
|
||
const totalPages = Math.ceil(totalLeads / pageSize);
|
||
if (currentPage < totalPages - 1) {
|
||
currentPage++;
|
||
loadLeads();
|
||
}
|
||
}
|
||
|
||
function showNewLeadModal() {
|
||
document.getElementById('newLeadModal').classList.remove('hidden');
|
||
}
|
||
|
||
function closeNewLeadModal() {
|
||
document.getElementById('newLeadModal').classList.add('hidden');
|
||
document.getElementById('newLeadForm').reset();
|
||
}
|
||
|
||
async function createLead(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
const leadData = {
|
||
name: formData.get('name') || null,
|
||
email: formData.get('email') || null,
|
||
phone: formData.get('phone') || null,
|
||
company: formData.get('company') || null,
|
||
platform: formData.get('platform'),
|
||
priority: formData.get('priority'),
|
||
interest: formData.get('interest') || null,
|
||
notes: formData.get('notes') || null
|
||
};
|
||
|
||
try {
|
||
const res = await fetch('/api/leads/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(leadData)
|
||
});
|
||
|
||
if (res.ok) {
|
||
closeNewLeadModal();
|
||
loadLeads();
|
||
} else {
|
||
const data = await res.json();
|
||
alert('Error: ' + (data.detail || 'Error creando lead'));
|
||
}
|
||
} catch (error) {
|
||
alert('Error creando lead');
|
||
}
|
||
}
|
||
|
||
async function editLead(leadId) {
|
||
try {
|
||
const res = await fetch(`/api/leads/${leadId}`);
|
||
const lead = await res.json();
|
||
|
||
document.getElementById('editLeadId').value = lead.id;
|
||
document.getElementById('editName').value = lead.name || '';
|
||
document.getElementById('editEmail').value = lead.email || '';
|
||
document.getElementById('editPhone').value = lead.phone || '';
|
||
document.getElementById('editCompany').value = lead.company || '';
|
||
document.getElementById('editStatus').value = lead.status;
|
||
document.getElementById('editPriority').value = lead.priority;
|
||
document.getElementById('editInterest').value = lead.interest || '';
|
||
document.getElementById('editNotes').value = lead.notes || '';
|
||
|
||
document.getElementById('editLeadModal').classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
alert('Error cargando lead');
|
||
}
|
||
}
|
||
|
||
function closeEditLeadModal() {
|
||
document.getElementById('editLeadModal').classList.add('hidden');
|
||
}
|
||
|
||
async function updateLead(event) {
|
||
event.preventDefault();
|
||
const leadId = document.getElementById('editLeadId').value;
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
const leadData = {
|
||
name: formData.get('name') || null,
|
||
email: formData.get('email') || null,
|
||
phone: formData.get('phone') || null,
|
||
company: formData.get('company') || null,
|
||
status: formData.get('status'),
|
||
priority: formData.get('priority'),
|
||
interest: formData.get('interest') || null,
|
||
notes: formData.get('notes') || null
|
||
};
|
||
|
||
try {
|
||
const res = await fetch(`/api/leads/${leadId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(leadData)
|
||
});
|
||
|
||
if (res.ok) {
|
||
closeEditLeadModal();
|
||
loadLeads();
|
||
} else {
|
||
const data = await res.json();
|
||
alert('Error: ' + (data.detail || 'Error actualizando lead'));
|
||
}
|
||
} catch (error) {
|
||
alert('Error actualizando lead');
|
||
}
|
||
}
|
||
|
||
async function deleteLead(leadId) {
|
||
if (!confirm('¿Estás seguro de eliminar este lead?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/leads/${leadId}`, { method: 'DELETE' });
|
||
if (res.ok) {
|
||
loadLeads();
|
||
} else {
|
||
alert('Error eliminando lead');
|
||
}
|
||
} catch (error) {
|
||
alert('Error eliminando lead');
|
||
}
|
||
}
|
||
|
||
async function syncSingleLead(leadId) {
|
||
try {
|
||
const res = await fetch(`/api/leads/${leadId}/sync-odoo`, { method: 'POST' });
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
alert('Lead sincronizado exitosamente');
|
||
loadLeads();
|
||
} else {
|
||
alert('Error: ' + (data.detail || 'Error sincronizando'));
|
||
}
|
||
} catch (error) {
|
||
alert('Error sincronizando lead');
|
||
}
|
||
}
|
||
|
||
async function syncLeadsToOdoo() {
|
||
try {
|
||
const res = await fetch('/api/odoo/sync/leads', { method: 'POST' });
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
alert(`Sincronización completada: ${data.created} leads exportados`);
|
||
loadLeads();
|
||
} else {
|
||
alert('Error: ' + (data.detail || 'Error sincronizando'));
|
||
}
|
||
} catch (error) {
|
||
alert('Error sincronizando leads');
|
||
}
|
||
}
|
||
|
||
// Load on page load
|
||
document.addEventListener('DOMContentLoaded', loadLeads);
|
||
</script>
|
||
</body>
|
||
</html>
|