Files
social-media-automation/dashboard/templates/analytics.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

447 lines
19 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>Analytics - Social Media Automation</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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; }
.stat-up { color: #10b981; }
.stat-down { color: #ef4444; }
</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> - Analytics
</h1>
<nav class="flex gap-4">
<a href="/dashboard" class="px-4 py-2 rounded hover:bg-gray-800">Home</a>
<a href="/dashboard/compose" class="px-4 py-2 rounded hover:bg-gray-800 accent">+ Crear Post</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/analytics" class="px-4 py-2 rounded bg-gray-800">Analytics</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</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">
<!-- Period Selector -->
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">Dashboard de Analytics</h2>
<div class="flex gap-2">
<select id="periodSelect" onchange="loadDashboard()"
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
<option value="7">Últimos 7 días</option>
<option value="30" selected>Últimos 30 días</option>
<option value="90">Últimos 90 días</option>
</select>
<select id="platformSelect" onchange="loadDashboard()"
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>
<button onclick="generateReport()" class="btn-primary px-4 py-2 rounded">
Generar Reporte
</button>
</div>
</div>
<!-- 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="totalPosts">-</div>
<div class="text-gray-400 text-sm">Posts Publicados</div>
</div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-blue-400" id="totalImpressions">-</div>
<div class="text-gray-400 text-sm">Impresiones</div>
</div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-green-400" id="totalEngagements">-</div>
<div class="text-gray-400 text-sm">Interacciones</div>
</div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-purple-400" id="engagementRate">-</div>
<div class="text-gray-400 text-sm">Engagement Rate</div>
</div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-yellow-400" id="pendingInteractions">-</div>
<div class="text-gray-400 text-sm">Por Responder</div>
</div>
</div>
<!-- Engagement Breakdown -->
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="card p-4 flex items-center justify-between">
<div>
<div class="text-gray-400 text-sm">Likes</div>
<div class="text-2xl font-bold" id="totalLikes">-</div>
</div>
<div class="text-4xl">❤️</div>
</div>
<div class="card p-4 flex items-center justify-between">
<div>
<div class="text-gray-400 text-sm">Comentarios</div>
<div class="text-2xl font-bold" id="totalComments">-</div>
</div>
<div class="text-4xl">💬</div>
</div>
<div class="card p-4 flex items-center justify-between">
<div>
<div class="text-gray-400 text-sm">Compartidos</div>
<div class="text-2xl font-bold" id="totalShares">-</div>
</div>
<div class="text-4xl">🔄</div>
</div>
</div>
<div class="grid grid-cols-2 gap-6 mb-8">
<!-- Engagement Trend Chart -->
<div class="card p-6">
<h3 class="text-lg font-semibold mb-4">Tendencia de Engagement</h3>
<canvas id="engagementChart" height="200"></canvas>
</div>
<!-- Platform Breakdown -->
<div class="card p-6">
<h3 class="text-lg font-semibold mb-4">Por Plataforma</h3>
<div id="platformBreakdown" class="space-y-3">
<!-- Populated by JS -->
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-6 mb-8">
<!-- Top Posts -->
<div class="card p-6">
<h3 class="text-lg font-semibold mb-4">Top Posts por Engagement</h3>
<div id="topPosts" class="space-y-3">
<!-- Populated by JS -->
</div>
</div>
<!-- Optimal Times Heatmap -->
<div class="card p-6">
<h3 class="text-lg font-semibold mb-4">Mejores Horarios</h3>
<div id="optimalTimes" class="space-y-2">
<!-- Populated by JS -->
</div>
</div>
</div>
<!-- Content Type Performance -->
<div class="card p-6 mb-8">
<h3 class="text-lg font-semibold mb-4">Rendimiento por Tipo de Contenido</h3>
<div id="contentBreakdown" class="grid grid-cols-4 gap-4">
<!-- Populated by JS -->
</div>
</div>
<!-- Reports History -->
<div class="card p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Reportes Anteriores</h3>
<button onclick="sendReportTelegram()" class="btn-secondary px-4 py-2 rounded text-sm">
Enviar a Telegram
</button>
</div>
<div id="reportsList" class="space-y-2">
<!-- Populated by JS -->
</div>
</div>
</main>
<script>
let engagementChart = null;
async function loadDashboard() {
const days = document.getElementById('periodSelect').value;
const platform = document.getElementById('platformSelect').value;
try {
// Load dashboard stats
const params = new URLSearchParams({ days });
if (platform) params.append('platform', platform);
const statsRes = await fetch(`/api/analytics/dashboard?${params}`);
const stats = await statsRes.json();
// Update stat cards
document.getElementById('totalPosts').textContent = stats.total_posts;
document.getElementById('totalImpressions').textContent = formatNumber(stats.total_impressions);
document.getElementById('totalEngagements').textContent = formatNumber(stats.total_engagements);
document.getElementById('engagementRate').textContent = stats.avg_engagement_rate + '%';
document.getElementById('pendingInteractions').textContent = stats.pending_interactions;
document.getElementById('totalLikes').textContent = formatNumber(stats.total_likes);
document.getElementById('totalComments').textContent = formatNumber(stats.total_comments);
document.getElementById('totalShares').textContent = formatNumber(stats.total_shares);
// Platform breakdown
renderPlatformBreakdown(stats.platform_breakdown);
// Content breakdown
renderContentBreakdown(stats.content_breakdown);
// Load engagement trend
const trendRes = await fetch(`/api/analytics/engagement-trend?${params}`);
const trendData = await trendRes.json();
renderEngagementChart(trendData.trend);
// Load top posts
const topRes = await fetch(`/api/analytics/top-posts?${params}&limit=5`);
const topData = await topRes.json();
renderTopPosts(topData.posts);
// Load optimal times
const timesRes = await fetch(`/api/analytics/optimal-times?days=${days}${platform ? '&platform=' + platform : ''}`);
const timesData = await timesRes.json();
renderOptimalTimes(timesData.optimal_times);
// Load reports
const reportsRes = await fetch('/api/analytics/reports?limit=5');
const reportsData = await reportsRes.json();
renderReports(reportsData.reports);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function renderPlatformBreakdown(breakdown) {
const container = document.getElementById('platformBreakdown');
container.innerHTML = '';
const platformIcons = {
'x': '𝕏',
'threads': '🧵',
'instagram': '📷',
'facebook': '📘'
};
for (const [platform, data] of Object.entries(breakdown)) {
container.innerHTML += `
<div class="flex items-center justify-between bg-gray-800 rounded p-3">
<div class="flex items-center gap-3">
<span class="text-2xl">${platformIcons[platform] || '📱'}</span>
<div>
<div class="font-medium capitalize">${platform}</div>
<div class="text-sm text-gray-400">${data.posts} posts</div>
</div>
</div>
<div class="text-right">
<div class="text-green-400 font-bold">${formatNumber(data.engagements)}</div>
<div class="text-sm text-gray-400">interacciones</div>
</div>
</div>
`;
}
if (Object.keys(breakdown).length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay datos disponibles</p>';
}
}
function renderContentBreakdown(breakdown) {
const container = document.getElementById('contentBreakdown');
container.innerHTML = '';
for (const [type, data] of Object.entries(breakdown)) {
container.innerHTML += `
<div class="bg-gray-800 rounded p-4 text-center">
<div class="text-lg font-bold accent">${data.posts}</div>
<div class="text-sm text-gray-400 capitalize">${type.replace('_', ' ')}</div>
<div class="text-xs text-green-400 mt-1">${formatNumber(data.engagements)} eng.</div>
</div>
`;
}
if (Object.keys(breakdown).length === 0) {
container.innerHTML = '<p class="text-gray-500 col-span-4">No hay datos disponibles</p>';
}
}
function renderEngagementChart(trend) {
const ctx = document.getElementById('engagementChart').getContext('2d');
if (engagementChart) {
engagementChart.destroy();
}
engagementChart = new Chart(ctx, {
type: 'line',
data: {
labels: trend.map(d => d.date),
datasets: [
{
label: 'Impresiones',
data: trend.map(d => d.impressions),
borderColor: '#60a5fa',
backgroundColor: 'rgba(96, 165, 250, 0.1)',
tension: 0.3,
fill: true
},
{
label: 'Interacciones',
data: trend.map(d => d.engagements),
borderColor: '#34d399',
backgroundColor: 'rgba(52, 211, 153, 0.1)',
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
plugins: {
legend: {
labels: { color: '#9ca3af' }
}
},
scales: {
x: {
ticks: { color: '#9ca3af' },
grid: { color: '#374151' }
},
y: {
ticks: { color: '#9ca3af' },
grid: { color: '#374151' }
}
}
}
});
}
function renderTopPosts(posts) {
const container = document.getElementById('topPosts');
container.innerHTML = '';
posts.forEach((post, i) => {
container.innerHTML += `
<div class="bg-gray-800 rounded p-3">
<div class="flex justify-between items-start mb-2">
<span class="text-accent font-bold">#${i + 1}</span>
<span class="text-green-400 text-sm">${post.engagement_rate}% eng.</span>
</div>
<p class="text-sm text-gray-300 mb-2">${post.content}</p>
<div class="flex gap-4 text-xs text-gray-500">
<span>❤️ ${post.likes}</span>
<span>💬 ${post.comments}</span>
<span>🔄 ${post.shares}</span>
<span class="ml-auto">${post.platforms.join(', ')}</span>
</div>
</div>
`;
});
if (posts.length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay posts con métricas</p>';
}
}
function renderOptimalTimes(times) {
const container = document.getElementById('optimalTimes');
container.innerHTML = '';
// Show top 10 times
times.slice(0, 10).forEach(time => {
const barWidth = Math.min(time.avg_engagement_rate * 10, 100);
container.innerHTML += `
<div class="flex items-center gap-3">
<span class="w-20 text-sm text-gray-400">${time.day_name} ${time.hour_formatted}</span>
<div class="flex-1 bg-gray-800 rounded h-4 overflow-hidden">
<div class="bg-accent h-full" style="width: ${barWidth}%"></div>
</div>
<span class="text-sm text-green-400 w-16 text-right">${time.avg_engagement_rate}%</span>
</div>
`;
});
if (times.length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay suficientes datos</p>';
}
}
function renderReports(reports) {
const container = document.getElementById('reportsList');
container.innerHTML = '';
reports.forEach(report => {
container.innerHTML += `
<div class="bg-gray-800 rounded p-3 flex justify-between items-center">
<div>
<span class="font-medium capitalize">${report.report_type}</span>
<span class="text-gray-500 text-sm ml-2">
${report.period_start} - ${report.period_end}
</span>
</div>
<div class="flex gap-4 text-sm">
<span>${report.total_posts} posts</span>
<span class="text-green-400">${report.avg_engagement_rate}% eng.</span>
</div>
</div>
`;
});
if (reports.length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay reportes generados</p>';
}
}
async function generateReport() {
try {
const res = await fetch('/api/analytics/reports/generate?report_type=weekly', {
method: 'POST'
});
const data = await res.json();
if (res.ok) {
alert('Reporte generado exitosamente');
loadDashboard();
} else {
alert('Error: ' + data.detail);
}
} catch (error) {
alert('Error generando reporte');
}
}
async function sendReportTelegram() {
try {
const res = await fetch('/api/analytics/reports/send-telegram', {
method: 'POST'
});
const data = await res.json();
alert(data.message);
} catch (error) {
alert('Error enviando reporte');
}
}
// Load on page load
document.addEventListener('DOMContentLoaded', loadDashboard);
</script>
</body>
</html>