- Fix YAML files with unquoted strings containing quotes - Export singleton instances in ai/__init__.py - Fix validator scoring prompt to use replace() instead of format() to avoid conflicts with JSON curly braces Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
531 lines
24 KiB
HTML
531 lines
24 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Leads{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="animate-fade-in">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h1 class="text-3xl font-bold">Leads</h1>
|
||
<p class="text-gray-400 mt-1">Gestiona tus prospectos y oportunidades</p>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<button onclick="showNewLeadModal()" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
|
||
<span>➕</span>
|
||
<span>Nuevo Lead</span>
|
||
</button>
|
||
<button onclick="syncLeadsToOdoo()" class="btn-secondary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
|
||
<span>🔄</span>
|
||
<span>Sincronizar a Odoo</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stats Cards -->
|
||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
|
||
<div class="stat-card card rounded-2xl p-6">
|
||
<p class="text-3xl font-bold text-primary" id="totalLeads">-</p>
|
||
<p class="text-gray-400 text-sm mt-1">Total Leads</p>
|
||
</div>
|
||
<div class="stat-card card rounded-2xl p-6">
|
||
<p class="text-3xl font-bold text-blue-400" id="newLeads">-</p>
|
||
<p class="text-gray-400 text-sm mt-1">Nuevos</p>
|
||
</div>
|
||
<div class="stat-card card rounded-2xl p-6">
|
||
<p class="text-3xl font-bold text-purple-400" id="contactedLeads">-</p>
|
||
<p class="text-gray-400 text-sm mt-1">Contactados</p>
|
||
</div>
|
||
<div class="stat-card card rounded-2xl p-6">
|
||
<p class="text-3xl font-bold text-green-400" id="qualifiedLeads">-</p>
|
||
<p class="text-gray-400 text-sm mt-1">Calificados</p>
|
||
</div>
|
||
<div class="stat-card card rounded-2xl p-6">
|
||
<p class="text-3xl font-bold text-yellow-400" id="unsyncedLeads">-</p>
|
||
<p class="text-gray-400 text-sm mt-1">Sin Sincronizar</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="card rounded-2xl p-4 mb-6">
|
||
<div class="flex flex-wrap gap-4 items-center">
|
||
<select id="statusFilter" onchange="loadLeads()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||
<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-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||
<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-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||
<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>
|
||
|
||
<!-- Leads List -->
|
||
<div class="card rounded-2xl p-6">
|
||
<div id="leadsList" class="space-y-4">
|
||
<div class="text-center py-8 text-gray-500">Cargando...</div>
|
||
</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-xl" disabled>
|
||
Anterior
|
||
</button>
|
||
<span id="pageInfo" class="px-4 py-2 text-gray-400">Página 1</span>
|
||
<button onclick="nextPage()" id="nextBtn" class="btn-secondary px-4 py-2 rounded-xl">
|
||
Siguiente
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- New Lead Modal -->
|
||
<div id="newLeadModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
|
||
<div class="card rounded-2xl p-6 w-full max-w-lg mx-4">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h3 class="text-xl font-semibold">Nuevo Lead</h3>
|
||
<button onclick="closeNewLeadModal()" class="text-gray-400 hover:text-white text-xl">✕</button>
|
||
</div>
|
||
<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-2">Nombre</label>
|
||
<input type="text" name="name" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Email</label>
|
||
<input type="email" name="email" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Teléfono</label>
|
||
<input type="text" name="phone" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Empresa</label>
|
||
<input type="text" name="company" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Plataforma *</label>
|
||
<select name="platform" required class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
<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-2">Prioridad</label>
|
||
<select name="priority" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
<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-2">Interés</label>
|
||
<textarea name="interest" rows="2" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none resize-none"></textarea>
|
||
</div>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" onclick="closeNewLeadModal()" class="btn-secondary px-4 py-2 rounded-xl">
|
||
Cancelar
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-xl">
|
||
Crear Lead
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Lead Modal -->
|
||
<div id="editLeadModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
|
||
<div class="card rounded-2xl p-6 w-full max-w-lg mx-4">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h3 class="text-xl font-semibold">Editar Lead</h3>
|
||
<button onclick="closeEditLeadModal()" class="text-gray-400 hover:text-white text-xl">✕</button>
|
||
</div>
|
||
<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-2">Nombre</label>
|
||
<input type="text" name="name" id="editName" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Email</label>
|
||
<input type="email" name="email" id="editEmail" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Teléfono</label>
|
||
<input type="text" name="phone" id="editPhone" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Empresa</label>
|
||
<input type="text" name="company" id="editCompany" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-400 mb-2">Estado</label>
|
||
<select name="status" id="editStatus" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
<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-2">Prioridad</label>
|
||
<select name="priority" id="editPriority" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
|
||
<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-2">Interés</label>
|
||
<textarea name="interest" id="editInterest" rows="2" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none resize-none"></textarea>
|
||
</div>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" onclick="closeEditLeadModal()" class="btn-secondary px-4 py-2 rounded-xl">
|
||
Cancelar
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-xl">
|
||
Guardar
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_scripts %}
|
||
<style>
|
||
.priority-urgent { border-left: 4px solid #ef4444; }
|
||
.priority-high { border-left: 4px solid #f59e0b; }
|
||
.priority-medium { border-left: 4px solid #6366f1; }
|
||
.priority-low { border-left: 4px solid #6b7280; }
|
||
.status-new { background-color: rgba(59, 130, 246, 0.3); color: #60a5fa; }
|
||
.status-contacted { background-color: rgba(139, 92, 246, 0.3); color: #a78bfa; }
|
||
.status-qualified { background-color: rgba(245, 158, 11, 0.3); color: #fbbf24; }
|
||
.status-proposal { background-color: rgba(236, 72, 153, 0.3); color: #f472b6; }
|
||
.status-won { background-color: rgba(16, 185, 129, 0.3); color: #34d399; }
|
||
.status-lost { background-color: rgba(239, 68, 68, 0.3); color: #f87171; }
|
||
</style>
|
||
<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();
|
||
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');
|
||
|
||
const statusLabels = {
|
||
new: 'Nuevo',
|
||
contacted: 'Contactado',
|
||
qualified: 'Calificado',
|
||
proposal: 'Propuesta',
|
||
won: 'Ganado',
|
||
lost: 'Perdido'
|
||
};
|
||
|
||
const platformIcons = {
|
||
x: '𝕏',
|
||
threads: '🧵',
|
||
instagram: '📷',
|
||
facebook: '📘',
|
||
manual: '✏️'
|
||
};
|
||
|
||
if (leads.length === 0) {
|
||
container.innerHTML = '<p class="text-gray-500 text-center py-8">No hay leads que coincidan con los filtros</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = leads.map(lead => `
|
||
<div class="bg-dark-800/50 rounded-xl 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-full text-xs">
|
||
${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-primary hover:underline">
|
||
Sincronizar
|
||
</button>
|
||
` : ''}
|
||
<button onclick="deleteLead(${lead.id})" class="text-sm text-red-400 hover:underline">
|
||
Eliminar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
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
|
||
};
|
||
|
||
try {
|
||
const res = await fetch('/api/leads/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(leadData)
|
||
});
|
||
|
||
if (res.ok) {
|
||
closeNewLeadModal();
|
||
loadLeads();
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Lead creado</p></div>');
|
||
} else {
|
||
const data = await res.json();
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error creando lead') + '</p></div>');
|
||
}
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error creando lead</p></div>');
|
||
}
|
||
}
|
||
|
||
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('editLeadModal').classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error cargando lead</p></div>');
|
||
}
|
||
}
|
||
|
||
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
|
||
};
|
||
|
||
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();
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error actualizando') + '</p></div>');
|
||
}
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error actualizando lead</p></div>');
|
||
}
|
||
}
|
||
|
||
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 {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando lead</p></div>');
|
||
}
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando lead</p></div>');
|
||
}
|
||
}
|
||
|
||
async function syncSingleLead(leadId) {
|
||
try {
|
||
const res = await fetch(`/api/leads/${leadId}/sync-odoo`, { method: 'POST' });
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Lead sincronizado</p></div>');
|
||
loadLeads();
|
||
} else {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error sincronizando') + '</p></div>');
|
||
}
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando lead</p></div>');
|
||
}
|
||
}
|
||
|
||
async function syncLeadsToOdoo() {
|
||
showModal('Sincronizando leads a Odoo...', true);
|
||
try {
|
||
const res = await fetch('/api/odoo/sync/leads', { method: 'POST' });
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Sincronización completada</p><p class="text-gray-400 mt-2">${data.created} leads exportados</p></div>`);
|
||
loadLeads();
|
||
} else {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error sincronizando') + '</p></div>');
|
||
}
|
||
} catch (error) {
|
||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando leads</p></div>');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', loadLeads);
|
||
</script>
|
||
{% endblock %}
|