Files
social-media-automation/dashboard/templates/leads.html
Consultoría AS e32885afc5 fix: Fix YAML syntax errors and validator prompt formatting
- 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>
2026-01-28 21:13:58 +00:00

531 lines
24 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.
{% 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 %}