Files
social-media-automation/dashboard/templates/leads.html
Consultoría AS ecc2ca73ea feat: Add Analytics, Odoo Integration, A/B Testing, and Content features
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>
2026-01-28 03:10:42 +00:00

558 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>