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>
This commit is contained in:
2026-01-28 21:13:58 +00:00
parent 11b0ba46fa
commit e32885afc5
19 changed files with 3440 additions and 4186 deletions

View File

@@ -27,7 +27,18 @@
"Bash(tee:*)", "Bash(tee:*)",
"Bash(systemctl start:*)", "Bash(systemctl start:*)",
"Bash(systemctl enable:*)", "Bash(systemctl enable:*)",
"Bash(systemctl is-enabled:*)" "Bash(systemctl is-enabled:*)",
"Bash(for f in dashboard/templates/*.html)",
"Bash(do)",
"Bash(if grep -q \"extends \"\"base.html\"\"\" \"$f\")",
"Bash(then)",
"Bash(echo:*)",
"Bash(else)",
"Bash(fi)",
"Bash(done)",
"Bash(xargs basename:*)",
"Bash(docker-compose down:*)",
"Bash(for:*)"
] ]
} }
} }

View File

@@ -106,7 +106,7 @@ topics_to_cover:
- Automatización de tareas repetitivas - Automatización de tareas repetitivas
avoid: avoid:
- "Levántate a las 5am" sin contexto - '"Levántate a las 5am" sin contexto'
- Hustle culture tóxica - Hustle culture tóxica
- Productividad como fin en sí misma - Productividad como fin en sí misma
- Consejos que solo funcionan para privilegiados - Consejos que solo funcionan para privilegiados

View File

@@ -68,7 +68,7 @@ avoid:
- Más de 2 hashtags - Más de 2 hashtags
- Emojis excesivos - Emojis excesivos
- Links en el medio del texto - Links en el medio del texto
- "Hilo:" sin contenido de hilo real - '"Hilo:" sin contenido de hilo real'
adaptation_rules: | adaptation_rules: |
Cuando adaptes contenido para X: Cuando adaptes contenido para X:

View File

@@ -78,5 +78,5 @@ tone_guidelines:
- "¿Te interesa? Escríbenos para más detalles" - "¿Te interesa? Escríbenos para más detalles"
dont: dont:
- "¡¡¡OFERTA INCREÍBLE!!!" - "¡¡¡OFERTA INCREÍBLE!!!"
- "ÚLTIMAS UNIDADES" (a menos que sea verdad) - '"ÚLTIMAS UNIDADES" (a menos que sea verdad)'
- "Compra ahora antes de que se acabe" - "Compra ahora antes de que se acabe"

View File

@@ -9,16 +9,23 @@ Este módulo contiene los componentes del motor de generación de contenido:
- ContentValidator: Validación y scoring con IA - ContentValidator: Validación y scoring con IA
""" """
from app.services.ai.prompt_library import PromptLibrary from app.services.ai.prompt_library import PromptLibrary, prompt_library
from app.services.ai.context_engine import ContextEngine from app.services.ai.context_engine import ContextEngine, context_engine
from app.services.ai.generator import ContentGeneratorV2 from app.services.ai.generator import ContentGeneratorV2, content_generator_v2
from app.services.ai.platform_adapter import PlatformAdapter from app.services.ai.platform_adapter import PlatformAdapter, platform_adapter
from app.services.ai.validator import ContentValidator from app.services.ai.validator import ContentValidator, content_validator
__all__ = [ __all__ = [
# Classes
"PromptLibrary", "PromptLibrary",
"ContextEngine", "ContextEngine",
"ContentGeneratorV2", "ContentGeneratorV2",
"PlatformAdapter", "PlatformAdapter",
"ContentValidator", "ContentValidator",
# Singleton instances
"prompt_library",
"context_engine",
"content_generator_v2",
"platform_adapter",
"content_validator",
] ]

View File

@@ -261,11 +261,8 @@ class ContentValidator:
if not scoring_prompt: if not scoring_prompt:
scoring_prompt = self._default_scoring_prompt() scoring_prompt = self._default_scoring_prompt()
# Renderizar prompt # Renderizar prompt usando replace para evitar problemas con JSON
prompt = scoring_prompt.format( prompt = scoring_prompt.replace("{content}", content).replace("{platform}", platform)
content=content,
platform=platform
)
# Llamar a DeepSeek # Llamar a DeepSeek
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
@@ -340,7 +337,7 @@ CRITERIOS (suma = 100):
- CTA (0-10): ¿CTA claro si aplica? - CTA (0-10): ¿CTA claro si aplica?
RESPONDE EN JSON: RESPONDE EN JSON:
{{"total": N, "breakdown": {{"hook_strength": N, "clarity": N, "actionability": N, "originality": N, "brand_voice": N, "cta_effectiveness": N}}, "feedback": "sugerencia"}}""" {"total": N, "breakdown": {"hook_strength": N, "clarity": N, "actionability": N, "originality": N, "brand_voice": N, "cta_effectiveness": N}, "feedback": "sugerencia"}"""
def _extract_score_from_text(self, text: str) -> Dict: def _extract_score_from_text(self, text: str) -> Dict:
"""Extraer score de texto si falla JSON parsing.""" """Extraer score de texto si falla JSON parsing."""

View File

@@ -1,222 +1,133 @@
<!DOCTYPE html> {% extends "base.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"> {% block title %}Analytics{% endblock %}
<!-- Period Selector -->
<div class="flex justify-between items-center mb-6"> {% block extra_head %}
<h2 class="text-xl font-semibold">Dashboard de Analytics</h2> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div class="flex gap-2"> {% endblock %}
<select id="periodSelect" onchange="loadDashboard()"
class="bg-gray-800 border border-gray-700 rounded px-4 py-2"> {% block content %}
<div class="animate-fade-in">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Analytics</h1>
<p class="text-gray-400 mt-1">Métricas y rendimiento de tus publicaciones</p>
</div>
<div class="flex gap-3">
<select id="periodSelect" onchange="loadDashboard()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
<option value="7">Últimos 7 días</option> <option value="7">Últimos 7 días</option>
<option value="30" selected>Últimos 30 días</option> <option value="30" selected>Últimos 30 días</option>
<option value="90">Últimos 90 días</option> <option value="90">Últimos 90 días</option>
</select> </select>
<select id="platformSelect" onchange="loadDashboard()" <select id="platformSelect" onchange="loadDashboard()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
<option value="">Todas las plataformas</option> <option value="">Todas las plataformas</option>
<option value="x">X (Twitter)</option> <option value="x">X (Twitter)</option>
<option value="threads">Threads</option>
<option value="instagram">Instagram</option>
<option value="facebook">Facebook</option> <option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="threads">Threads</option>
</select> </select>
<button onclick="generateReport()" class="btn-primary px-4 py-2 rounded">
Generar Reporte
</button>
</div> </div>
</div> </div>
<!-- Stats Cards --> <!-- Stats Grid -->
<div class="grid grid-cols-5 gap-4 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold accent" id="totalPosts">-</div> <div class="flex items-center justify-between mb-4">
<div class="text-gray-400 text-sm">Posts Publicados</div> <div class="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
<span class="text-2xl">📝</span>
</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>
<div class="card p-4 text-center"> <p class="text-3xl font-bold" id="stat-posts">-</p>
<div class="text-3xl font-bold text-green-400" id="totalEngagements">-</div> <p class="text-gray-400 text-sm mt-1">Posts Totales</p>
<div class="text-gray-400 text-sm">Interacciones</div>
</div> </div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-purple-400" id="engagementRate">-</div> <div class="stat-card card rounded-2xl p-6">
<div class="text-gray-400 text-sm">Engagement Rate</div> <div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
<span class="text-2xl">👁️</span>
</div> </div>
<div class="card p-4 text-center"> </div>
<div class="text-3xl font-bold text-yellow-400" id="pendingInteractions">-</div> <p class="text-3xl font-bold" id="stat-impressions">-</p>
<div class="text-gray-400 text-sm">Por Responder</div> <p class="text-gray-400 text-sm mt-1">Impresiones</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
<span class="text-2xl">❤️</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-engagements">-</p>
<p class="text-gray-400 text-sm mt-1">Interacciones</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-yellow-500/20 flex items-center justify-center">
<span class="text-2xl">📊</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-rate">-</p>
<p class="text-gray-400 text-sm mt-1">Engagement Rate</p>
</div> </div>
</div> </div>
<!-- Engagement Breakdown --> <!-- Charts -->
<div class="grid grid-cols-3 gap-4 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="card p-4 flex items-center justify-between"> <div class="card rounded-2xl p-6">
<div> <h3 class="font-semibold mb-4">Engagement por Plataforma</h3>
<div class="text-gray-400 text-sm">Likes</div> <canvas id="platformChart" height="200"></canvas>
<div class="text-2xl font-bold" id="totalLikes">-</div>
</div> </div>
<div class="text-4xl">❤️</div> <div class="card rounded-2xl p-6">
</div> <h3 class="font-semibold mb-4">Rendimiento por Tipo de Contenido</h3>
<div class="card p-4 flex items-center justify-between"> <canvas id="contentChart" height="200"></canvas>
<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> </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 --> <!-- Top Posts -->
<div class="card p-6"> <div class="card rounded-2xl p-6 mb-8">
<h3 class="text-lg font-semibold mb-4">Top Posts por Engagement</h3> <h3 class="font-semibold mb-4">Mejores Posts</h3>
<div id="topPosts" class="space-y-3"> <div id="top-posts" class="space-y-4">
<!-- Populated by JS --> <div class="text-center py-8 text-gray-500">Cargando...</div>
</div> </div>
</div> </div>
<!-- Optimal Times Heatmap --> <!-- Optimal Times -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<h3 class="text-lg font-semibold mb-4">Mejores Horarios</h3> <h3 class="font-semibold mb-4">Mejores Horarios para Publicar</h3>
<div id="optimalTimes" class="space-y-2"> <div id="optimal-times" class="grid grid-cols-7 gap-2">
<!-- Populated by JS --> <div class="text-center py-8 text-gray-500 col-span-7">Cargando...</div>
</div>
</div> </div>
</div> </div>
</div>
{% endblock %}
<!-- Content Type Performance --> {% block extra_scripts %}
<div class="card p-6 mb-8"> <script>
<h3 class="text-lg font-semibold mb-4">Rendimiento por Tipo de Contenido</h3> let platformChart, contentChart;
<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() { async function loadDashboard() {
const days = document.getElementById('periodSelect').value; const period = document.getElementById('periodSelect').value;
const platform = document.getElementById('platformSelect').value; const platform = document.getElementById('platformSelect').value;
try { const params = new URLSearchParams({ days: period });
// Load dashboard stats
const params = new URLSearchParams({ days });
if (platform) params.append('platform', platform); if (platform) params.append('platform', platform);
try {
const statsRes = await fetch(`/api/analytics/dashboard?${params}`); const statsRes = await fetch(`/api/analytics/dashboard?${params}`);
const stats = await statsRes.json(); const stats = await statsRes.json();
// Update stat cards document.getElementById('stat-posts').textContent = stats.total_posts || 0;
document.getElementById('totalPosts').textContent = stats.total_posts; document.getElementById('stat-impressions').textContent = formatNumber(stats.total_impressions || 0);
document.getElementById('totalImpressions').textContent = formatNumber(stats.total_impressions); document.getElementById('stat-engagements').textContent = formatNumber(stats.total_engagements || 0);
document.getElementById('totalEngagements').textContent = formatNumber(stats.total_engagements); document.getElementById('stat-rate').textContent = (stats.avg_engagement_rate || 0).toFixed(2) + '%';
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 updatePlatformChart(stats.platform_breakdown || {});
renderPlatformBreakdown(stats.platform_breakdown); updateContentChart(stats.content_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);
loadTopPosts(period, platform);
loadOptimalTimes(platform);
} catch (error) { } catch (error) {
console.error('Error loading dashboard:', error); console.error('Error loading dashboard:', error);
} }
@@ -228,219 +139,118 @@
return num.toString(); return num.toString();
} }
function renderPlatformBreakdown(breakdown) { function updatePlatformChart(data) {
const container = document.getElementById('platformBreakdown'); const ctx = document.getElementById('platformChart').getContext('2d');
container.innerHTML = ''; const labels = Object.keys(data).length ? Object.keys(data) : ['Sin datos'];
const values = Object.keys(data).length ? Object.values(data) : [0];
const platformIcons = { if (platformChart) platformChart.destroy();
'x': '𝕏',
'threads': '🧵',
'instagram': '📷',
'facebook': '📘'
};
for (const [platform, data] of Object.entries(breakdown)) { platformChart = new Chart(ctx, {
container.innerHTML += ` type: 'doughnut',
<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: { data: {
labels: trend.map(d => d.date), labels: labels,
datasets: [ datasets: [{
{ data: values,
label: 'Impresiones', backgroundColor: ['#6366f1', '#8b5cf6', '#ec4899', '#f59e0b'],
data: trend.map(d => d.impressions), borderWidth: 0
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: { options: {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: { position: 'bottom', labels: { color: '#9ca3af' } }
labels: { color: '#9ca3af' }
} }
}
});
}
function updateContentChart(data) {
const ctx = document.getElementById('contentChart').getContext('2d');
const labels = Object.keys(data).length ? Object.keys(data) : ['Sin datos'];
const values = Object.keys(data).length ? Object.values(data) : [0];
if (contentChart) contentChart.destroy();
contentChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Posts',
data: values,
backgroundColor: '#6366f1',
borderRadius: 8
}]
}, },
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { scales: {
x: { y: { grid: { color: '#334155' }, ticks: { color: '#9ca3af' } },
ticks: { color: '#9ca3af' }, x: { grid: { display: false }, ticks: { color: '#9ca3af' } }
grid: { color: '#374151' }
},
y: {
ticks: { color: '#9ca3af' },
grid: { color: '#374151' }
}
} }
} }
}); });
} }
function renderTopPosts(posts) { async function loadTopPosts(period, platform) {
const container = document.getElementById('topPosts'); const container = document.getElementById('top-posts');
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 { try {
const res = await fetch('/api/analytics/reports/generate?report_type=weekly', { const params = new URLSearchParams({ days: period, limit: 5 });
method: 'POST' if (platform) params.append('platform', platform);
});
const data = await res.json();
if (res.ok) { const response = await fetch(`/api/analytics/top-posts?${params}`);
alert('Reporte generado exitosamente'); const data = await response.json();
loadDashboard();
if (data.posts && data.posts.length) {
container.innerHTML = data.posts.map(post => `
<div class="bg-dark-800/50 rounded-xl p-4">
<p class="text-sm text-gray-300 mb-2">${post.content?.substring(0, 100) || 'Sin contenido'}...</p>
<div class="flex items-center justify-between text-xs text-gray-500">
<span>${post.platforms?.join(', ') || '-'}</span>
<span>Engagement: ${(post.engagement_rate || 0).toFixed(2)}%</span>
</div>
</div>
`).join('');
} else { } else {
alert('Error: ' + data.detail); container.innerHTML = '<div class="text-center py-8 text-gray-500">No hay datos suficientes</div>';
} }
} catch (error) { } catch (error) {
alert('Error generando reporte'); container.innerHTML = '<div class="text-center py-8 text-red-400">Error cargando datos</div>';
} }
} }
async function sendReportTelegram() { async function loadOptimalTimes(platform) {
const container = document.getElementById('optimal-times');
try { try {
const res = await fetch('/api/analytics/reports/send-telegram', { const params = platform ? `?platform=${platform}` : '';
method: 'POST' const response = await fetch(`/api/analytics/optimal-times${params}`);
}); const data = await response.json();
const data = await res.json();
alert(data.message); const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
if (data.optimal_times && Object.keys(data.optimal_times).length) {
container.innerHTML = days.map((day, i) => {
const times = data.optimal_times[i] || [];
return `
<div class="bg-dark-800/50 rounded-xl p-3 text-center">
<p class="font-medium text-sm mb-2">${day}</p>
<div class="space-y-1">
${times.length ? times.slice(0, 3).map(t => `<span class="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full block">${t}:00</span>`).join('') : '<span class="text-xs text-gray-500">-</span>'}
</div>
</div>
`;
}).join('');
} else {
container.innerHTML = '<div class="text-center py-8 text-gray-500 col-span-7">No hay datos suficientes para calcular horarios óptimos</div>';
}
} catch (error) { } catch (error) {
alert('Error enviando reporte'); container.innerHTML = '<div class="text-center py-8 text-red-400 col-span-7">Error cargando datos</div>';
} }
} }
// Load on page load // Load on page load
document.addEventListener('DOMContentLoaded', loadDashboard); loadDashboard();
</script> </script>
</body> {% endblock %}
</html>

View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Dashboard{% endblock %} - Social Media Automation</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#d4a574',
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617'
}
},
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.gradient-bg { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); }
.card { background: rgba(30, 41, 59, 0.8); backdrop-filter: blur(10px); border: 1px solid rgba(99, 102, 241, 0.1); }
.card:hover { border-color: rgba(99, 102, 241, 0.3); }
.btn-primary { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
.btn-primary:hover { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); transform: translateY(-1px); }
.btn-secondary { background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); }
.btn-secondary:hover { background: rgba(99, 102, 241, 0.2); }
.sidebar-link { transition: all 0.2s ease; }
.sidebar-link:hover { background: rgba(99, 102, 241, 0.1); transform: translateX(4px); }
.sidebar-link.active { background: rgba(99, 102, 241, 0.2); border-left: 3px solid #6366f1; }
.stat-card { transition: all 0.3s ease; }
.stat-card:hover { transform: translateY(-4px); box-shadow: 0 10px 40px rgba(99, 102, 241, 0.2); }
.glow { box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #1e293b; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #6366f1; }
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="gradient-bg min-h-screen text-gray-100">
<div class="flex">
<!-- Sidebar -->
<aside class="w-64 min-h-screen bg-dark-900/50 backdrop-blur-xl border-r border-dark-700/50 fixed">
<div class="p-6">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
<span class="text-xl">📱</span>
</div>
<div>
<h1 class="font-bold text-lg">Social Media</h1>
<p class="text-xs text-gray-500">Automation</p>
</div>
</div>
<nav class="space-y-1">
<a href="/" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/' %}active{% endif %}">
<span class="text-xl">🏠</span>
<span>Dashboard</span>
</a>
<a href="/compose" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/compose' %}active{% endif %}">
<span class="text-xl">✍️</span>
<span>Crear Post</span>
</a>
<a href="/posts" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/posts' %}active{% endif %}">
<span class="text-xl">📝</span>
<span>Posts</span>
</a>
<a href="/calendar" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/calendar' %}active{% endif %}">
<span class="text-xl">📅</span>
<span>Calendario</span>
</a>
<a href="/interactions" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/interactions' %}active{% endif %}">
<span class="text-xl">💬</span>
<span>Interacciones</span>
</a>
<div class="pt-4 mt-4 border-t border-dark-700/50">
<p class="px-4 text-xs text-gray-500 uppercase tracking-wider mb-2">Catálogo</p>
</div>
<a href="/products" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/products' %}active{% endif %}">
<span class="text-xl">📦</span>
<span>Productos</span>
</a>
<a href="/services" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/services' %}active{% endif %}">
<span class="text-xl">🛠️</span>
<span>Servicios</span>
</a>
<div class="pt-4 mt-4 border-t border-dark-700/50">
<p class="px-4 text-xs text-gray-500 uppercase tracking-wider mb-2">Analytics</p>
</div>
<a href="/analytics" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/analytics' %}active{% endif %}">
<span class="text-xl">📊</span>
<span>Analytics</span>
</a>
<a href="/leads" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/leads' %}active{% endif %}">
<span class="text-xl">🎯</span>
<span>Leads</span>
</a>
<div class="pt-4 mt-4 border-t border-dark-700/50">
<p class="px-4 text-xs text-gray-500 uppercase tracking-wider mb-2">Sistema</p>
</div>
<a href="/settings" class="sidebar-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white {% if request.url.path == '/settings' %}active{% endif %}">
<span class="text-xl">⚙️</span>
<span>Configuración</span>
</a>
</nav>
</div>
<!-- User Section -->
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-dark-700/50 bg-dark-900/50">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-sm">
{{ user.username[0]|upper if user else '?' }}
</div>
<div class="text-sm">
<p class="font-medium">{{ user.username if user else 'Usuario' }}</p>
<p class="text-xs text-gray-500">Admin</p>
</div>
</div>
<a href="/logout" class="text-gray-400 hover:text-red-400 transition-colors" title="Cerrar sesión">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
</svg>
</a>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64 p-8">
{% block content %}{% endblock %}
</main>
</div>
<!-- Modal -->
<div id="modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="card rounded-2xl p-6 max-w-md w-full mx-4 animate-fade-in">
<div id="modal-content"></div>
<button onclick="closeModal()" class="mt-4 w-full btn-secondary px-4 py-2 rounded-lg">Cerrar</button>
</div>
</div>
<script>
function showModal(content, loading = false) {
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modal-content');
modalContent.innerHTML = loading
? `<div class="flex items-center gap-3"><div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div><span>${content}</span></div>`
: content;
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeModal() {
const modal = document.getElementById('modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
// Close modal on outside click
document.getElementById('modal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View File

@@ -1,114 +1,78 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendario - 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; }
.btn-secondary:hover { background-color: #4b5563; }
.calendar-day { min-height: 120px; }
.calendar-day:hover { background-color: #1e3a5f; }
.calendar-day.today { border: 2px solid #d4a574; }
.calendar-day.other-month { opacity: 0.4; }
.post-dot { width: 8px; height: 8px; border-radius: 50%; }
.post-item { font-size: 11px; padding: 2px 4px; border-radius: 4px; margin-bottom: 2px; cursor: pointer; }
.post-item:hover { opacity: 0.8; }
.status-published { background-color: #065f46; }
.status-scheduled { background-color: #1e40af; }
.status-pending { background-color: #92400e; }
</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> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded bg-gray-800 accent">Calendario</a>
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</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"> {% block title %}Calendario{% endblock %}
<!-- Calendar Controls -->
<div class="flex justify-between items-center mb-6"> {% block content %}
<div class="flex items-center gap-4"> <div class="animate-fade-in">
<button onclick="prevMonth()" class="btn-secondary px-3 py-2 rounded"> <!-- Header -->
← Anterior <div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Calendario</h1>
<p class="text-gray-400 mt-1">Visualiza y programa tus publicaciones</p>
</div>
<div class="flex gap-3">
<button onclick="prevMonth()" class="btn-secondary px-4 py-2 rounded-xl flex items-center gap-2">
<span></span> Anterior
</button> </button>
<h2 class="text-2xl font-bold" id="current-month">Enero 2025</h2> <h2 class="text-xl font-bold px-4 py-2" id="current-month">Enero 2025</h2>
<button onclick="nextMonth()" class="btn-secondary px-3 py-2 rounded"> <button onclick="nextMonth()" class="btn-secondary px-4 py-2 rounded-xl flex items-center gap-2">
Siguiente Siguiente <span></span>
</button> </button>
</div> </div>
</div>
<!-- View Controls -->
<div class="card rounded-2xl p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center justify-between">
<div class="flex gap-2"> <div class="flex gap-2">
<button onclick="setView('month')" id="btn-month" class="btn-primary px-4 py-2 rounded"> <button onclick="setView('month')" id="btn-month" class="btn-primary px-4 py-2 rounded-xl">
Mes Mes
</button> </button>
<button onclick="setView('week')" id="btn-week" class="btn-secondary px-4 py-2 rounded"> <button onclick="setView('week')" id="btn-week" class="btn-secondary px-4 py-2 rounded-xl">
Semana Semana
</button> </button>
<button onclick="goToToday()" class="btn-secondary px-4 py-2 rounded"> <button onclick="goToToday()" class="btn-secondary px-4 py-2 rounded-xl">
Hoy Hoy
</button> </button>
</div> </div>
</div>
<!-- Platform Filters -->
<div class="card p-4 mb-6">
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<span class="text-gray-400">Filtrar:</span> <span class="text-gray-400 text-sm">Filtrar:</span>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="x" onchange="filterCalendar()"> <input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="x" onchange="filterCalendar()">
<span class="bg-gray-800 px-2 py-1 rounded text-sm">X</span> <span class="text-sm bg-dark-700 px-2 py-1 rounded-lg">X</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="threads" onchange="filterCalendar()"> <input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="facebook" onchange="filterCalendar()">
<span class="bg-gray-800 px-2 py-1 rounded text-sm">Threads</span> <span class="text-sm bg-blue-600/30 text-blue-400 px-2 py-1 rounded-lg">Facebook</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="instagram" onchange="filterCalendar()"> <input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="instagram" onchange="filterCalendar()">
<span class="bg-pink-900 px-2 py-1 rounded text-sm">Instagram</span> <span class="text-sm bg-pink-600/30 text-pink-400 px-2 py-1 rounded-lg">Instagram</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="facebook" onchange="filterCalendar()"> <input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="threads" onchange="filterCalendar()">
<span class="bg-blue-900 px-2 py-1 rounded text-sm">Facebook</span> <span class="text-sm bg-dark-700 px-2 py-1 rounded-lg">Threads</span>
</label> </label>
<div class="flex-1"></div> </div>
<div class="flex gap-2 text-xs"> <div class="flex gap-3 text-xs">
<span class="flex items-center gap-1"><span class="post-dot status-scheduled"></span> Programado</span> <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500"></span> Programado</span>
<span class="flex items-center gap-1"><span class="post-dot status-pending"></span> Pendiente</span> <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-yellow-500"></span> Pendiente</span>
<span class="flex items-center gap-1"><span class="post-dot status-published"></span> Publicado</span> <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-green-500"></span> Publicado</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Calendar Grid --> <!-- Calendar Grid -->
<div class="card overflow-hidden"> <div id="month-view" class="card rounded-2xl overflow-hidden">
<!-- Day Headers --> <!-- Day Headers -->
<div class="grid grid-cols-7 bg-gray-800"> <div class="grid grid-cols-7 bg-dark-800">
<div class="p-3 text-center font-semibold border-r border-gray-700">Lun</div> <div class="p-3 text-center font-semibold border-r border-dark-600">Lun</div>
<div class="p-3 text-center font-semibold border-r border-gray-700">Mar</div> <div class="p-3 text-center font-semibold border-r border-dark-600">Mar</div>
<div class="p-3 text-center font-semibold border-r border-gray-700">Mié</div> <div class="p-3 text-center font-semibold border-r border-dark-600">Mié</div>
<div class="p-3 text-center font-semibold border-r border-gray-700">Jue</div> <div class="p-3 text-center font-semibold border-r border-dark-600">Jue</div>
<div class="p-3 text-center font-semibold border-r border-gray-700">Vie</div> <div class="p-3 text-center font-semibold border-r border-dark-600">Vie</div>
<div class="p-3 text-center font-semibold border-r border-gray-700 text-blue-400">Sáb</div> <div class="p-3 text-center font-semibold border-r border-dark-600 text-primary">Sáb</div>
<div class="p-3 text-center font-semibold text-blue-400">Dom</div> <div class="p-3 text-center font-semibold text-primary">Dom</div>
</div> </div>
<!-- Calendar Days --> <!-- Calendar Days -->
@@ -118,33 +82,44 @@
</div> </div>
<!-- Week View (hidden by default) --> <!-- Week View (hidden by default) -->
<div id="week-view" class="card hidden mt-6"> <div id="week-view" class="card rounded-2xl hidden">
<div id="week-grid" class="divide-y divide-gray-700"> <div id="week-grid" class="divide-y divide-dark-600">
<!-- Week days filled dynamically --> <!-- Week days filled dynamically -->
</div> </div>
</div> </div>
</main> </div>
<!-- Post Detail Modal --> <!-- Post Detail Modal -->
<div id="post-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"> <div id="post-modal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="card p-6 max-w-lg w-full mx-4"> <div class="card rounded-2xl p-6 max-w-lg w-full mx-4">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Detalle del Post</h3> <h3 class="font-semibold text-lg">Detalle del Post</h3>
<button onclick="closePostModal()" class="text-gray-400 hover:text-white"></button> <button onclick="closePostModal()" class="text-gray-400 hover:text-white text-xl"></button>
</div> </div>
<div id="post-modal-content"> <div id="post-modal-content">
<!-- Content loaded dynamically --> <!-- Content loaded dynamically -->
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script> {% block extra_scripts %}
<style>
.calendar-day { min-height: 120px; }
.calendar-day:hover { background-color: rgba(99, 102, 241, 0.1); }
.calendar-day.today { border: 2px solid #6366f1; }
.calendar-day.other-month { opacity: 0.4; }
.post-item { font-size: 11px; padding: 2px 6px; border-radius: 6px; margin-bottom: 2px; cursor: pointer; }
.post-item:hover { opacity: 0.8; }
.status-published { background-color: rgba(16, 185, 129, 0.3); color: #34d399; }
.status-scheduled { background-color: rgba(59, 130, 246, 0.3); color: #60a5fa; }
.status-pending_approval { background-color: rgba(245, 158, 11, 0.3); color: #fbbf24; }
</style>
<script>
let currentDate = new Date(); let currentDate = new Date();
let currentView = 'month'; let currentView = 'month';
let calendarData = {}; let calendarData = {};
let allPosts = [];
// Initialize
window.addEventListener('load', () => { window.addEventListener('load', () => {
loadCalendarData(); loadCalendarData();
}); });
@@ -153,17 +128,10 @@
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
const month = currentDate.getMonth(); const month = currentDate.getMonth();
// Get first and last day of visible calendar (including overflow days)
const firstDay = new Date(year, month, 1); const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Adjust for Monday start
const startDate = new Date(firstDay); const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1); startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1);
const endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (7 - endDate.getDay()) % 7);
try { try {
const response = await fetch( const response = await fetch(
`/api/calendar/posts/view?start_date=${formatDateISO(startDate)}&days=42` `/api/calendar/posts/view?start_date=${formatDateISO(startDate)}&days=42`
@@ -183,7 +151,6 @@
renderWeekView(); renderWeekView();
} }
// Update month title
const months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', const months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
document.getElementById('current-month').textContent = document.getElementById('current-month').textContent =
@@ -196,9 +163,6 @@
const month = currentDate.getMonth(); const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1); const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Start from Monday
const startDate = new Date(firstDay); const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1); startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1);
@@ -221,9 +185,9 @@
); );
html += ` html += `
<div class="calendar-day p-2 border-r border-b border-gray-700 ${isToday ? 'today' : ''} ${isOtherMonth ? 'other-month' : ''}" <div class="calendar-day p-2 border-r border-b border-dark-600 ${isToday ? 'today' : ''} ${isOtherMonth ? 'other-month' : ''}"
ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)"> ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="font-semibold text-sm mb-1 ${isToday ? 'accent' : ''}">${date.getDate()}</div> <div class="font-semibold text-sm mb-1 ${isToday ? 'text-primary' : ''}">${date.getDate()}</div>
<div class="space-y-1"> <div class="space-y-1">
${dayPosts.slice(0, 4).map(post => ` ${dayPosts.slice(0, 4).map(post => `
<div class="post-item status-${post.status} truncate" <div class="post-item status-${post.status} truncate"
@@ -246,7 +210,7 @@
function renderWeekView() { function renderWeekView() {
const container = document.getElementById('week-grid'); const container = document.getElementById('week-grid');
const weekStart = new Date(currentDate); const weekStart = new Date(currentDate);
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1); // Monday weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
const platforms = getSelectedPlatforms(); const platforms = getSelectedPlatforms();
const days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo']; const days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
@@ -266,15 +230,15 @@
); );
html += ` html += `
<div class="p-4 ${isToday ? 'bg-gray-800' : ''}" ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)"> <div class="p-4 ${isToday ? 'bg-primary/10' : ''}" ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<div class="font-semibold ${isToday ? 'accent' : ''}">${days[i]}</div> <div class="font-semibold ${isToday ? 'text-primary' : ''}">${days[i]}</div>
<div class="text-gray-400">${date.getDate()}/${date.getMonth() + 1}</div> <div class="text-gray-400">${date.getDate()}/${date.getMonth() + 1}</div>
${isToday ? '<span class="bg-amber-600 text-xs px-2 py-1 rounded">Hoy</span>' : ''} ${isToday ? '<span class="bg-primary text-white text-xs px-2 py-1 rounded-full">Hoy</span>' : ''}
</div> </div>
<div class="grid grid-cols-4 gap-2"> <div class="grid grid-cols-4 gap-2">
${dayPosts.map(post => ` ${dayPosts.map(post => `
<div class="post-item status-${post.status} p-2" <div class="post-item status-${post.status} p-2 rounded-lg"
draggable="true" draggable="true"
ondragstart="dragPost(event, ${post.id})" ondragstart="dragPost(event, ${post.id})"
onclick="showPostDetail(${post.id})"> onclick="showPostDetail(${post.id})">
@@ -299,10 +263,10 @@
function setView(view) { function setView(view) {
currentView = view; currentView = view;
document.getElementById('btn-month').className = view === 'month' ? 'btn-primary px-4 py-2 rounded' : 'btn-secondary px-4 py-2 rounded'; document.getElementById('btn-month').className = view === 'month' ? 'btn-primary px-4 py-2 rounded-xl' : 'btn-secondary px-4 py-2 rounded-xl';
document.getElementById('btn-week').className = view === 'week' ? 'btn-primary px-4 py-2 rounded' : 'btn-secondary px-4 py-2 rounded'; document.getElementById('btn-week').className = view === 'week' ? 'btn-primary px-4 py-2 rounded-xl' : 'btn-secondary px-4 py-2 rounded-xl';
document.getElementById('calendar-grid').parentElement.classList.toggle('hidden', view !== 'month'); document.getElementById('month-view').classList.toggle('hidden', view !== 'month');
document.getElementById('week-view').classList.toggle('hidden', view !== 'week'); document.getElementById('week-view').classList.toggle('hidden', view !== 'week');
renderCalendar(); renderCalendar();
@@ -339,7 +303,6 @@
renderCalendar(); renderCalendar();
} }
// Drag and drop
let draggedPostId = null; let draggedPostId = null;
function dragPost(event, postId) { function dragPost(event, postId) {
@@ -370,9 +333,7 @@
draggedPostId = null; draggedPostId = null;
} }
// Post detail modal
function showPostDetail(postId) { function showPostDetail(postId) {
// Find post in calendar data
let post = null; let post = null;
for (const dayPosts of Object.values(calendarData)) { for (const dayPosts of Object.values(calendarData)) {
post = dayPosts.find(p => p.id === postId); post = dayPosts.find(p => p.id === postId);
@@ -384,17 +345,17 @@
const content = ` const content = `
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<span class="status-${post.status} px-2 py-1 rounded text-xs">${post.status}</span> <span class="status-${post.status} px-2 py-1 rounded-lg text-xs">${post.status}</span>
${post.platforms ? post.platforms.map(p => `<span class="bg-gray-700 px-2 py-1 rounded text-xs ml-1">${p}</span>`).join('') : ''} ${post.platforms ? post.platforms.map(p => `<span class="bg-dark-700 px-2 py-1 rounded-lg text-xs ml-1">${p}</span>`).join('') : ''}
</div> </div>
<p class="text-gray-300">${escapeHtml(post.content || '')}</p> <p class="text-gray-300">${escapeHtml(post.content || '')}</p>
<div class="text-gray-400 text-sm"> <div class="text-gray-400 text-sm">
${post.scheduled_at ? `Programado: ${new Date(post.scheduled_at).toLocaleString('es-MX')}` : ''} ${post.scheduled_at ? `Programado: ${new Date(post.scheduled_at).toLocaleString('es-MX')}` : ''}
</div> </div>
<div class="flex gap-2 pt-4 border-t border-gray-700"> <div class="flex gap-2 pt-4 border-t border-dark-600">
<a href="/dashboard/posts" class="btn-secondary px-4 py-2 rounded text-sm">Ver en Posts</a> <a href="/posts" class="btn-secondary px-4 py-2 rounded-xl text-sm">Ver en Posts</a>
${post.status === 'scheduled' ? ` ${post.status === 'scheduled' ? `
<button onclick="publishNowFromModal(${post.id})" class="btn-primary px-4 py-2 rounded text-sm">Publicar Ahora</button> <button onclick="publishNowFromModal(${post.id})" class="btn-primary px-4 py-2 rounded-xl text-sm">Publicar Ahora</button>
` : ''} ` : ''}
</div> </div>
</div> </div>
@@ -415,13 +376,12 @@
await fetch(`/api/calendar/posts/${postId}/publish-now`, { method: 'POST' }); await fetch(`/api/calendar/posts/${postId}/publish-now`, { method: 'POST' });
closePostModal(); closePostModal();
loadCalendarData(); loadCalendarData();
alert('Post enviado a publicación'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p>Post enviado a publicación</p></div>');
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + error.message + '</p></div>');
} }
} }
// Utilities
function formatDateISO(date) { function formatDateISO(date) {
return date.toISOString().split('T')[0]; return date.toISOString().split('T')[0];
} }
@@ -431,6 +391,5 @@
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@@ -1,626 +1,348 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head> {% block title %}Crear Post{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>Crear Post - Social Media Automation</title> <div class="animate-fade-in">
<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; }
textarea { background-color: #0f172a; border-color: #334155; }
textarea:focus { border-color: #d4a574; outline: none; }
.platform-btn { transition: all 0.2s; }
.platform-btn.selected { ring: 2px; ring-color: #d4a574; }
.char-warning { color: #fbbf24; }
.char-error { color: #ef4444; }
</style>
</head>
<body class="min-h-screen">
<!-- Header --> <!-- Header -->
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4"> <div class="mb-8">
<div class="flex justify-between items-center"> <h1 class="text-3xl font-bold">Crear Publicación</h1>
<h1 class="text-2xl font-bold"> <p class="text-gray-400 mt-1">Genera y programa contenido para redes sociales</p>
<span class="accent">Consultoría AS</span> - Social Media
</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 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/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> </div>
</header>
<main class="container mx-auto px-6 py-8 max-w-4xl"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<h2 class="text-2xl font-bold mb-6">Crear Nueva Publicación</h2> <!-- Main Form -->
<div class="lg:col-span-2 space-y-6">
<form id="compose-form" class="space-y-6">
<!-- Platform Selection --> <!-- Platform Selection -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Plataformas</h3> <h3 class="font-semibold mb-4 flex items-center gap-2">
<div class="flex gap-4"> <span>📱</span>
<button type="button" onclick="togglePlatform('x')" <span>Plataformas</span>
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2" </h3>
id="btn-x"> <div class="flex flex-wrap gap-3">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"> <button type="button" onclick="togglePlatform('x')" id="btn-x"
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/> class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
</svg> <span class="text-xl">𝕏</span>
<span>X</span> <span>Twitter/X</span>
</button> </button>
<button type="button" onclick="togglePlatform('threads')" <button type="button" onclick="togglePlatform('facebook')" id="btn-facebook"
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2" class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
id="btn-threads"> <span class="text-xl">📘</span>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.96-.065-1.17.408-2.168 1.332-2.81.88-.612 2.104-.9 3.64-.857 1.016.028 1.96.132 2.828.31-.055-.792-.283-1.392-.683-1.786-.468-.46-1.166-.682-2.135-.682h-.07c-.834.019-1.548.26-2.063.7-.472.4-.768.945-.859 1.576l-2.028-.283c.142-.981.58-1.838 1.265-2.477.891-.829 2.092-1.27 3.474-1.274h.096c1.492.013 2.706.46 3.607 1.328.857.825 1.348 2.007 1.461 3.517.636.174 1.227.398 1.768.67 1.327.666 2.358 1.634 2.982 2.8.818 1.524.876 3.916-.935 5.686-1.818 1.779-4.16 2.606-7.378 2.606zm-.39-5.086c1.075-.055 1.834-.467 2.254-1.22.396-.71.583-1.745.558-3.078-.842-.156-1.73-.242-2.66-.258-1.927-.048-3.085.484-3.181 1.461-.064.648.222 1.27.806 1.753.618.512 1.42.785 2.223.785z"/>
</svg>
<span>Threads</span>
</button>
<button type="button" onclick="togglePlatform('facebook')"
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2"
id="btn-facebook">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
<span>Facebook</span> <span>Facebook</span>
</button> </button>
<button type="button" onclick="togglePlatform('instagram')" <button type="button" onclick="togglePlatform('instagram')" id="btn-instagram"
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2" class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
id="btn-instagram"> <span class="text-xl">📸</span>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/>
</svg>
<span>Instagram</span> <span>Instagram</span>
</button> </button>
<button type="button" onclick="togglePlatform('threads')" id="btn-threads"
class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
<span class="text-xl">🧵</span>
<span>Threads</span>
</button>
</div> </div>
<p class="text-gray-500 text-sm mt-2">Selecciona una o más plataformas</p>
</div> </div>
<!-- Content --> <!-- Content -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<div class="flex justify-between items-center mb-4"> <h3 class="font-semibold mb-4 flex items-center gap-2">
<h3 class="font-semibold">Contenido</h3> <span>✍️</span>
<div id="char-counter" class="text-sm"> <span>Contenido</span>
<span id="char-count">0</span> caracteres </h3>
</div>
</div>
<textarea <textarea
id="content" id="content"
name="content"
rows="6" rows="6"
class="w-full px-4 py-3 rounded-lg border resize-none" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-primary focus:outline-none resize-none transition-colors"
placeholder="Escribe tu publicación aquí..." placeholder="Escribe tu publicación aquí..."
oninput="updateCharCount()" oninput="updateCharCount()"
></textarea> ></textarea>
<div class="flex justify-between items-center mt-3">
<!-- Platform limits --> <div id="char-count" class="text-sm text-gray-500">0 / 280</div>
<div id="platform-limits" class="mt-3 space-y-1 text-sm"> <div class="flex gap-2">
<!-- Se llena dinámicamente --> <button type="button" onclick="generateWithAI('tip')" class="btn-secondary px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<span>💡</span> Generar Tip
</button>
<button type="button" onclick="generateWithAI('promo')" class="btn-secondary px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<span>📢</span> Generar Promo
</button>
</div>
</div> </div>
</div> </div>
<!-- AI Assist --> <!-- Hashtags -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Asistente IA (DeepSeek)</h3> <h3 class="font-semibold mb-4 flex items-center gap-2">
<div class="flex gap-2 flex-wrap"> <span>#️⃣</span>
<button type="button" onclick="generateTip()" <span>Hashtags</span>
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2"> </h3>
<span>Generar Tip Tech</span> <input
</button> type="text"
<button type="button" onclick="improveContent()" id="hashtags"
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-primary focus:outline-none transition-colors"
<span>Mejorar Texto</span> placeholder="#Tecnología #Automatización #Tijuana"
</button> >
<button type="button" onclick="adaptContent()" <div class="flex flex-wrap gap-2 mt-3">
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2"> <button type="button" onclick="addHashtag('#ConsultoríaAS')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#ConsultoríaAS</button>
<span>Adaptar por Plataforma</span> <button type="button" onclick="addHashtag('#Tijuana')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Tijuana</button>
</button> <button type="button" onclick="addHashtag('#Tecnología')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Tecnología</button>
<button type="button" onclick="addHashtag('#Automatización')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Automatización</button>
</div> </div>
<p id="ai-status" class="text-gray-500 text-sm mt-2">
Verificando estado de IA...
</p>
</div> </div>
<!-- Schedule --> <!-- Schedule -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Programación</h3> <h3 class="font-semibold mb-4 flex items-center gap-2">
<div class="flex gap-4 items-center"> <span>📅</span>
<label class="flex items-center gap-2"> <span>Programación</span>
<input type="radio" name="schedule" value="now" checked </h3>
class="text-amber-500" onchange="toggleSchedule()"> <div class="grid grid-cols-2 gap-4">
<span>Publicar ahora</span> <div>
</label> <label class="block text-sm text-gray-400 mb-2">Fecha</label>
<label class="flex items-center gap-2"> <input
<input type="radio" name="schedule" value="later" type="date"
class="text-amber-500" onchange="toggleSchedule()"> id="schedule-date"
<span>Programar</span> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white focus:border-primary focus:outline-none transition-colors"
</label> >
</div> </div>
<div id="schedule-picker" class="mt-4 hidden"> <div>
<input type="datetime-local" id="scheduled_at" name="scheduled_at" <label class="block text-sm text-gray-400 mb-2">Hora</label>
class="bg-gray-800 border border-gray-700 rounded-lg px-4 py-2"> <input
type="time"
id="schedule-time"
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white focus:border-primary focus:outline-none transition-colors"
>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Preview -->
<div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4 flex items-center gap-2">
<span>👁️</span>
<span>Vista Previa</span>
</h3>
<div id="preview" class="bg-dark-800 rounded-xl p-4 min-h-32">
<p class="text-gray-500 text-sm italic">El contenido aparecerá aquí...</p>
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex gap-4 justify-end"> <div class="card rounded-2xl p-6">
<button type="button" onclick="saveDraft()" <h3 class="font-semibold mb-4 flex items-center gap-2">
class="btn-secondary px-6 py-3 rounded-lg"> <span></span>
Guardar Borrador <span>Acciones</span>
</h3>
<div class="space-y-3">
<button onclick="saveAsDraft()" class="w-full btn-secondary px-4 py-3 rounded-xl flex items-center justify-center gap-2">
<span>💾</span>
<span>Guardar Borrador</span>
</button> </button>
<button type="button" onclick="previewPost()" <button onclick="schedulePost()" class="w-full btn-secondary px-4 py-3 rounded-xl flex items-center justify-center gap-2">
class="btn-secondary px-6 py-3 rounded-lg"> <span>📅</span>
Vista Previa <span>Programar</span>
</button> </button>
<button type="submit" class="btn-primary px-8 py-3 rounded-lg font-semibold"> <button onclick="publishNow()" class="w-full btn-primary px-4 py-3 rounded-xl flex items-center justify-center gap-2 font-medium">
Publicar <span>🚀</span>
<span>Publicar Ahora</span>
</button> </button>
</div> </div>
</form>
<!-- Preview Modal -->
<div id="preview-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="card p-6 max-w-2xl w-full mx-4 max-h-screen overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Vista Previa</h3>
<button onclick="closePreview()" class="text-gray-400 hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div id="preview-content" class="space-y-4">
<!-- Se llena dinámicamente -->
</div>
</div>
</div> </div>
<!-- Result Modal --> <!-- AI Status -->
<div id="result-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"> <div class="card rounded-2xl p-6">
<div class="card p-6 max-w-md w-full mx-4"> <h3 class="font-semibold mb-4 flex items-center gap-2">
<div id="result-content"> <span>🤖</span>
<!-- Se llena dinámicamente --> <span>IA</span>
</div> </h3>
<button onclick="closeResult()" class="btn-primary w-full mt-4 py-2 rounded-lg"> <div id="ai-status" class="text-sm text-gray-400">
Cerrar Verificando...
</button>
</div> </div>
</div> </div>
</main> </div>
</div>
</div>
{% endblock %}
<script> {% block extra_scripts %}
// State <script>
let selectedPlatforms = []; let selectedPlatforms = [];
const charLimits = { const charLimits = { x: 280, facebook: 63206, instagram: 2200, threads: 500 };
x: 280,
threads: 500,
instagram: 2200,
facebook: 63206
};
// Platform selection
function togglePlatform(platform) { function togglePlatform(platform) {
const btn = document.getElementById(`btn-${platform}`); const btn = document.getElementById(`btn-${platform}`);
const index = selectedPlatforms.indexOf(platform); const index = selectedPlatforms.indexOf(platform);
if (index > -1) { if (index > -1) {
selectedPlatforms.splice(index, 1); selectedPlatforms.splice(index, 1);
btn.classList.remove('ring-2', 'ring-amber-500'); btn.classList.remove('border-primary', 'bg-primary/20');
btn.classList.add('border-transparent');
} else { } else {
selectedPlatforms.push(platform); selectedPlatforms.push(platform);
btn.classList.add('ring-2', 'ring-amber-500'); btn.classList.add('border-primary', 'bg-primary/20');
btn.classList.remove('border-transparent');
} }
updateCharCount(); updateCharCount();
} }
// Character counter
function updateCharCount() { function updateCharCount() {
const content = document.getElementById('content').value; const content = document.getElementById('content').value;
const count = content.length; const countEl = document.getElementById('char-count');
const counter = document.getElementById('char-count'); const previewEl = document.getElementById('preview');
const limitsDiv = document.getElementById('platform-limits');
counter.textContent = count; const limit = selectedPlatforms.includes('x') ? 280 :
selectedPlatforms.includes('threads') ? 500 : 2200;
// Update limits display countEl.textContent = `${content.length} / ${limit}`;
let limitsHtml = '';
selectedPlatforms.forEach(platform => {
const limit = charLimits[platform];
const remaining = limit - count;
let statusClass = '';
let icon = '✓';
if (remaining < 0) { if (content.length > limit) {
statusClass = 'char-error'; countEl.classList.add('text-red-400');
icon = '✗'; countEl.classList.remove('text-yellow-400', 'text-gray-500');
} else if (remaining < 50) { } else if (content.length > limit * 0.9) {
statusClass = 'char-warning'; countEl.classList.add('text-yellow-400');
icon = '⚠'; countEl.classList.remove('text-red-400', 'text-gray-500');
} else {
countEl.classList.add('text-gray-500');
countEl.classList.remove('text-red-400', 'text-yellow-400');
} }
limitsHtml += ` // Update preview
<div class="${statusClass}"> if (content) {
${icon} ${platform.charAt(0).toUpperCase() + platform.slice(1)}: previewEl.innerHTML = `<p class="text-white whitespace-pre-wrap">${content}</p>`;
${remaining >= 0 ? remaining + ' restantes' : Math.abs(remaining) + ' excedidos'} } else {
(máx ${limit}) previewEl.innerHTML = `<p class="text-gray-500 text-sm italic">El contenido aparecerá aquí...</p>`;
</div> }
`;
});
limitsDiv.innerHTML = limitsHtml;
} }
// Schedule toggle function addHashtag(tag) {
function toggleSchedule() { const input = document.getElementById('hashtags');
const scheduleValue = document.querySelector('input[name="schedule"]:checked').value; if (!input.value.includes(tag)) {
const picker = document.getElementById('schedule-picker'); input.value = input.value ? `${input.value} ${tag}` : tag;
picker.classList.toggle('hidden', scheduleValue === 'now'); }
} }
// Preview async function generateWithAI(type) {
function previewPost() { showModal('Generando contenido con IA...', true);
if (selectedPlatforms.length === 0) {
alert('Selecciona al menos una plataforma');
return;
}
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Escribe el contenido del post');
return;
}
const previewDiv = document.getElementById('preview-content');
let html = '';
selectedPlatforms.forEach(platform => {
const limit = charLimits[platform];
const truncated = content.length > limit;
const displayContent = truncated ? content.substring(0, limit) + '...' : content;
html += `
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold">${platform.charAt(0).toUpperCase() + platform.slice(1)}</span>
${truncated ? '<span class="text-red-400 text-xs">(truncado)</span>' : ''}
</div>
<p class="text-gray-300 whitespace-pre-wrap">${displayContent}</p>
<div class="text-gray-500 text-xs mt-2">
${content.length}/${limit} caracteres
</div>
</div>
`;
});
previewDiv.innerHTML = html;
document.getElementById('preview-modal').classList.remove('hidden');
document.getElementById('preview-modal').classList.add('flex');
}
function closePreview() {
document.getElementById('preview-modal').classList.add('hidden');
document.getElementById('preview-modal').classList.remove('flex');
}
// Result modal
function showResult(success, message, details = null) {
const resultDiv = document.getElementById('result-content');
const icon = success ? '✅' : '❌';
let html = `
<div class="text-center">
<div class="text-4xl mb-4">${icon}</div>
<h3 class="text-xl font-semibold mb-2">${success ? 'Publicado' : 'Error'}</h3>
<p class="text-gray-400">${message}</p>
</div>
`;
if (details) {
html += `
<div class="mt-4 space-y-2">
${Object.entries(details).map(([platform, result]) => `
<div class="flex justify-between items-center bg-gray-800 rounded p-2">
<span>${platform}</span>
<span class="${result.success ? 'text-green-400' : 'text-red-400'}">
${result.success ? '✓ OK' : '✗ ' + (result.error || 'Error')}
</span>
</div>
`).join('')}
</div>
`;
}
resultDiv.innerHTML = html;
document.getElementById('result-modal').classList.remove('hidden');
document.getElementById('result-modal').classList.add('flex');
}
function closeResult() {
document.getElementById('result-modal').classList.add('hidden');
document.getElementById('result-modal').classList.remove('flex');
}
// Form submission
document.getElementById('compose-form').addEventListener('submit', async (e) => {
e.preventDefault();
if (selectedPlatforms.length === 0) {
alert('Selecciona al menos una plataforma');
return;
}
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Escribe el contenido del post');
return;
}
// Prepare content per platform
const platformContent = {};
selectedPlatforms.forEach(p => {
platformContent[p] = content;
});
try { try {
const response = await fetch('/api/publish/multiple', { const response = await fetch(`/api/generate/${type}`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body: JSON.stringify({ platforms: selectedPlatforms.length ? selectedPlatforms : ['x'] })
}, });
const data = await response.json();
if (data.success && data.content) {
document.getElementById('content').value = data.content;
updateCharCount();
closeModal();
} else {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error generando contenido</p></div>`);
}
} catch (error) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>`);
}
}
async function saveAsDraft() {
const content = document.getElementById('content').value;
if (!content) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
return;
}
await savePost('draft');
}
async function schedulePost() {
const content = document.getElementById('content').value;
const date = document.getElementById('schedule-date').value;
const time = document.getElementById('schedule-time').value;
if (!content) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
return;
}
if (!date || !time) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Selecciona fecha y hora</p></div>`);
return;
}
await savePost('scheduled', `${date}T${time}:00`);
}
async function publishNow() {
const content = document.getElementById('content').value;
if (!content) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
return;
}
if (!selectedPlatforms.length) {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Selecciona al menos una plataforma</p></div>`);
return;
}
showModal('Publicando...', true);
try {
const response = await fetch('/api/publish/single', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
content: content,
platforms: selectedPlatforms, platforms: selectedPlatforms,
content: platformContent hashtags: document.getElementById('hashtags').value.split(' ').filter(h => h)
}) })
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
showResult(true, showModal(`<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p class="text-lg">¡Publicado!</p><p class="text-gray-400 mt-2">${data.message || 'Post publicado correctamente'}</p></div>`);
`Publicado en ${data.successful_platforms.length} plataforma(s)`,
data.results
);
// Clear form
document.getElementById('content').value = ''; document.getElementById('content').value = '';
selectedPlatforms.forEach(p => togglePlatform(p)); updateCharCount();
selectedPlatforms = [];
} else { } else {
showResult(false, showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ${data.detail || data.error || 'Error al publicar'}</p></div>`);
'Algunas publicaciones fallaron',
data.results
);
} }
} catch (error) { } catch (error) {
showResult(false, error.message); showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>`);
} }
});
// AI functions
let aiLoading = false;
function setAiLoading(loading) {
aiLoading = loading;
document.querySelectorAll('.ai-btn').forEach(btn => {
btn.disabled = loading;
if (loading) {
btn.classList.add('opacity-50', 'cursor-wait');
} else {
btn.classList.remove('opacity-50', 'cursor-wait');
}
});
} }
async function checkAiStatus() { async function savePost(status, scheduledAt = null) {
showModal('Guardando...', true);
try { try {
const response = await fetch('/api/generate/status'); const body = {
const data = await response.json(); content: document.getElementById('content').value,
return data.configured && data.status === 'connected'; platforms: selectedPlatforms.length ? selectedPlatforms : ['x'],
} catch { hashtags: document.getElementById('hashtags').value.split(' ').filter(h => h),
return false; status: status
} };
} if (scheduledAt) body.scheduled_at = scheduledAt;
async function generateTip() { const response = await fetch('/api/posts/', {
if (aiLoading) return;
// Show category selector
const categories = [
'productividad', 'seguridad', 'ia', 'programacion',
'hardware', 'redes', 'cloud', 'automatizacion', 'impresion3d', 'general'
];
const category = prompt(
'Categoría del tip:\n\n' + categories.join(', ') + '\n\n(Enter para "general")'
) || 'general';
const platform = selectedPlatforms[0] || 'x';
setAiLoading(true);
try {
const response = await fetch('/api/generate/tip', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, platform }) body: JSON.stringify(body)
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { showModal(`<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p class="text-lg">Guardado</p><p class="text-gray-400 mt-2">Post ${status === 'scheduled' ? 'programado' : 'guardado como borrador'}</p></div>`);
document.getElementById('content').value = data.content;
updateCharCount();
} else {
alert('Error: ' + (data.error || 'No se pudo generar'));
}
} catch (error) { } catch (error) {
alert('Error de conexión: ' + error.message); showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error guardando</p></div>`);
} finally {
setAiLoading(false);
} }
} }
async function improveContent() {
if (aiLoading) return;
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Primero escribe algo de contenido para mejorar');
return;
}
const styles = ['engaging', 'professional', 'casual', 'educational'];
const style = prompt(
'Estilo de mejora:\n\n' +
'- engaging: Más atractivo\n' +
'- professional: Más formal\n' +
'- casual: Más cercano\n' +
'- educational: Más didáctico\n\n' +
'(Enter para "engaging")'
) || 'engaging';
const platform = selectedPlatforms[0] || 'x';
setAiLoading(true);
try {
const response = await fetch('/api/generate/improve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, platform, style })
});
const data = await response.json();
if (data.success) {
document.getElementById('content').value = data.content;
updateCharCount();
} else {
alert('Error: ' + (data.error || 'No se pudo mejorar'));
}
} catch (error) {
alert('Error de conexión: ' + error.message);
} finally {
setAiLoading(false);
}
}
async function adaptContent() {
if (aiLoading) return;
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Primero escribe algo de contenido para adaptar');
return;
}
if (selectedPlatforms.length < 2) {
alert('Selecciona al menos 2 plataformas para adaptar el contenido');
return;
}
setAiLoading(true);
try {
// Adaptar a cada plataforma excepto la primera (que es el contenido original)
const adaptedContent = { [selectedPlatforms[0]]: content };
for (let i = 1; i < selectedPlatforms.length; i++) {
const platform = selectedPlatforms[i];
const response = await fetch('/api/generate/adapt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, target_platform: platform })
});
const data = await response.json();
if (data.success) {
adaptedContent[platform] = data.content;
}
}
// Mostrar resultados en preview
const previewDiv = document.getElementById('preview-content');
let html = '<p class="text-gray-400 mb-4">Contenido adaptado por plataforma:</p>';
for (const [platform, text] of Object.entries(adaptedContent)) {
html += `
<div class="bg-gray-800 rounded-lg p-4 mb-2">
<div class="font-semibold mb-2">${platform.toUpperCase()}</div>
<p class="text-gray-300 whitespace-pre-wrap text-sm">${text}</p>
<button onclick="useAdaptedContent('${platform}', this)"
class="mt-2 text-amber-500 text-sm hover:underline"
data-content="${encodeURIComponent(text)}">
Usar este
</button>
</div>
`;
}
previewDiv.innerHTML = html;
document.getElementById('preview-modal').classList.remove('hidden');
document.getElementById('preview-modal').classList.add('flex');
} catch (error) {
alert('Error de conexión: ' + error.message);
} finally {
setAiLoading(false);
}
}
function useAdaptedContent(platform, btn) {
const content = decodeURIComponent(btn.dataset.content);
document.getElementById('content').value = content;
updateCharCount();
closePreview();
}
function saveDraft() {
const content = document.getElementById('content').value;
localStorage.setItem('draft_content', content);
localStorage.setItem('draft_platforms', JSON.stringify(selectedPlatforms));
alert('Borrador guardado localmente');
}
// Load draft on page load
window.addEventListener('load', async () => {
const draftContent = localStorage.getItem('draft_content');
const draftPlatforms = localStorage.getItem('draft_platforms');
if (draftContent) {
document.getElementById('content').value = draftContent;
}
if (draftPlatforms) {
const platforms = JSON.parse(draftPlatforms);
platforms.forEach(p => togglePlatform(p));
}
updateCharCount();
// Check AI status // Check AI status
const aiStatus = document.getElementById('ai-status'); async function checkAIStatus() {
try { try {
const response = await fetch('/api/generate/status'); const response = await fetch('/api/generate/status');
const data = await response.json(); const data = await response.json();
document.getElementById('ai-status').innerHTML = data.configured
if (data.configured && data.status === 'connected') { ? `<span class="text-green-400">✓</span> ${data.provider || 'DeepSeek'} configurado`
aiStatus.innerHTML = '<span class="text-green-400">IA conectada y lista</span>'; : `<span class="text-red-400">✗</span> No configurado`;
} else if (data.configured) {
aiStatus.innerHTML = '<span class="text-yellow-400">IA configurada pero con error: ' + (data.error || 'desconocido') + '</span>';
} else {
aiStatus.innerHTML = '<span class="text-red-400">IA no configurada. Agrega DEEPSEEK_API_KEY en .env</span>';
}
} catch (error) { } catch (error) {
aiStatus.innerHTML = '<span class="text-red-400">No se pudo verificar estado de IA</span>'; document.getElementById('ai-status').innerHTML = `<span class="text-red-400">✗</span> Error`;
} }
}); }
</script>
</body> checkAIStatus();
</html> </script>
{% endblock %}

View File

@@ -1,182 +1,250 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head> {% block title %}Dashboard{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>Dashboard - Social Media Automation</title> <div class="animate-fade-in">
<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; }
</style>
</head>
<body class="min-h-screen">
<!-- Header --> <!-- Header -->
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4"> <div class="flex items-center justify-between mb-8">
<div class="flex justify-between items-center"> <div>
<h1 class="text-2xl font-bold"> <h1 class="text-3xl font-bold">Dashboard</h1>
<span class="accent">Consultoría AS</span> - Social Media <p class="text-gray-400 mt-1">Bienvenido, {{ user.username }}</p>
</h1> </div>
<nav class="flex gap-4"> <a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2 transition-all">
<a href="/dashboard" class="px-4 py-2 rounded bg-gray-800">Home</a> <span>✍️</span>
<a href="/dashboard/compose" class="px-4 py-2 rounded hover:bg-gray-800 accent">+ Crear Post</a> <span>Crear Post</span>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a> </a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
</nav>
</div> </div>
</header>
<main class="container mx-auto px-6 py-8"> <!-- Stats Grid -->
<!-- Stats --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="grid grid-cols-5 gap-4 mb-8"> <div class="stat-card card rounded-2xl p-6">
<div class="card p-4 text-center"> <div class="flex items-center justify-between mb-4">
<div class="text-3xl font-bold accent">{{ stats.posts_today }}</div> <div class="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
<div class="text-gray-400 text-sm">Posts Hoy</div> <span class="text-2xl">📝</span>
</div> </div>
<div class="card p-4 text-center"> <span class="text-xs text-gray-500 bg-dark-700 px-2 py-1 rounded-full">Hoy</span>
<div class="text-3xl font-bold accent">{{ stats.posts_week }}</div>
<div class="text-gray-400 text-sm">Posts Semana</div>
</div> </div>
<div class="card p-4 text-center"> <p class="text-3xl font-bold">{{ stats.posts_today }}</p>
<div class="text-3xl font-bold text-yellow-500">{{ stats.pending_approval }}</div> <p class="text-gray-400 text-sm mt-1">Posts publicados</p>
<div class="text-gray-400 text-sm">Pendientes</div>
</div> </div>
<div class="card p-4 text-center">
<div class="text-3xl font-bold text-blue-500">{{ stats.scheduled }}</div> <div class="stat-card card rounded-2xl p-6">
<div class="text-gray-400 text-sm">Programados</div> <div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
<span class="text-2xl">📅</span>
</div> </div>
<div class="card p-4 text-center"> <span class="text-xs text-gray-500 bg-dark-700 px-2 py-1 rounded-full">Semana</span>
<div class="text-3xl font-bold text-red-500">{{ stats.interactions_pending }}</div> </div>
<div class="text-gray-400 text-sm">Interacciones</div> <p class="text-3xl font-bold">{{ stats.posts_week }}</p>
<p class="text-gray-400 text-sm mt-1">Posts esta semana</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-yellow-500/20 flex items-center justify-center">
<span class="text-2xl"></span>
</div>
<span class="text-xs text-yellow-400 bg-yellow-500/20 px-2 py-1 rounded-full">Pendiente</span>
</div>
<p class="text-3xl font-bold">{{ stats.pending_approval }}</p>
<p class="text-gray-400 text-sm mt-1">Por aprobar</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
<span class="text-2xl">🗓️</span>
</div>
<span class="text-xs text-green-400 bg-green-500/20 px-2 py-1 rounded-full">Programado</span>
</div>
<p class="text-3xl font-bold">{{ stats.scheduled }}</p>
<p class="text-gray-400 text-sm mt-1">Posts programados</p>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-6"> <!-- Main Content Grid -->
<!-- Pending Approval --> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="card p-6"> <!-- Pending Posts -->
<h2 class="text-xl font-semibold mb-4">Pendientes de Aprobación</h2> <div class="card rounded-2xl p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold flex items-center gap-2">
<span></span>
<span>Pendientes de Aprobación</span>
</h2>
<a href="/posts?status=pending" class="text-primary text-sm hover:underline">Ver todos</a>
</div>
<div class="space-y-4">
{% if pending_posts %} {% if pending_posts %}
{% for post in pending_posts %} {% for post in pending_posts %}
<div class="border-b border-gray-700 py-4 last:border-0"> <div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
<div class="flex justify-between items-start mb-2"> <p class="text-sm text-gray-300 line-clamp-2">{{ post.content[:100] }}{% if post.content|length > 100 %}...{% endif %}</p>
<span class="bg-blue-900 text-blue-300 px-2 py-1 rounded text-xs"> <div class="flex items-center justify-between mt-3">
{{ post.content_type }} <div class="flex items-center gap-2">
</span> {% for platform in post.platforms %}
<span class="text-gray-500 text-sm"> <span class="text-xs bg-dark-700 px-2 py-1 rounded-full">{{ platform }}</span>
{{ post.scheduled_at }} {% endfor %}
</span>
</div> </div>
<p class="text-gray-300 mb-3">{{ post.content[:200] }}...</p> <span class="text-xs text-gray-500">{{ post.created_at[:10] if post.created_at else '-' }}</span>
<div class="flex gap-2">
<button onclick="approvePost({{ post.id }})"
class="btn-primary px-3 py-1 rounded text-sm">
Aprobar
</button>
<button onclick="rejectPost({{ post.id }})"
class="bg-red-900 text-red-300 px-3 py-1 rounded text-sm">
Rechazar
</button>
<button onclick="editPost({{ post.id }})"
class="bg-gray-700 px-3 py-1 rounded text-sm">
Editar
</button>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p class="text-gray-500">No hay posts pendientes</p> <div class="text-center py-8 text-gray-500">
<span class="text-4xl mb-2 block"></span>
<p>No hay posts pendientes</p>
</div>
{% endif %} {% endif %}
</div> </div>
</div>
<!-- Scheduled --> <!-- Scheduled Posts -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<h2 class="text-xl font-semibold mb-4">Próximas Publicaciones</h2> <div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold flex items-center gap-2">
<span>🗓️</span>
<span>Próximas Publicaciones</span>
</h2>
<a href="/calendar" class="text-primary text-sm hover:underline">Ver calendario</a>
</div>
<div class="space-y-4">
{% if scheduled_posts %} {% if scheduled_posts %}
{% for post in scheduled_posts %} {% for post in scheduled_posts %}
<div class="border-b border-gray-700 py-3 last:border-0 flex items-center gap-4"> <div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
<div class="text-center"> <p class="text-sm text-gray-300 line-clamp-2">{{ post.content[:100] }}{% if post.content|length > 100 %}...{% endif %}</p>
<div class="text-accent font-bold">{{ post.scheduled_at }}</div> <div class="flex items-center justify-between mt-3">
</div> <div class="flex items-center gap-2">
<div class="flex-1">
<span class="text-xs bg-gray-700 px-2 py-1 rounded">
{{ post.content_type }}
</span>
<p class="text-gray-400 text-sm mt-1">
{{ post.content[:100] }}...
</p>
</div>
<div class="flex gap-1">
{% for platform in post.platforms %} {% for platform in post.platforms %}
<span class="text-xs bg-gray-800 px-2 py-1 rounded"> <span class="text-xs bg-dark-700 px-2 py-1 rounded-full">{{ platform }}</span>
{{ platform }}
</span>
{% endfor %} {% endfor %}
</div> </div>
<span class="text-xs text-green-400">{{ post.scheduled_at[:16] if post.scheduled_at else '-' }}</span>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p class="text-gray-500">No hay posts programados</p> <div class="text-center py-8 text-gray-500">
<span class="text-4xl mb-2 block">📭</span>
<p>No hay posts programados</p>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- Recent Interactions --> <!-- Recent Interactions -->
<div class="card p-6 mt-6"> <div class="card rounded-2xl p-6">
<h2 class="text-xl font-semibold mb-4">Interacciones Recientes</h2> <div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold flex items-center gap-2">
<span>💬</span>
<span>Interacciones Recientes</span>
</h2>
<a href="/interactions" class="text-primary text-sm hover:underline">Ver todas</a>
</div>
<div class="space-y-4">
{% if recent_interactions %} {% if recent_interactions %}
<div class="grid gap-4">
{% for interaction in recent_interactions %} {% for interaction in recent_interactions %}
<div class="bg-gray-800 rounded-lg p-4"> <div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
<div class="flex justify-between items-start mb-2"> <div class="flex items-center gap-3 mb-2">
<div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-sm">
{{ interaction.author_username[0]|upper if interaction.author_username else '?' }}
</div>
<div> <div>
<span class="font-semibold">@{{ interaction.author_username }}</span> <p class="text-sm font-medium">@{{ interaction.author_username or 'Usuario' }}</p>
<span class="text-gray-500 text-sm ml-2">{{ interaction.platform }}</span> <p class="text-xs text-gray-500">{{ interaction.interaction_type }}</p>
</div> </div>
<span class="text-gray-500 text-xs">{{ interaction.interaction_at }}</span>
</div>
<p class="text-gray-300">{{ interaction.content }}</p>
<div class="mt-3 flex gap-2">
<button class="bg-accent text-gray-900 px-3 py-1 rounded text-sm">
Responder
</button>
<button class="bg-gray-700 px-3 py-1 rounded text-sm">
Marcar como Lead
</button>
</div> </div>
<p class="text-sm text-gray-400 line-clamp-2">{{ interaction.content[:80] }}{% if interaction.content|length > 80 %}...{% endif %}</p>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<p class="text-gray-500">No hay interacciones pendientes</p> <div class="text-center py-8 text-gray-500">
<span class="text-4xl mb-2 block">💤</span>
<p>No hay interacciones pendientes</p>
</div>
{% endif %} {% endif %}
</div> </div>
</main> </div>
</div>
<script> <!-- Quick Actions -->
async function approvePost(postId) { <div class="mt-8">
const response = await fetch(`/api/posts/${postId}/approve`, { method: 'POST' }); <h2 class="text-lg font-semibold mb-4">Acciones Rápidas</h2>
if (response.ok) { <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
location.reload(); <a href="/compose" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">✍️</span>
<span class="text-sm text-gray-300">Nuevo Post</span>
</a>
<a href="/compose?type=tip" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">💡</span>
<span class="text-sm text-gray-300">Generar Tip</span>
</a>
<a href="/analytics" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">📊</span>
<span class="text-sm text-gray-300">Ver Analytics</span>
</a>
<a href="/settings" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">⚙️</span>
<span class="text-sm text-gray-300">Configuración</span>
</a>
</div>
</div>
<!-- Platform Status -->
<div class="mt-8">
<h2 class="text-lg font-semibold mb-4">Estado de Plataformas</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4" id="platform-status">
<!-- Loaded via JS -->
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
async function loadPlatformStatus() {
const container = document.getElementById('platform-status');
const platforms = ['x', 'facebook', 'instagram', 'threads'];
container.innerHTML = platforms.map(p => `
<div class="card rounded-xl p-4">
<div class="flex items-center justify-between">
<span class="text-xl">${getPlatformIcon(p)}</span>
<span class="text-xs bg-dark-700 px-2 py-1 rounded-full">Verificando...</span>
</div>
<p class="text-sm mt-2 capitalize">${p}</p>
</div>
`).join('');
try {
const response = await fetch('/api/publish/test');
const data = await response.json();
container.innerHTML = '';
for (const [platform, status] of Object.entries(data)) {
const isConnected = status.configured && status.connected;
container.innerHTML += `
<div class="card rounded-xl p-4">
<div class="flex items-center justify-between">
<span class="text-xl">${getPlatformIcon(platform)}</span>
<span class="text-xs ${isConnected ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'} px-2 py-1 rounded-full">
${isConnected ? 'Conectado' : 'Desconectado'}
</span>
</div>
<p class="text-sm mt-2 capitalize">${platform}</p>
${status.details?.username ? `<p class="text-xs text-gray-500">@${status.details.username}</p>` : ''}
</div>
`;
}
} catch (error) {
container.innerHTML = '<div class="col-span-4 text-center text-gray-500 py-4">Error cargando estado</div>';
} }
} }
async function rejectPost(postId) { function getPlatformIcon(platform) {
if (confirm('¿Seguro que quieres rechazar este post?')) { const icons = { x: '𝕏', facebook: '📘', instagram: '📸', threads: '🧵' };
const response = await fetch(`/api/posts/${postId}/reject`, { method: 'POST' }); return icons[platform] || '📱';
if (response.ok) {
location.reload();
}
}
} }
function editPost(postId) { loadPlatformStatus();
window.location.href = `/posts/${postId}/edit`; </script>
} {% endblock %}
</script>
</body>
</html>

View File

@@ -1,125 +1,117 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interacciones - 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; }
.btn-secondary:hover { background-color: #4b5563; }
.interaction-item { transition: all 0.2s; }
.interaction-item:hover { background-color: #1e3a5f; }
.interaction-item.selected { border-left: 3px solid #d4a574; }
.platform-x { color: #1d9bf0; }
.platform-threads { color: #fff; }
.platform-instagram { color: #e1306c; }
.platform-facebook { color: #1877f2; }
.type-comment { background-color: #1e40af; }
.type-mention { background-color: #7c3aed; }
.type-dm { background-color: #059669; }
</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> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/interactions" class="px-4 py-2 rounded bg-gray-800 accent">Interacciones</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</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"> {% block title %}Interacciones{% endblock %}
<div class="grid grid-cols-3 gap-6">
{% block content %}
<div class="animate-fade-in">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Interacciones</h1>
<p class="text-gray-400 mt-1">Gestiona comentarios, menciones y mensajes</p>
</div>
<button onclick="syncInteractions()" class="btn-secondary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
<span id="sync-icon">🔄</span>
<span>Sincronizar</span>
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Interactions List --> <!-- Interactions List -->
<div class="col-span-2"> <div class="lg:col-span-2 space-y-6">
<!-- Filters --> <!-- Filters -->
<div class="card p-4 mb-4"> <div class="card rounded-2xl p-4">
<div class="flex gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center">
<select id="filter-platform" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-platform" onchange="filterInteractions()" 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="">Todas las plataformas</option>
<option value="x">X (Twitter)</option> <option value="x">X (Twitter)</option>
<option value="threads">Threads</option> <option value="threads">Threads</option>
<option value="instagram">Instagram</option> <option value="instagram">Instagram</option>
<option value="facebook">Facebook</option> <option value="facebook">Facebook</option>
</select> </select>
<select id="filter-type" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-type" onchange="filterInteractions()" 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 tipos</option> <option value="">Todos los tipos</option>
<option value="comment">Comentarios</option> <option value="comment">Comentarios</option>
<option value="mention">Menciones</option> <option value="mention">Menciones</option>
<option value="dm">Mensajes</option> <option value="dm">Mensajes</option>
</select> </select>
<select id="filter-status" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-status" onchange="filterInteractions()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
<option value="pending">Sin responder</option> <option value="pending">Sin responder</option>
<option value="responded">Respondidos</option> <option value="responded">Respondidos</option>
<option value="all">Todos</option> <option value="all">Todos</option>
</select> </select>
<div class="flex-1"></div>
<button onclick="syncInteractions()" class="btn-secondary px-4 py-2 rounded flex items-center gap-2">
<span id="sync-icon">🔄</span> Sincronizar
</button>
</div> </div>
</div> </div>
<!-- Stats --> <!-- Stats -->
<div class="grid grid-cols-4 gap-4 mb-4"> <div class="grid grid-cols-4 gap-4">
<div class="card p-3 text-center"> <div class="card rounded-2xl p-4 text-center">
<div class="text-xl font-bold text-red-400" id="stat-pending">0</div> <div class="text-2xl font-bold text-red-400" id="stat-pending">0</div>
<div class="text-xs text-gray-400">Sin responder</div> <div class="text-xs text-gray-400">Sin responder</div>
</div> </div>
<div class="card p-3 text-center"> <div class="card rounded-2xl p-4 text-center">
<div class="text-xl font-bold text-green-400" id="stat-responded">0</div> <div class="text-2xl font-bold text-green-400" id="stat-responded">0</div>
<div class="text-xs text-gray-400">Respondidos</div> <div class="text-xs text-gray-400">Respondidos</div>
</div> </div>
<div class="card p-3 text-center"> <div class="card rounded-2xl p-4 text-center">
<div class="text-xl font-bold text-purple-400" id="stat-leads">0</div> <div class="text-2xl font-bold text-purple-400" id="stat-leads">0</div>
<div class="text-xs text-gray-400">Leads</div> <div class="text-xs text-gray-400">Leads</div>
</div> </div>
<div class="card p-3 text-center"> <div class="card rounded-2xl p-4 text-center">
<div class="text-xl font-bold text-gray-400" id="stat-total">0</div> <div class="text-2xl font-bold text-gray-400" id="stat-total">0</div>
<div class="text-xs text-gray-400">Total</div> <div class="text-xs text-gray-400">Total</div>
</div> </div>
</div> </div>
<!-- List --> <!-- List -->
<div class="card"> <div class="card rounded-2xl overflow-hidden">
<div class="p-4 border-b border-gray-700"> <div class="p-4 border-b border-dark-600">
<h2 class="font-semibold">Interacciones</h2> <h2 class="font-semibold">Interacciones</h2>
</div> </div>
<div id="interactions-list" class="divide-y divide-gray-700 max-h-screen overflow-y-auto"> <div id="interactions-list" class="divide-y divide-dark-600 max-h-[600px] overflow-y-auto">
<div class="p-8 text-center text-gray-500">Cargando...</div> <div class="p-8 text-center text-gray-500">Cargando...</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Detail Panel --> <!-- Detail Panel -->
<div class="col-span-1"> <div class="lg:col-span-1">
<div class="card p-6 sticky top-6" id="detail-panel"> <div class="card rounded-2xl p-6 sticky top-6" id="detail-panel">
<div class="text-center text-gray-500 py-8"> <div class="text-center text-gray-500 py-8">
<span class="text-4xl block mb-4">💬</span>
<p>Selecciona una interacción</p> <p>Selecciona una interacción</p>
<p class="text-sm mt-2">para ver detalles y responder</p> <p class="text-sm mt-2">para ver detalles y responder</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</main> </div>
{% endblock %}
<script> {% block extra_scripts %}
<style>
.interaction-item { transition: all 0.2s; }
.interaction-item:hover { background-color: rgba(99, 102, 241, 0.1); }
.interaction-item.selected { border-left: 3px solid #6366f1; background-color: rgba(99, 102, 241, 0.1); }
.platform-x { color: #1d9bf0; }
.platform-threads { color: #fff; }
.platform-instagram { color: #e1306c; }
.platform-facebook { color: #1877f2; }
.type-comment { background-color: rgba(59, 130, 246, 0.3); color: #60a5fa; }
.type-mention { background-color: rgba(139, 92, 246, 0.3); color: #a78bfa; }
.type-dm { background-color: rgba(16, 185, 129, 0.3); color: #34d399; }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
<script>
let interactions = []; let interactions = [];
let selectedId = null; let selectedId = null;
@@ -181,14 +173,14 @@
container.innerHTML = items.map(item => ` container.innerHTML = items.map(item => `
<div class="interaction-item p-4 cursor-pointer ${selectedId === item.id ? 'selected' : ''}" <div class="interaction-item p-4 cursor-pointer ${selectedId === item.id ? 'selected' : ''}"
onclick="selectInteraction(${item.id})"> onclick="selectInteraction(${item.id}, this)">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="platform-${item.platform} font-medium">@${item.author_username || 'usuario'}</span> <span class="platform-${item.platform} font-medium">@${item.author_username || 'usuario'}</span>
<span class="type-${item.interaction_type} text-xs px-2 py-0.5 rounded">${item.interaction_type}</span> <span class="type-${item.interaction_type} text-xs px-2 py-0.5 rounded-full">${item.interaction_type}</span>
<span class="text-gray-500 text-xs">${item.platform}</span> <span class="text-gray-500 text-xs">${item.platform}</span>
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-0.5 rounded">Lead</span>' : ''} ${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-0.5 rounded-full">Lead</span>' : ''}
</div> </div>
<p class="text-gray-300 text-sm line-clamp-2">${escapeHtml(item.content || '')}</p> <p class="text-gray-300 text-sm line-clamp-2">${escapeHtml(item.content || '')}</p>
<div class="text-gray-500 text-xs mt-1"> <div class="text-gray-500 text-xs mt-1">
@@ -196,35 +188,33 @@
${item.responded ? '<span class="text-green-400 ml-2">✓ Respondido</span>' : ''} ${item.responded ? '<span class="text-green-400 ml-2">✓ Respondido</span>' : ''}
</div> </div>
</div> </div>
${!item.responded ? '<span class="w-2 h-2 bg-red-500 rounded-full"></span>' : ''} ${!item.responded ? '<span class="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"></span>' : ''}
</div> </div>
</div> </div>
`).join(''); `).join('');
} }
function selectInteraction(id) { function selectInteraction(id, element) {
selectedId = id; selectedId = id;
const item = interactions.find(i => i.id === id); const item = interactions.find(i => i.id === id);
if (!item) return; if (!item) return;
// Update selection visual
document.querySelectorAll('.interaction-item').forEach(el => el.classList.remove('selected')); document.querySelectorAll('.interaction-item').forEach(el => el.classList.remove('selected'));
event.currentTarget?.classList.add('selected'); element?.classList.add('selected');
// Render detail panel
const panel = document.getElementById('detail-panel'); const panel = document.getElementById('detail-panel');
panel.innerHTML = ` panel.innerHTML = `
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="platform-${item.platform} font-semibold text-lg">@${item.author_username || 'usuario'}</span> <span class="platform-${item.platform} font-semibold text-lg">@${item.author_username || 'usuario'}</span>
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-1 rounded">Lead</span>' : ''} ${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-1 rounded-full">Lead</span>' : ''}
</div> </div>
<span class="type-${item.interaction_type} text-xs px-2 py-1 rounded">${item.interaction_type}</span> <span class="type-${item.interaction_type} text-xs px-2 py-1 rounded-full">${item.interaction_type}</span>
</div> </div>
<div class="bg-gray-800 rounded-lg p-4"> <div class="bg-dark-800 rounded-xl p-4">
<p class="text-gray-200">${escapeHtml(item.content || '')}</p> <p class="text-gray-200">${escapeHtml(item.content || '')}</p>
<div class="text-gray-500 text-xs mt-2"> <div class="text-gray-500 text-xs mt-2">
${formatDate(item.interaction_at)}${item.platform} ${formatDate(item.interaction_at)}${item.platform}
@@ -232,7 +222,7 @@
</div> </div>
${item.responded && item.response_content ? ` ${item.responded && item.response_content ? `
<div class="bg-green-900 bg-opacity-30 rounded-lg p-4"> <div class="bg-green-900/30 rounded-xl p-4">
<div class="text-xs text-green-400 mb-1">Tu respuesta:</div> <div class="text-xs text-green-400 mb-1">Tu respuesta:</div>
<p class="text-gray-200">${escapeHtml(item.response_content)}</p> <p class="text-gray-200">${escapeHtml(item.response_content)}</p>
</div> </div>
@@ -242,11 +232,11 @@
<div> <div>
<label class="block text-sm text-gray-400 mb-2">Responder</label> <label class="block text-sm text-gray-400 mb-2">Responder</label>
<textarea id="response-text" rows="3" <textarea id="response-text" rows="3"
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm" class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-sm focus:border-primary focus:outline-none"
placeholder="Escribe tu respuesta..."></textarea> placeholder="Escribe tu respuesta..."></textarea>
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2">
<button onclick="generateSuggestions(${item.id})" class="btn-secondary px-3 py-1 rounded text-sm flex-1"> <button onclick="generateSuggestions(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
🤖 Sugerir IA 🤖 Sugerir IA
</button> </button>
</div> </div>
@@ -257,19 +247,19 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button onclick="sendResponse(${item.id})" class="btn-primary flex-1 py-2 rounded"> <button onclick="sendResponse(${item.id})" class="btn-primary flex-1 py-3 rounded-xl">
Responder Responder
</button> </button>
</div> </div>
` : ''} ` : ''}
<div class="flex gap-2 pt-4 border-t border-gray-700"> <div class="flex gap-2 pt-4 border-t border-dark-600">
${!item.is_potential_lead ? ` ${!item.is_potential_lead ? `
<button onclick="markAsLead(${item.id})" class="btn-secondary px-3 py-2 rounded text-sm flex-1"> <button onclick="markAsLead(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
⭐ Marcar Lead ⭐ Marcar Lead
</button> </button>
` : ` ` : `
<button onclick="unmarkLead(${item.id})" class="btn-secondary px-3 py-2 rounded text-sm flex-1"> <button onclick="unmarkLead(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
Quitar Lead Quitar Lead
</button> </button>
`} `}
@@ -303,7 +293,7 @@
if (data.success && data.contents) { if (data.success && data.contents) {
suggestionsDiv.innerHTML = data.contents.map((suggestion, i) => ` suggestionsDiv.innerHTML = data.contents.map((suggestion, i) => `
<div class="bg-gray-800 rounded p-2 text-sm cursor-pointer hover:bg-gray-700" <div class="bg-dark-800 rounded-xl p-3 text-sm cursor-pointer hover:bg-dark-700 transition-colors"
onclick="useSuggestion('${escapeHtml(suggestion).replace(/'/g, "\\'")}')"> onclick="useSuggestion('${escapeHtml(suggestion).replace(/'/g, "\\'")}')">
${i + 1}. ${escapeHtml(suggestion)} ${i + 1}. ${escapeHtml(suggestion)}
</div> </div>
@@ -323,7 +313,7 @@
async function sendResponse(id) { async function sendResponse(id) {
const responseText = document.getElementById('response-text').value.trim(); const responseText = document.getElementById('response-text').value.trim();
if (!responseText) { if (!responseText) {
alert('Escribe una respuesta'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe una respuesta</p></div>');
return; return;
} }
@@ -336,7 +326,7 @@
loadInteractions(); loadInteractions();
} catch (error) { } catch (error) {
alert('Error al enviar: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al enviar</p></div>');
} }
} }
@@ -345,7 +335,7 @@
await fetch(`/api/interactions/${id}/mark-lead`, { method: 'POST' }); await fetch(`/api/interactions/${id}/mark-lead`, { method: 'POST' });
loadInteractions(); loadInteractions();
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
} }
} }
@@ -354,7 +344,7 @@
await fetch(`/api/interactions/${id}/unmark-lead`, { method: 'POST' }); await fetch(`/api/interactions/${id}/unmark-lead`, { method: 'POST' });
loadInteractions(); loadInteractions();
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
} }
} }
@@ -365,9 +355,9 @@
await fetch(`/api/interactions/${id}/archive`, { method: 'POST' }); await fetch(`/api/interactions/${id}/archive`, { method: 'POST' });
loadInteractions(); loadInteractions();
document.getElementById('detail-panel').innerHTML = document.getElementById('detail-panel').innerHTML =
'<div class="text-center text-gray-500 py-8"><p>Interacción archivada</p></div>'; '<div class="text-center text-gray-500 py-8"><span class="text-4xl block mb-4">📦</span><p>Interacción archivada</p></div>';
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
} }
} }
@@ -379,7 +369,7 @@
await fetch('/api/interactions/sync', { method: 'POST' }); await fetch('/api/interactions/sync', { method: 'POST' });
await loadInteractions(); await loadInteractions();
} catch (error) { } catch (error) {
alert('Error al sincronizar: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al sincronizar</p></div>');
} }
icon.style.animation = ''; icon.style.animation = '';
@@ -406,19 +396,5 @@
div.textContent = text || ''; div.textContent = text || '';
return div.innerHTML; return div.innerHTML;
} }
</script> </script>
{% endblock %}
<style>
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</body>
</html>

View File

@@ -1,77 +1,55 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head> {% block title %}Leads{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>Leads - Social Media Automation</title> <div class="animate-fade-in">
<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 -->
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4"> <div class="flex items-center justify-between mb-8">
<div class="flex justify-between items-center"> <div>
<h1 class="text-2xl font-bold"> <h1 class="text-3xl font-bold">Leads</h1>
<span class="accent">Consultoría AS</span> - Leads <p class="text-gray-400 mt-1">Gestiona tus prospectos y oportunidades</p>
</h1> </div>
<nav class="flex gap-4"> <div class="flex gap-3">
<a href="/dashboard" class="px-4 py-2 rounded hover:bg-gray-800">Home</a> <button onclick="showNewLeadModal()" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a> <span></span>
<a href="/dashboard/leads" class="px-4 py-2 rounded bg-gray-800">Leads</a> <span>Nuevo Lead</span>
<a href="/dashboard/analytics" class="px-4 py-2 rounded hover:bg-gray-800">Analytics</a> </button>
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a> <button onclick="syncLeadsToOdoo()" class="btn-secondary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
</nav> <span>🔄</span>
<span>Sincronizar a Odoo</span>
</button>
</div>
</div> </div>
</header>
<main class="container mx-auto px-6 py-8">
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="grid grid-cols-5 gap-4 mb-8"> <div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold accent" id="totalLeads">-</div> <p class="text-3xl font-bold text-primary" id="totalLeads">-</p>
<div class="text-gray-400 text-sm">Total Leads</div> <p class="text-gray-400 text-sm mt-1">Total Leads</p>
</div> </div>
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold text-blue-400" id="newLeads">-</div> <p class="text-3xl font-bold text-blue-400" id="newLeads">-</p>
<div class="text-gray-400 text-sm">Nuevos</div> <p class="text-gray-400 text-sm mt-1">Nuevos</p>
</div> </div>
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold text-purple-400" id="contactedLeads">-</div> <p class="text-3xl font-bold text-purple-400" id="contactedLeads">-</p>
<div class="text-gray-400 text-sm">Contactados</div> <p class="text-gray-400 text-sm mt-1">Contactados</p>
</div> </div>
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold text-green-400" id="qualifiedLeads">-</div> <p class="text-3xl font-bold text-green-400" id="qualifiedLeads">-</p>
<div class="text-gray-400 text-sm">Calificados</div> <p class="text-gray-400 text-sm mt-1">Calificados</p>
</div> </div>
<div class="card p-4 text-center"> <div class="stat-card card rounded-2xl p-6">
<div class="text-3xl font-bold text-yellow-400" id="unsyncedLeads">-</div> <p class="text-3xl font-bold text-yellow-400" id="unsyncedLeads">-</p>
<div class="text-gray-400 text-sm">Sin Sincronizar</div> <p class="text-gray-400 text-sm mt-1">Sin Sincronizar</p>
</div> </div>
</div> </div>
<!-- Filters and Actions --> <!-- Filters -->
<div class="flex justify-between items-center mb-6"> <div class="card rounded-2xl p-4 mb-6">
<div class="flex gap-2"> <div class="flex flex-wrap gap-4 items-center">
<select id="statusFilter" onchange="loadLeads()" <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">
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
<option value="">Todos los estados</option> <option value="">Todos los estados</option>
<option value="new">Nuevo</option> <option value="new">Nuevo</option>
<option value="contacted">Contactado</option> <option value="contacted">Contactado</option>
@@ -80,16 +58,14 @@
<option value="won">Ganado</option> <option value="won">Ganado</option>
<option value="lost">Perdido</option> <option value="lost">Perdido</option>
</select> </select>
<select id="priorityFilter" onchange="loadLeads()" <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">
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
<option value="">Todas las prioridades</option> <option value="">Todas las prioridades</option>
<option value="urgent">Urgente</option> <option value="urgent">Urgente</option>
<option value="high">Alta</option> <option value="high">Alta</option>
<option value="medium">Media</option> <option value="medium">Media</option>
<option value="low">Baja</option> <option value="low">Baja</option>
</select> </select>
<select id="platformFilter" onchange="loadLeads()" <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">
class="bg-gray-800 border border-gray-700 rounded px-4 py-2">
<option value="">Todas las plataformas</option> <option value="">Todas las plataformas</option>
<option value="x">X (Twitter)</option> <option value="x">X (Twitter)</option>
<option value="threads">Threads</option> <option value="threads">Threads</option>
@@ -97,60 +73,55 @@
<option value="facebook">Facebook</option> <option value="facebook">Facebook</option>
</select> </select>
</div> </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> </div>
<!-- Leads List --> <!-- Leads List -->
<div class="card p-6"> <div class="card rounded-2xl p-6">
<div id="leadsList" class="space-y-4"> <div id="leadsList" class="space-y-4">
<!-- Populated by JS --> <div class="text-center py-8 text-gray-500">Cargando...</div>
</div> </div>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="flex justify-center mt-6 gap-2"> <div class="flex justify-center mt-6 gap-2">
<button onclick="prevPage()" id="prevBtn" class="btn-secondary px-4 py-2 rounded" disabled> <button onclick="prevPage()" id="prevBtn" class="btn-secondary px-4 py-2 rounded-xl" disabled>
Anterior Anterior
</button> </button>
<span id="pageInfo" class="px-4 py-2">Página 1</span> <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"> <button onclick="nextPage()" id="nextBtn" class="btn-secondary px-4 py-2 rounded-xl">
Siguiente Siguiente
</button> </button>
</div> </div>
</main> </div>
<!-- New Lead Modal --> <!-- New Lead Modal -->
<div id="newLeadModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50"> <div id="newLeadModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
<div class="card p-6 w-full max-w-lg"> <div class="card rounded-2xl p-6 w-full max-w-lg mx-4">
<h3 class="text-xl font-semibold mb-4">Nuevo Lead</h3> <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)"> <form id="newLeadForm" onsubmit="createLead(event)">
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Nombre</label> <label class="block text-sm text-gray-400 mb-2">Nombre</label>
<input type="text" name="name" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Email</label> <label class="block text-sm text-gray-400 mb-2">Email</label>
<input type="email" name="email" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Teléfono</label> <label class="block text-sm text-gray-400 mb-2">Teléfono</label>
<input type="text" name="phone" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Empresa</label> <label class="block text-sm text-gray-400 mb-2">Empresa</label>
<input type="text" name="company" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Plataforma *</label> <label class="block text-sm text-gray-400 mb-2">Plataforma *</label>
<select name="platform" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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="manual">Manual</option>
<option value="x">X (Twitter)</option> <option value="x">X (Twitter)</option>
<option value="threads">Threads</option> <option value="threads">Threads</option>
@@ -159,8 +130,8 @@
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Prioridad</label> <label class="block text-sm text-gray-400 mb-2">Prioridad</label>
<select name="priority" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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="medium">Media</option>
<option value="low">Baja</option> <option value="low">Baja</option>
<option value="high">Alta</option> <option value="high">Alta</option>
@@ -169,51 +140,50 @@
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">Interés</label> <label class="block text-sm text-gray-400 mb-2">Interés</label>
<textarea name="interest" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> <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="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>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" onclick="closeNewLeadModal()" class="btn-secondary px-4 py-2 rounded"> <button type="button" onclick="closeNewLeadModal()" class="btn-secondary px-4 py-2 rounded-xl">
Cancelar Cancelar
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded"> <button type="submit" class="btn-primary px-4 py-2 rounded-xl">
Crear Lead Crear Lead
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Edit Lead Modal --> <!-- Edit Lead Modal -->
<div id="editLeadModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50"> <div id="editLeadModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
<div class="card p-6 w-full max-w-lg"> <div class="card rounded-2xl p-6 w-full max-w-lg mx-4">
<h3 class="text-xl font-semibold mb-4">Editar Lead</h3> <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)"> <form id="editLeadForm" onsubmit="updateLead(event)">
<input type="hidden" name="lead_id" id="editLeadId"> <input type="hidden" name="lead_id" id="editLeadId">
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Nombre</label> <label class="block text-sm text-gray-400 mb-2">Nombre</label>
<input type="text" name="name" id="editName" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Email</label> <label class="block text-sm text-gray-400 mb-2">Email</label>
<input type="email" name="email" id="editEmail" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Teléfono</label> <label class="block text-sm text-gray-400 mb-2">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"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Empresa</label> <label class="block text-sm text-gray-400 mb-2">Empresa</label>
<input type="text" name="company" id="editCompany" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Estado</label> <label class="block text-sm text-gray-400 mb-2">Estado</label>
<select name="status" id="editStatus" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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="new">Nuevo</option>
<option value="contacted">Contactado</option> <option value="contacted">Contactado</option>
<option value="qualified">Calificado</option> <option value="qualified">Calificado</option>
@@ -223,8 +193,8 @@
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Prioridad</label> <label class="block text-sm text-gray-400 mb-2">Prioridad</label>
<select name="priority" id="editPriority" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <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="low">Baja</option>
<option value="medium">Media</option> <option value="medium">Media</option>
<option value="high">Alta</option> <option value="high">Alta</option>
@@ -233,26 +203,36 @@
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">Interés</label> <label class="block text-sm text-gray-400 mb-2">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> <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="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>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" onclick="closeEditLeadModal()" class="btn-secondary px-4 py-2 rounded"> <button type="button" onclick="closeEditLeadModal()" class="btn-secondary px-4 py-2 rounded-xl">
Cancelar Cancelar
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded"> <button type="submit" class="btn-primary px-4 py-2 rounded-xl">
Guardar Guardar
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
{% endblock %}
<script> {% 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; let currentPage = 0;
const pageSize = 20; const pageSize = 20;
let totalLeads = 0; let totalLeads = 0;
@@ -278,8 +258,6 @@
totalLeads = data.total; totalLeads = data.total;
renderLeads(data.leads); renderLeads(data.leads);
updatePagination(); updatePagination();
// Also load stats
loadStats(); loadStats();
} catch (error) { } catch (error) {
@@ -305,7 +283,6 @@
function renderLeads(leads) { function renderLeads(leads) {
const container = document.getElementById('leadsList'); const container = document.getElementById('leadsList');
container.innerHTML = '';
const statusLabels = { const statusLabels = {
new: 'Nuevo', new: 'Nuevo',
@@ -324,9 +301,13 @@
manual: '✏️' manual: '✏️'
}; };
leads.forEach(lead => { if (leads.length === 0) {
container.innerHTML += ` container.innerHTML = '<p class="text-gray-500 text-center py-8">No hay leads que coincidan con los filtros</p>';
<div class="bg-gray-800 rounded-lg p-4 priority-${lead.priority}"> 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 justify-between items-start mb-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-2xl">${platformIcons[lead.platform] || '📱'}</span> <span class="text-2xl">${platformIcons[lead.platform] || '📱'}</span>
@@ -338,7 +319,7 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="status-${lead.status} px-2 py-1 rounded text-xs text-white"> <span class="status-${lead.status} px-2 py-1 rounded-full text-xs">
${statusLabels[lead.status] || lead.status} ${statusLabels[lead.status] || lead.status}
</span> </span>
${lead.synced_to_odoo ? '<span class="text-green-400 text-xs">✓ Odoo</span>' : ''} ${lead.synced_to_odoo ? '<span class="text-green-400 text-xs">✓ Odoo</span>' : ''}
@@ -357,7 +338,7 @@
Editar Editar
</button> </button>
${!lead.synced_to_odoo ? ` ${!lead.synced_to_odoo ? `
<button onclick="syncSingleLead(${lead.id})" class="text-sm text-accent hover:underline"> <button onclick="syncSingleLead(${lead.id})" class="text-sm text-primary hover:underline">
Sincronizar Sincronizar
</button> </button>
` : ''} ` : ''}
@@ -367,12 +348,7 @@
</div> </div>
</div> </div>
</div> </div>
`; `).join('');
});
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() { function updatePagination() {
@@ -418,8 +394,7 @@
company: formData.get('company') || null, company: formData.get('company') || null,
platform: formData.get('platform'), platform: formData.get('platform'),
priority: formData.get('priority'), priority: formData.get('priority'),
interest: formData.get('interest') || null, interest: formData.get('interest') || null
notes: formData.get('notes') || null
}; };
try { try {
@@ -432,12 +407,13 @@
if (res.ok) { if (res.ok) {
closeNewLeadModal(); closeNewLeadModal();
loadLeads(); loadLeads();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Lead creado</p></div>');
} else { } else {
const data = await res.json(); const data = await res.json();
alert('Error: ' + (data.detail || 'Error creando lead')); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error creando lead') + '</p></div>');
} }
} catch (error) { } catch (error) {
alert('Error creando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error creando lead</p></div>');
} }
} }
@@ -454,12 +430,11 @@
document.getElementById('editStatus').value = lead.status; document.getElementById('editStatus').value = lead.status;
document.getElementById('editPriority').value = lead.priority; document.getElementById('editPriority').value = lead.priority;
document.getElementById('editInterest').value = lead.interest || ''; document.getElementById('editInterest').value = lead.interest || '';
document.getElementById('editNotes').value = lead.notes || '';
document.getElementById('editLeadModal').classList.remove('hidden'); document.getElementById('editLeadModal').classList.remove('hidden');
} catch (error) { } catch (error) {
alert('Error cargando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error cargando lead</p></div>');
} }
} }
@@ -480,8 +455,7 @@
company: formData.get('company') || null, company: formData.get('company') || null,
status: formData.get('status'), status: formData.get('status'),
priority: formData.get('priority'), priority: formData.get('priority'),
interest: formData.get('interest') || null, interest: formData.get('interest') || null
notes: formData.get('notes') || null
}; };
try { try {
@@ -496,10 +470,10 @@
loadLeads(); loadLeads();
} else { } else {
const data = await res.json(); const data = await res.json();
alert('Error: ' + (data.detail || 'Error actualizando lead')); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error actualizando') + '</p></div>');
} }
} catch (error) { } catch (error) {
alert('Error actualizando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error actualizando lead</p></div>');
} }
} }
@@ -511,10 +485,10 @@
if (res.ok) { if (res.ok) {
loadLeads(); loadLeads();
} else { } else {
alert('Error eliminando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando lead</p></div>');
} }
} catch (error) { } catch (error) {
alert('Error eliminando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando lead</p></div>');
} }
} }
@@ -524,34 +498,33 @@
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
alert('Lead sincronizado exitosamente'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Lead sincronizado</p></div>');
loadLeads(); loadLeads();
} else { } else {
alert('Error: ' + (data.detail || 'Error sincronizando')); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error sincronizando') + '</p></div>');
} }
} catch (error) { } catch (error) {
alert('Error sincronizando lead'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando lead</p></div>');
} }
} }
async function syncLeadsToOdoo() { async function syncLeadsToOdoo() {
showModal('Sincronizando leads a Odoo...', true);
try { try {
const res = await fetch('/api/odoo/sync/leads', { method: 'POST' }); const res = await fetch('/api/odoo/sync/leads', { method: 'POST' });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
alert(`Sincronización completada: ${data.created} leads exportados`); 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(); loadLeads();
} else { } else {
alert('Error: ' + (data.detail || 'Error sincronizando')); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + (data.detail || 'Error sincronizando') + '</p></div>');
} }
} catch (error) { } catch (error) {
alert('Error sincronizando leads'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando leads</p></div>');
} }
} }
// Load on page load
document.addEventListener('DOMContentLoaded', loadLeads); document.addEventListener('DOMContentLoaded', loadLeads);
</script> </script>
</body> {% endblock %}
</html>

View File

@@ -4,86 +4,61 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Social Media Automation</title> <title>Login - 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.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
body { body { font-family: 'Inter', sans-serif; }
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); .gradient-bg { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); }
min-height: 100vh; .card { background: rgba(30, 41, 59, 0.8); backdrop-filter: blur(10px); border: 1px solid rgba(99, 102, 241, 0.1); }
} .btn-primary { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
.card { .btn-primary:hover { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); }
background-color: #16213e; .input-field { background: rgba(15, 23, 42, 0.5); border: 1px solid rgba(99, 102, 241, 0.2); }
border-radius: 12px; .input-field:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); }
} .animate-float { animation: float 3s ease-in-out infinite; }
.accent { color: #d4a574; } @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.btn-primary {
background-color: #d4a574;
color: #1a1a2e;
transition: all 0.3s;
}
.btn-primary:hover {
background-color: #c49564;
transform: translateY(-2px);
}
.input-field {
background-color: #1a1a2e;
border: 1px solid #2d3748;
color: #fff;
transition: border-color 0.3s;
}
.input-field:focus {
border-color: #d4a574;
outline: none;
}
.input-field::placeholder {
color: #6b7280;
}
</style> </style>
</head> </head>
<body class="flex items-center justify-center p-4"> <body class="gradient-bg min-h-screen flex items-center justify-center p-4">
<div class="card p-8 w-full max-w-md"> <div class="w-full max-w-md">
<!-- Logo --> <!-- Logo -->
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-gradient-to-br from-yellow-600 to-yellow-800 mb-4"> <div class="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center mb-4 animate-float">
<span class="text-2xl font-bold text-white">AS</span> <span class="text-4xl">📱</span>
</div> </div>
<h1 class="text-2xl font-bold text-white">Social Media Automation</h1> <h1 class="text-2xl font-bold text-white">Social Media Automation</h1>
<p class="text-gray-400 mt-1">Consultoría AS</p> <p class="text-gray-400 mt-2">Consultoría AS</p>
</div> </div>
<!-- Error message --> <!-- Login Card -->
<div class="card rounded-2xl p-8">
<h2 class="text-xl font-semibold text-white mb-6 text-center">Iniciar Sesión</h2>
{% if error %} {% if error %}
<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-6"> <div class="bg-red-500/20 border border-red-500/50 text-red-400 px-4 py-3 rounded-lg mb-6 text-sm">
{{ error }} {{ error }}
</div> </div>
{% endif %} {% endif %}
<!-- Login Form --> <form method="POST" action="/login" class="space-y-5">
<form method="POST" action="/login"> <div>
<div class="mb-4"> <label class="block text-sm font-medium text-gray-300 mb-2">Usuario o Email</label>
<label for="username" class="block text-gray-400 text-sm mb-2">
Usuario o Email
</label>
<input <input
type="text" type="text"
id="username"
name="username" name="username"
required required
class="input-field w-full px-4 py-3 rounded-lg" class="input-field w-full px-4 py-3 rounded-xl text-white placeholder-gray-500 focus:outline-none transition-all"
placeholder="tu.usuario" placeholder="tu@email.com"
autocomplete="username" autocomplete="username"
> >
</div> </div>
<div class="mb-6"> <div>
<label for="password" class="block text-gray-400 text-sm mb-2"> <label class="block text-sm font-medium text-gray-300 mb-2">Contraseña</label>
Contraseña
</label>
<input <input
type="password" type="password"
id="password"
name="password" name="password"
required required
class="input-field w-full px-4 py-3 rounded-lg" class="input-field w-full px-4 py-3 rounded-xl text-white placeholder-gray-500 focus:outline-none transition-all"
placeholder="••••••••" placeholder="••••••••"
autocomplete="current-password" autocomplete="current-password"
> >
@@ -91,21 +66,17 @@
<button <button
type="submit" type="submit"
class="btn-primary w-full py-3 rounded-lg font-semibold text-lg" class="btn-primary w-full py-3 rounded-xl text-white font-medium transition-all hover:shadow-lg hover:shadow-indigo-500/25"
> >
Iniciar Sesión Entrar
</button> </button>
</form> </form>
</div>
<!-- Footer --> <!-- Footer -->
<div class="mt-8 text-center text-gray-500 text-sm"> <p class="text-center text-gray-500 text-sm mt-8">
<p>Sistema de automatización de redes sociales</p> © 2025 <a href="https://consultoria-as.com" target="_blank" class="text-indigo-400 hover:underline">Consultoría AS</a> - Tijuana, México
<p class="mt-1">
<a href="https://consultoria-as.com" target="_blank" class="accent hover:underline">
consultoria-as.com
</a>
</p> </p>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@@ -1,449 +1,156 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Posts - 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; }
.btn-secondary:hover { background-color: #4b5563; }
.btn-danger { background-color: #dc2626; }
.btn-danger:hover { background-color: #b91c1c; }
.status-published { background-color: #065f46; color: #6ee7b7; }
.status-scheduled { background-color: #1e40af; color: #93c5fd; }
.status-pending { background-color: #92400e; color: #fcd34d; }
.status-draft { background-color: #374151; color: #9ca3af; }
.status-failed { background-color: #991b1b; color: #fca5a5; }
.platform-x { background-color: #1d1d1d; }
.platform-threads { background-color: #1a1a1a; }
.platform-instagram { background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); }
.platform-facebook { background-color: #1877f2; }
</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> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded bg-gray-800 accent">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</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"> {% block title %}Posts{% endblock %}
<!-- Filters -->
<div class="card p-4 mb-6"> {% block content %}
<div class="flex flex-wrap gap-4 items-center"> <div class="animate-fade-in">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Estado</label> <h1 class="text-3xl font-bold">Posts</h1>
<select id="filter-status" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <p class="text-gray-400 mt-1">Gestiona todas tus publicaciones</p>
<option value="">Todos</option> </div>
<a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2 transition-all">
<span>✍️</span>
<span>Crear Post</span>
</a>
</div>
<!-- Filters -->
<div class="card rounded-2xl p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center">
<select id="filter-status" onchange="filterPosts()" 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="published">Publicados</option> <option value="published">Publicados</option>
<option value="scheduled">Programados</option> <option value="scheduled">Programados</option>
<option value="pending_approval">Pendientes</option> <option value="pending_approval">Pendientes</option>
<option value="draft">Borradores</option> <option value="draft">Borradores</option>
<option value="failed">Fallidos</option> <option value="failed">Fallidos</option>
</select> </select>
</div> <select id="filter-platform" onchange="filterPosts()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
<div> <option value="">Todas las plataformas</option>
<label class="text-sm text-gray-400 block mb-1">Plataforma</label>
<select id="filter-platform" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
<option value="">Todas</option>
<option value="x">X (Twitter)</option> <option value="x">X (Twitter)</option>
<option value="threads">Threads</option>
<option value="instagram">Instagram</option>
<option value="facebook">Facebook</option> <option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="threads">Threads</option>
</select> </select>
</div> <input type="text" id="filter-search" placeholder="Buscar..." onkeyup="filterPosts()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white placeholder-gray-500 focus:border-primary focus:outline-none flex-1 min-w-48">
<div>
<label class="text-sm text-gray-400 block mb-1">Tipo</label>
<select id="filter-type" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
<option value="">Todos</option>
<option value="tip">Tips</option>
<option value="product">Productos</option>
<option value="service">Servicios</option>
<option value="engagement">Engagement</option>
</select>
</div>
<div class="flex-1"></div>
<div>
<a href="/dashboard/compose" class="btn-primary px-4 py-2 rounded inline-block">
+ Nuevo Post
</a>
</div>
</div>
</div>
<!-- Stats -->
<div class="grid grid-cols-5 gap-4 mb-6">
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-green-400" id="stat-published">0</div>
<div class="text-gray-400 text-sm">Publicados</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-blue-400" id="stat-scheduled">0</div>
<div class="text-gray-400 text-sm">Programados</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-yellow-400" id="stat-pending">0</div>
<div class="text-gray-400 text-sm">Pendientes</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-gray-400" id="stat-draft">0</div>
<div class="text-gray-400 text-sm">Borradores</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-red-400" id="stat-failed">0</div>
<div class="text-gray-400 text-sm">Fallidos</div>
</div> </div>
</div> </div>
<!-- Posts List --> <!-- Posts List -->
<div class="card"> <div id="posts-container" class="space-y-4">
<div class="p-4 border-b border-gray-700 flex justify-between items-center"> {% for post in posts %}
<h2 class="text-xl font-semibold">Posts</h2> <div class="card rounded-2xl p-6 hover:border-primary/30 transition-all post-item"
<span class="text-gray-400 text-sm" id="posts-count">0 posts</span> data-status="{{ post.status }}"
data-platforms="{{ post.platforms|join(',') if post.platforms else '' }}"
data-content="{{ post.content|lower }}">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<p class="text-white mb-3">{{ post.content[:200] }}{% if post.content|length > 200 %}...{% endif %}</p>
<div class="flex flex-wrap items-center gap-2">
{% for platform in post.platforms %}
<span class="text-xs px-2 py-1 rounded-full
{% if platform == 'x' %}bg-gray-700{% endif %}
{% if platform == 'facebook' %}bg-blue-600/30 text-blue-400{% endif %}
{% if platform == 'instagram' %}bg-pink-600/30 text-pink-400{% endif %}
{% if platform == 'threads' %}bg-gray-600{% endif %}
">{{ platform }}</span>
{% endfor %}
<span class="text-xs px-2 py-1 rounded-full
{% if post.status == 'published' %}bg-green-500/20 text-green-400{% endif %}
{% if post.status == 'scheduled' %}bg-blue-500/20 text-blue-400{% endif %}
{% if post.status == 'pending_approval' %}bg-yellow-500/20 text-yellow-400{% endif %}
{% if post.status == 'draft' %}bg-gray-500/20 text-gray-400{% endif %}
{% if post.status == 'failed' %}bg-red-500/20 text-red-400{% endif %}
">{{ post.status }}</span>
</div> </div>
<div id="posts-list" class="divide-y divide-gray-700"> </div>
<!-- Posts loaded dynamically --> <div class="flex flex-col items-end gap-2">
<div class="p-8 text-center text-gray-500"> <span class="text-xs text-gray-500">{{ post.created_at[:16] if post.created_at else '-' }}</span>
Cargando posts... <div class="flex gap-2">
{% if post.status == 'draft' or post.status == 'pending_approval' %}
<button onclick="publishPost({{ post.id }})" class="text-xs bg-green-500/20 text-green-400 px-3 py-1 rounded-lg hover:bg-green-500/30 transition-colors">
Publicar
</button>
{% endif %}
<button onclick="deletePost({{ post.id }})" class="text-xs bg-red-500/20 text-red-400 px-3 py-1 rounded-lg hover:bg-red-500/30 transition-colors">
Eliminar
</button>
</div> </div>
</div> </div>
</div> </div>
</div>
{% else %}
<div class="text-center py-16 text-gray-500">
<span class="text-6xl mb-4 block">📭</span>
<p class="text-xl">No hay posts</p>
<p class="mt-2">Crea tu primera publicación</p>
<a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium inline-flex items-center gap-2 mt-4">
<span>✍️</span>
<span>Crear Post</span>
</a>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- Pagination --> {% block extra_scripts %}
<div class="flex justify-center gap-2 mt-6" id="pagination"> <script>
<!-- Pagination buttons -->
</div>
</main>
<!-- Edit Modal -->
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="card p-6 max-w-2xl w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Editar Post</h3>
<button onclick="closeEditModal()" class="text-gray-400 hover:text-white"></button>
</div>
<form id="edit-form">
<input type="hidden" id="edit-post-id">
<div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">Contenido</label>
<textarea id="edit-content" rows="4" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
<div class="text-right text-sm text-gray-500 mt-1">
<span id="edit-char-count">0</span> caracteres
</div>
</div>
<div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">Programar para</label>
<input type="datetime-local" id="edit-scheduled" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeEditModal()" 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 allPosts = [];
let currentPage = 1;
const postsPerPage = 20;
// Load posts on page load
window.addEventListener('load', loadPosts);
async function loadPosts() {
try {
const response = await fetch('/api/posts/');
allPosts = await response.json();
updateStats();
filterPosts();
} catch (error) {
console.error('Error loading posts:', error);
document.getElementById('posts-list').innerHTML =
'<div class="p-8 text-center text-red-400">Error cargando posts</div>';
}
}
function updateStats() {
const stats = {
published: 0,
scheduled: 0,
pending_approval: 0,
draft: 0,
failed: 0
};
allPosts.forEach(post => {
if (stats[post.status] !== undefined) {
stats[post.status]++;
}
});
document.getElementById('stat-published').textContent = stats.published;
document.getElementById('stat-scheduled').textContent = stats.scheduled;
document.getElementById('stat-pending').textContent = stats.pending_approval;
document.getElementById('stat-draft').textContent = stats.draft;
document.getElementById('stat-failed').textContent = stats.failed;
}
function filterPosts() { function filterPosts() {
const status = document.getElementById('filter-status').value; const status = document.getElementById('filter-status').value;
const platform = document.getElementById('filter-platform').value; const platform = document.getElementById('filter-platform').value;
const type = document.getElementById('filter-type').value; const search = document.getElementById('filter-search').value.toLowerCase();
let filtered = allPosts; document.querySelectorAll('.post-item').forEach(post => {
const postStatus = post.dataset.status;
const postPlatforms = post.dataset.platforms;
const postContent = post.dataset.content;
if (status) { const matchesStatus = !status || postStatus === status;
filtered = filtered.filter(p => p.status === status); const matchesPlatform = !platform || postPlatforms.includes(platform);
} const matchesSearch = !search || postContent.includes(search);
if (platform) {
filtered = filtered.filter(p => p.platforms && p.platforms.includes(platform));
}
if (type) {
filtered = filtered.filter(p => p.content_type === type);
}
renderPosts(filtered); post.style.display = matchesStatus && matchesPlatform && matchesSearch ? 'block' : 'none';
}
function renderPosts(posts) {
const container = document.getElementById('posts-list');
document.getElementById('posts-count').textContent = `${posts.length} posts`;
if (posts.length === 0) {
container.innerHTML = '<div class="p-8 text-center text-gray-500">No hay posts</div>';
return;
}
// Pagination
const start = (currentPage - 1) * postsPerPage;
const pagePosts = posts.slice(start, start + postsPerPage);
container.innerHTML = pagePosts.map(post => `
<div class="p-4 hover:bg-gray-800 transition-colors" data-post-id="${post.id}">
<div class="flex gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="status-${post.status} px-2 py-1 rounded text-xs font-medium">
${getStatusLabel(post.status)}
</span>
${post.platforms ? post.platforms.map(p => `
<span class="platform-${p} px-2 py-1 rounded text-xs">${p}</span>
`).join('') : ''}
<span class="text-gray-500 text-xs">${post.content_type || 'general'}</span>
</div>
<p class="text-gray-300 mb-2">${escapeHtml(post.content || '').substring(0, 200)}${post.content && post.content.length > 200 ? '...' : ''}</p>
<div class="text-gray-500 text-xs">
${post.scheduled_at ? `Programado: ${formatDate(post.scheduled_at)}` : ''}
${post.published_at ? `Publicado: ${formatDate(post.published_at)}` : ''}
</div>
</div>
<div class="flex flex-col gap-2">
${post.status === 'pending_approval' ? `
<button onclick="approvePost(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Aprobar</button>
<button onclick="rejectPost(${post.id})" class="btn-danger px-3 py-1 rounded text-sm">Rechazar</button>
` : ''}
${post.status === 'scheduled' ? `
<button onclick="publishNow(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Publicar Ya</button>
<button onclick="cancelPost(${post.id})" class="btn-secondary px-3 py-1 rounded text-sm">Cancelar</button>
` : ''}
${post.status === 'draft' ? `
<button onclick="schedulePost(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Programar</button>
` : ''}
<button onclick="editPost(${post.id})" class="btn-secondary px-3 py-1 rounded text-sm">Editar</button>
<button onclick="deletePost(${post.id})" class="text-red-400 hover:text-red-300 text-sm">Eliminar</button>
</div>
</div>
</div>
`).join('');
// Render pagination
renderPagination(posts.length);
}
function renderPagination(totalPosts) {
const totalPages = Math.ceil(totalPosts / postsPerPage);
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= totalPages; i++) {
html += `<button onclick="goToPage(${i})" class="${i === currentPage ? 'btn-primary' : 'btn-secondary'} px-3 py-1 rounded">${i}</button>`;
}
container.innerHTML = html;
}
function goToPage(page) {
currentPage = page;
filterPosts();
}
function getStatusLabel(status) {
const labels = {
'published': 'Publicado',
'scheduled': 'Programado',
'pending_approval': 'Pendiente',
'draft': 'Borrador',
'failed': 'Fallido'
};
return labels[status] || status;
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString('es-MX', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit'
}); });
} }
function escapeHtml(text) { async function publishPost(id) {
const div = document.createElement('div'); if (!confirm('¿Publicar este post ahora?')) return;
div.textContent = text;
return div.innerHTML;
}
// Actions
async function approvePost(id) {
if (!confirm('¿Aprobar este post para publicación?')) return;
showModal('Publicando...', true);
try { try {
await fetch(`/api/posts/${id}/approve`, { method: 'POST' }); const response = await fetch(`/api/posts/${id}/publish`, { method: 'POST' });
loadPosts();
} catch (error) {
alert('Error al aprobar: ' + error.message);
}
}
async function rejectPost(id) {
if (!confirm('¿Rechazar este post?')) return;
try {
await fetch(`/api/posts/${id}/reject`, { method: 'POST' });
loadPosts();
} catch (error) {
alert('Error al rechazar: ' + error.message);
}
}
async function publishNow(id) {
if (!confirm('¿Publicar inmediatamente?')) return;
try {
await fetch(`/api/calendar/posts/${id}/publish-now`, { method: 'POST' });
alert('Post enviado a publicación');
loadPosts();
} catch (error) {
alert('Error: ' + error.message);
}
}
async function cancelPost(id) {
if (!confirm('¿Cancelar programación?')) return;
try {
await fetch(`/api/calendar/posts/${id}/cancel`, { method: 'POST' });
loadPosts();
} catch (error) {
alert('Error: ' + error.message);
}
}
async function schedulePost(id) {
try {
const response = await fetch(`/api/calendar/posts/${id}/schedule?auto=true`, { method: 'POST' });
const data = await response.json(); const data = await response.json();
alert(`Programado para: ${formatDate(data.scheduled_at)}`);
loadPosts(); if (data.success) {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p>¡Publicado!</p></div>');
setTimeout(() => location.reload(), 1500);
} else {
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>${data.error || 'Error'}</p></div>`);
}
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
function editPost(id) {
const post = allPosts.find(p => p.id === id);
if (!post) return;
document.getElementById('edit-post-id').value = id;
document.getElementById('edit-content').value = post.content || '';
document.getElementById('edit-char-count').textContent = (post.content || '').length;
if (post.scheduled_at) {
const date = new Date(post.scheduled_at);
document.getElementById('edit-scheduled').value = date.toISOString().slice(0, 16);
}
document.getElementById('edit-modal').classList.remove('hidden');
document.getElementById('edit-modal').classList.add('flex');
}
function closeEditModal() {
document.getElementById('edit-modal').classList.add('hidden');
document.getElementById('edit-modal').classList.remove('flex');
}
document.getElementById('edit-content').addEventListener('input', function() {
document.getElementById('edit-char-count').textContent = this.value.length;
});
document.getElementById('edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
const id = document.getElementById('edit-post-id').value;
const content = document.getElementById('edit-content').value;
const scheduled = document.getElementById('edit-scheduled').value;
try {
await fetch(`/api/posts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content,
scheduled_at: scheduled || null
})
});
closeEditModal();
loadPosts();
} catch (error) {
alert('Error al guardar: ' + error.message);
}
});
async function deletePost(id) { async function deletePost(id) {
if (!confirm('¿Eliminar este post permanentemente?')) return; if (!confirm('¿Eliminar este post?')) return;
showModal('Eliminando...', true);
try { try {
await fetch(`/api/posts/${id}`, { method: 'DELETE' }); const response = await fetch(`/api/posts/${id}`, { method: 'DELETE' });
loadPosts();
if (response.ok) {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">🗑️</span><p>Eliminado</p></div>');
setTimeout(() => location.reload(), 1000);
} else {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando</p></div>');
}
} catch (error) { } catch (error) {
alert('Error al eliminar: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@@ -1,65 +1,53 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Productos - 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; }
.btn-secondary:hover { background-color: #4b5563; }
.btn-danger { background-color: #dc2626; }
.btn-danger:hover { background-color: #b91c1c; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 50; }
.modal.active { display: flex; justify-content: center; align-items: center; }
</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> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/products" class="px-4 py-2 rounded bg-gray-800 accent">Productos</a>
<a href="/dashboard/services" class="px-4 py-2 rounded hover:bg-gray-800">Servicios</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</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"> {% block title %}Productos{% endblock %}
<!-- Header & Actions -->
<div class="flex justify-between items-center mb-6"> {% block content %}
<h2 class="text-xl font-bold">Catálogo de Productos</h2> <div class="animate-fade-in">
<button onclick="openModal('add')" class="btn-primary px-4 py-2 rounded"> <!-- Header -->
+ Agregar Producto <div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Productos</h1>
<p class="text-gray-400 mt-1">Catálogo de productos para generar contenido</p>
</div>
<button onclick="openModal('add')" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
<span></span>
<span>Agregar Producto</span>
</button> </button>
</div> </div>
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-primary" id="stat-total">0</p>
<p class="text-gray-400 text-sm mt-1">Total Productos</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-green-400" id="stat-active">0</p>
<p class="text-gray-400 text-sm mt-1">Activos</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-yellow-400" id="stat-featured">0</p>
<p class="text-gray-400 text-sm mt-1">Destacados</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-blue-400" id="stat-categories">0</p>
<p class="text-gray-400 text-sm mt-1">Categorías</p>
</div>
</div>
<!-- Filters --> <!-- Filters -->
<div class="card p-4 mb-6"> <div class="card rounded-2xl p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center">
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Categoría</label> <label class="text-sm text-gray-400 block mb-1">Categoría</label>
<select id="filter-category" onchange="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-category" onchange="loadProducts()" 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</option> <option value="">Todas</option>
</select> </select>
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Estado</label> <label class="text-sm text-gray-400 block mb-1">Estado</label>
<select id="filter-active" onchange="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-active" onchange="loadProducts()" 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</option> <option value="">Todos</option>
<option value="true">Activos</option> <option value="true">Activos</option>
<option value="false">Inactivos</option> <option value="false">Inactivos</option>
@@ -67,7 +55,7 @@
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Destacados</label> <label class="text-sm text-gray-400 block mb-1">Destacados</label>
<select id="filter-featured" onchange="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-featured" onchange="loadProducts()" 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</option> <option value="">Todos</option>
<option value="true">Destacados</option> <option value="true">Destacados</option>
</select> </select>
@@ -75,53 +63,33 @@
<div class="flex-1"> <div class="flex-1">
<label class="text-sm text-gray-400 block mb-1">Buscar</label> <label class="text-sm text-gray-400 block mb-1">Buscar</label>
<input type="text" id="filter-search" onkeyup="loadProducts()" placeholder="Nombre del producto..." <input type="text" id="filter-search" onkeyup="loadProducts()" placeholder="Nombre del producto..."
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 w-full"> class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white w-full focus:border-primary focus:outline-none">
</div> </div>
</div> </div>
</div> </div>
<!-- Stats -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="card p-4 text-center">
<div class="text-2xl font-bold accent" id="stat-total">0</div>
<div class="text-gray-400 text-sm">Total Productos</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-green-400" id="stat-active">0</div>
<div class="text-gray-400 text-sm">Activos</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-yellow-400" id="stat-featured">0</div>
<div class="text-gray-400 text-sm">Destacados</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-blue-400" id="stat-categories">0</div>
<div class="text-gray-400 text-sm">Categorías</div>
</div>
</div>
<!-- Products Grid --> <!-- Products Grid -->
<div id="products-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="products-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Products loaded dynamically --> <!-- Products loaded dynamically -->
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div id="empty-state" class="card p-12 text-center hidden"> <div id="empty-state" class="card rounded-2xl p-12 text-center hidden">
<div class="text-6xl mb-4">📦</div> <div class="text-6xl mb-4">📦</div>
<h3 class="text-xl font-bold mb-2">No hay productos</h3> <h3 class="text-xl font-bold mb-2">No hay productos</h3>
<p class="text-gray-400 mb-4">Agrega tu primer producto para empezar a generar contenido</p> <p class="text-gray-400 mb-4">Agrega tu primer producto para empezar a generar contenido</p>
<button onclick="openModal('add')" class="btn-primary px-6 py-2 rounded"> <button onclick="openModal('add')" class="btn-primary px-6 py-3 rounded-xl">
+ Agregar Producto + Agregar Producto
</button> </button>
</div> </div>
</main> </div>
<!-- Add/Edit Modal --> <!-- Add/Edit Modal -->
<div id="product-modal" class="modal"> <div id="product-modal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
<div class="card p-6 w-full max-w-2xl max-h-screen overflow-y-auto"> <div class="card rounded-2xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold" id="modal-title">Agregar Producto</h3> <h3 class="text-xl font-semibold" id="modal-title">Agregar Producto</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-2xl">&times;</button> <button onclick="closeModal()" class="text-gray-400 hover:text-white text-xl"></button>
</div> </div>
<form id="product-form" onsubmit="saveProduct(event)"> <form id="product-form" onsubmit="saveProduct(event)">
@@ -129,14 +97,14 @@
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Nombre *</label> <label class="text-sm text-gray-400 block mb-2">Nombre *</label>
<input type="text" id="product-name" required <input type="text" id="product-name" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Categoría *</label> <label class="text-sm text-gray-400 block mb-2">Categoría *</label>
<select id="product-category" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="product-category" 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="laptops">Laptops</option> <option value="laptops">Laptops</option>
<option value="desktops">Equipos de Escritorio</option> <option value="desktops">Equipos de Escritorio</option>
<option value="impresoras_3d">Impresoras 3D</option> <option value="impresoras_3d">Impresoras 3D</option>
@@ -149,70 +117,65 @@
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Precio (MXN) *</label> <label class="text-sm text-gray-400 block mb-2">Precio (MXN) *</label>
<input type="number" id="product-price" step="0.01" min="0" required <input type="number" id="product-price" step="0.01" min="0" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Descripción</label> <label class="text-sm text-gray-400 block mb-2">Descripción</label>
<textarea id="product-description" rows="3" <textarea id="product-description" rows="3"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> 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>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Stock</label> <label class="text-sm text-gray-400 block mb-2">Stock</label>
<input type="number" id="product-stock" min="0" value="0" <input type="number" id="product-stock" min="0" value="0"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">URL Imagen</label> <label class="text-sm text-gray-400 block mb-2">URL Imagen</label>
<input type="url" id="product-image" <input type="url" id="product-image"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Puntos Destacados (uno por línea)</label> <label class="text-sm text-gray-400 block mb-2">Puntos Destacados (uno por línea)</label>
<textarea id="product-highlights" rows="3" placeholder="Alta velocidad&#10;Diseño compacto&#10;Garantía 2 años" <textarea id="product-highlights" rows="3" placeholder="Alta velocidad&#10;Diseño compacto&#10;Garantía 2 años"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> 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>
<div class="col-span-2"> <div class="col-span-2 flex items-center gap-6">
<label class="text-sm text-gray-400 block mb-1">Especificaciones (JSON)</label>
<textarea id="product-specs" rows="3" placeholder='{"procesador": "Intel i5", "ram": "16GB"}'
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 font-mono text-sm"></textarea>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="product-active" checked class="w-4 h-4"> <input type="checkbox" id="product-active" checked class="w-4 h-4 rounded">
<span>Activo</span> <span>Activo</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="product-featured" class="w-4 h-4"> <input type="checkbox" id="product-featured" class="w-4 h-4 rounded">
<span>Destacado</span> <span>Destacado</span>
</label> </label>
</div> </div>
</div> </div>
<div class="flex justify-end gap-3 mt-6"> <div class="flex justify-end gap-3 mt-6">
<button type="button" onclick="closeModal()" class="btn-secondary px-4 py-2 rounded"> <button type="button" onclick="closeModal()" class="btn-secondary px-4 py-2 rounded-xl">
Cancelar Cancelar
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded"> <button type="submit" class="btn-primary px-4 py-2 rounded-xl">
Guardar Guardar
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
{% endblock %}
<script> {% block extra_scripts %}
<script>
let products = []; let products = [];
let categories = []; let categories = [];
// Load products on page load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadCategories(); loadCategories();
loadProducts(); loadProducts();
@@ -253,7 +216,6 @@
if (response.ok) { if (response.ok) {
products = await response.json(); products = await response.json();
// Client-side search filter
if (search) { if (search) {
products = products.filter(p => products = products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase()) p.name.toLowerCase().includes(search.toLowerCase())
@@ -288,10 +250,10 @@
empty.classList.add('hidden'); empty.classList.add('hidden');
grid.innerHTML = products.map(product => ` grid.innerHTML = products.map(product => `
<div class="card p-4 ${!product.is_active ? 'opacity-60' : ''}"> <div class="card rounded-2xl p-4 ${!product.is_active ? 'opacity-60' : ''}">
${product.image_url ? ${product.image_url ?
`<img src="${product.image_url}" alt="${product.name}" class="w-full h-40 object-cover rounded mb-4">` : `<img src="${product.image_url}" alt="${product.name}" class="w-full h-40 object-cover rounded-xl mb-4">` :
`<div class="w-full h-40 bg-gray-800 rounded mb-4 flex items-center justify-center text-4xl">📦</div>` `<div class="w-full h-40 bg-dark-800 rounded-xl mb-4 flex items-center justify-center text-4xl">📦</div>`
} }
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
@@ -303,7 +265,7 @@
${product.category.replace('_', ' ')} ${product.category.replace('_', ' ')}
</div> </div>
<div class="text-xl font-bold accent mb-3"> <div class="text-xl font-bold text-primary mb-3">
$${product.price.toLocaleString('es-MX')} MXN $${product.price.toLocaleString('es-MX')} MXN
</div> </div>
@@ -311,23 +273,23 @@
`<p class="text-sm text-gray-400 mb-3 line-clamp-2">${product.description}</p>` : '' `<p class="text-sm text-gray-400 mb-3 line-clamp-2">${product.description}</p>` : ''
} }
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm mb-4">
<span class="${product.stock > 0 ? 'text-green-400' : 'text-red-400'}"> <span class="${product.stock > 0 ? 'text-green-400' : 'text-red-400'}">
${product.stock > 0 ? `${product.stock} en stock` : 'Sin stock'} ${product.stock > 0 ? `${product.stock} en stock` : 'Sin stock'}
</span> </span>
</div> </div>
<div class="flex gap-2 mt-4"> <div class="flex gap-2">
<button onclick="generatePost(${product.id})" <button onclick="generatePost(${product.id})"
class="flex-1 btn-primary px-3 py-2 rounded text-sm"> class="flex-1 btn-primary px-3 py-2 rounded-xl text-sm">
Generar Post Generar Post
</button> </button>
<button onclick="editProduct(${product.id})" <button onclick="editProduct(${product.id})"
class="btn-secondary px-3 py-2 rounded text-sm"> class="btn-secondary px-3 py-2 rounded-xl text-sm">
Editar Editar
</button> </button>
<button onclick="deleteProduct(${product.id})" <button onclick="deleteProduct(${product.id})"
class="btn-danger px-3 py-2 rounded text-sm"> class="bg-red-500/20 text-red-400 px-3 py-2 rounded-xl text-sm hover:bg-red-500/30 transition-colors">
🗑 🗑
</button> </button>
</div> </div>
@@ -341,11 +303,11 @@
document.getElementById('product-form').reset(); document.getElementById('product-form').reset();
document.getElementById('product-id').value = ''; document.getElementById('product-id').value = '';
document.getElementById('product-active').checked = true; document.getElementById('product-active').checked = true;
document.getElementById('product-modal').classList.add('active'); document.getElementById('product-modal').classList.remove('hidden');
} }
function closeModal() { function closeModal() {
document.getElementById('product-modal').classList.remove('active'); document.getElementById('product-modal').classList.add('hidden');
} }
function editProduct(id) { function editProduct(id) {
@@ -361,11 +323,10 @@
document.getElementById('product-stock').value = product.stock || 0; document.getElementById('product-stock').value = product.stock || 0;
document.getElementById('product-image').value = product.image_url || ''; document.getElementById('product-image').value = product.image_url || '';
document.getElementById('product-highlights').value = (product.highlights || []).join('\n'); document.getElementById('product-highlights').value = (product.highlights || []).join('\n');
document.getElementById('product-specs').value = product.specs ? JSON.stringify(product.specs, null, 2) : '';
document.getElementById('product-active').checked = product.is_active; document.getElementById('product-active').checked = product.is_active;
document.getElementById('product-featured').checked = product.is_featured; document.getElementById('product-featured').checked = product.is_featured;
document.getElementById('product-modal').classList.add('active'); document.getElementById('product-modal').classList.remove('hidden');
} }
async function saveProduct(event) { async function saveProduct(event) {
@@ -373,17 +334,6 @@
const id = document.getElementById('product-id').value; const id = document.getElementById('product-id').value;
const highlightsText = document.getElementById('product-highlights').value; const highlightsText = document.getElementById('product-highlights').value;
const specsText = document.getElementById('product-specs').value;
let specs = {};
if (specsText) {
try {
specs = JSON.parse(specsText);
} catch (e) {
alert('JSON de especificaciones inválido');
return;
}
}
const data = { const data = {
name: document.getElementById('product-name').value, name: document.getElementById('product-name').value,
@@ -393,7 +343,6 @@
stock: parseInt(document.getElementById('product-stock').value) || 0, stock: parseInt(document.getElementById('product-stock').value) || 0,
image_url: document.getElementById('product-image').value || null, image_url: document.getElementById('product-image').value || null,
highlights: highlightsText ? highlightsText.split('\n').filter(h => h.trim()) : [], highlights: highlightsText ? highlightsText.split('\n').filter(h => h.trim()) : [],
specs: specs,
is_active: document.getElementById('product-active').checked, is_active: document.getElementById('product-active').checked,
is_featured: document.getElementById('product-featured').checked is_featured: document.getElementById('product-featured').checked
}; };
@@ -411,13 +360,14 @@
if (response.ok) { if (response.ok) {
closeModal(); closeModal();
loadProducts(); loadProducts();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Producto guardado</p></div>');
} else { } else {
const error = await response.json(); const error = await response.json();
alert(error.detail || 'Error al guardar'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>' + (error.detail || 'Error al guardar') + '</p></div>');
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Error de conexión'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
@@ -441,6 +391,8 @@
const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x'); const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x');
if (!platform) return; if (!platform) return;
showModal('Generando post con IA...', true);
try { try {
const response = await fetch('/api/generate/product', { const response = await fetch('/api/generate/product', {
method: 'POST', method: 'POST',
@@ -453,15 +405,22 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
alert('Post generado:\n\n' + data.content); showModal(`<div class="text-left"><p class="font-semibold mb-2">Post generado:</p><p class="bg-dark-800 p-4 rounded-xl text-sm whitespace-pre-wrap">${data.content}</p></div>`);
} else { } else {
alert('Error al generar post'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al generar post</p></div>');
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Error de conexión'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
</script> </script>
</body> <style>
</html> .line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
{% endblock %}

View File

@@ -1,66 +1,53 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Servicios - 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; }
.btn-secondary:hover { background-color: #4b5563; }
.btn-danger { background-color: #dc2626; }
.btn-danger:hover { background-color: #b91c1c; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 50; }
.modal.active { display: flex; justify-content: center; align-items: center; }
.tag { background-color: #374151; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
</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> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/products" class="px-4 py-2 rounded hover:bg-gray-800">Productos</a>
<a href="/dashboard/services" class="px-4 py-2 rounded bg-gray-800 accent">Servicios</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</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"> {% block title %}Servicios{% endblock %}
<!-- Header & Actions -->
<div class="flex justify-between items-center mb-6"> {% block content %}
<h2 class="text-xl font-bold">Catálogo de Servicios</h2> <div class="animate-fade-in">
<button onclick="openModal('add')" class="btn-primary px-4 py-2 rounded"> <!-- Header -->
+ Agregar Servicio <div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Servicios</h1>
<p class="text-gray-400 mt-1">Catálogo de servicios para generar contenido</p>
</div>
<button onclick="openModal('add')" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
<span></span>
<span>Agregar Servicio</span>
</button> </button>
</div> </div>
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-primary" id="stat-total">0</p>
<p class="text-gray-400 text-sm mt-1">Total Servicios</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-green-400" id="stat-active">0</p>
<p class="text-gray-400 text-sm mt-1">Activos</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-yellow-400" id="stat-featured">0</p>
<p class="text-gray-400 text-sm mt-1">Destacados</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<p class="text-3xl font-bold text-blue-400" id="stat-categories">0</p>
<p class="text-gray-400 text-sm mt-1">Categorías</p>
</div>
</div>
<!-- Filters --> <!-- Filters -->
<div class="card p-4 mb-6"> <div class="card rounded-2xl p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center">
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Categoría</label> <label class="text-sm text-gray-400 block mb-1">Categoría</label>
<select id="filter-category" onchange="loadServices()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-category" onchange="loadServices()" 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</option> <option value="">Todas</option>
</select> </select>
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Estado</label> <label class="text-sm text-gray-400 block mb-1">Estado</label>
<select id="filter-active" onchange="loadServices()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-active" onchange="loadServices()" 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</option> <option value="">Todos</option>
<option value="true">Activos</option> <option value="true">Activos</option>
<option value="false">Inactivos</option> <option value="false">Inactivos</option>
@@ -68,7 +55,7 @@
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">Destacados</label> <label class="text-sm text-gray-400 block mb-1">Destacados</label>
<select id="filter-featured" onchange="loadServices()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="filter-featured" onchange="loadServices()" 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</option> <option value="">Todos</option>
<option value="true">Destacados</option> <option value="true">Destacados</option>
</select> </select>
@@ -76,53 +63,33 @@
<div class="flex-1"> <div class="flex-1">
<label class="text-sm text-gray-400 block mb-1">Buscar</label> <label class="text-sm text-gray-400 block mb-1">Buscar</label>
<input type="text" id="filter-search" onkeyup="loadServices()" placeholder="Nombre del servicio..." <input type="text" id="filter-search" onkeyup="loadServices()" placeholder="Nombre del servicio..."
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 w-full"> class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white w-full focus:border-primary focus:outline-none">
</div> </div>
</div> </div>
</div> </div>
<!-- Stats -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="card p-4 text-center">
<div class="text-2xl font-bold accent" id="stat-total">0</div>
<div class="text-gray-400 text-sm">Total Servicios</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-green-400" id="stat-active">0</div>
<div class="text-gray-400 text-sm">Activos</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-yellow-400" id="stat-featured">0</div>
<div class="text-gray-400 text-sm">Destacados</div>
</div>
<div class="card p-4 text-center">
<div class="text-2xl font-bold text-blue-400" id="stat-categories">0</div>
<div class="text-gray-400 text-sm">Categorías</div>
</div>
</div>
<!-- Services Grid --> <!-- Services Grid -->
<div id="services-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="services-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Services loaded dynamically --> <!-- Services loaded dynamically -->
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div id="empty-state" class="card p-12 text-center hidden"> <div id="empty-state" class="card rounded-2xl p-12 text-center hidden">
<div class="text-6xl mb-4">🛠️</div> <div class="text-6xl mb-4">🛠️</div>
<h3 class="text-xl font-bold mb-2">No hay servicios</h3> <h3 class="text-xl font-bold mb-2">No hay servicios</h3>
<p class="text-gray-400 mb-4">Agrega tu primer servicio para empezar a generar contenido</p> <p class="text-gray-400 mb-4">Agrega tu primer servicio para empezar a generar contenido</p>
<button onclick="openModal('add')" class="btn-primary px-6 py-2 rounded"> <button onclick="openModal('add')" class="btn-primary px-6 py-3 rounded-xl">
+ Agregar Servicio + Agregar Servicio
</button> </button>
</div> </div>
</main> </div>
<!-- Add/Edit Modal --> <!-- Add/Edit Modal -->
<div id="service-modal" class="modal"> <div id="service-modal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden flex items-center justify-center z-50">
<div class="card p-6 w-full max-w-2xl max-h-screen overflow-y-auto"> <div class="card rounded-2xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold" id="modal-title">Agregar Servicio</h3> <h3 class="text-xl font-semibold" id="modal-title">Agregar Servicio</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-2xl">&times;</button> <button onclick="closeModal()" class="text-gray-400 hover:text-white text-xl"></button>
</div> </div>
<form id="service-form" onsubmit="saveService(event)"> <form id="service-form" onsubmit="saveService(event)">
@@ -130,14 +97,14 @@
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Nombre *</label> <label class="text-sm text-gray-400 block mb-2">Nombre *</label>
<input type="text" id="service-name" required <input type="text" id="service-name" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Categoría *</label> <label class="text-sm text-gray-400 block mb-2">Categoría *</label>
<select id="service-category" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> <select id="service-category" 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="ai_automation">Automatización con IA</option> <option value="ai_automation">Automatización con IA</option>
<option value="consulting">Consultoría TI</option> <option value="consulting">Consultoría TI</option>
<option value="development">Desarrollo de Software</option> <option value="development">Desarrollo de Software</option>
@@ -150,64 +117,74 @@
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Descripción</label> <label class="text-sm text-gray-400 block mb-2">Descripción</label>
<textarea id="service-description" rows="3" <textarea id="service-description" rows="3"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> 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>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Sectores Objetivo (uno por línea)</label> <label class="text-sm text-gray-400 block mb-2">Sectores Objetivo (uno por línea)</label>
<textarea id="service-sectors" rows="3" placeholder="Retail&#10;Manufactura&#10;Servicios" <textarea id="service-sectors" rows="3" placeholder="Retail&#10;Manufactura&#10;Servicios"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> 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>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Beneficios (uno por línea)</label> <label class="text-sm text-gray-400 block mb-2">Beneficios (uno por línea)</label>
<textarea id="service-benefits" rows="3" placeholder="Reduce costos operativos&#10;Aumenta productividad&#10;Operación 24/7" <textarea id="service-benefits" rows="3" placeholder="Reduce costos operativos&#10;Aumenta productividad&#10;Operación 24/7"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea> 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>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-sm text-gray-400 block mb-1">Call to Action</label> <label class="text-sm text-gray-400 block mb-2">Call to Action</label>
<input type="text" id="service-cta" placeholder="Agenda una demo gratuita" <input type="text" id="service-cta" placeholder="Agenda una demo gratuita"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div> <div>
<label class="text-sm text-gray-400 block mb-1">URL Imagen</label> <label class="text-sm text-gray-400 block mb-2">URL Imagen</label>
<input type="url" id="service-image" <input type="url" id="service-image"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"> class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-6">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="service-active" checked class="w-4 h-4"> <input type="checkbox" id="service-active" checked class="w-4 h-4 rounded">
<span>Activo</span> <span>Activo</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="service-featured" class="w-4 h-4"> <input type="checkbox" id="service-featured" class="w-4 h-4 rounded">
<span>Destacado</span> <span>Destacado</span>
</label> </label>
</div> </div>
</div> </div>
<div class="flex justify-end gap-3 mt-6"> <div class="flex justify-end gap-3 mt-6">
<button type="button" onclick="closeModal()" class="btn-secondary px-4 py-2 rounded"> <button type="button" onclick="closeModal()" class="btn-secondary px-4 py-2 rounded-xl">
Cancelar Cancelar
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded"> <button type="submit" class="btn-primary px-4 py-2 rounded-xl">
Guardar Guardar
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
{% endblock %}
<script> {% block extra_scripts %}
<style>
.tag { background-color: rgba(99, 102, 241, 0.2); color: #a5b4fc; padding: 2px 8px; border-radius: 6px; font-size: 12px; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
<script>
let services = []; let services = [];
let categories = []; let categories = [];
// Load services on page load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadCategories(); loadCategories();
loadServices(); loadServices();
@@ -246,6 +223,20 @@
return names[cat] || cat.replace('_', ' '); return names[cat] || cat.replace('_', ' ');
} }
function getCategoryIcon(category) {
const icons = {
'ai_automation': '🤖',
'consulting': '💼',
'development': '💻',
'infrastructure': '🖥️',
'support': '🛠️',
'training': '📚',
'3d_printing': '🖨️',
'data_analysis': '📊'
};
return icons[category] || '🛠️';
}
async function loadServices() { async function loadServices() {
const category = document.getElementById('filter-category').value; const category = document.getElementById('filter-category').value;
const active = document.getElementById('filter-active').value; const active = document.getElementById('filter-active').value;
@@ -262,7 +253,6 @@
if (response.ok) { if (response.ok) {
services = await response.json(); services = await response.json();
// Client-side search filter
if (search) { if (search) {
services = services.filter(s => services = services.filter(s =>
s.name.toLowerCase().includes(search.toLowerCase()) s.name.toLowerCase().includes(search.toLowerCase())
@@ -297,12 +287,12 @@
empty.classList.add('hidden'); empty.classList.add('hidden');
grid.innerHTML = services.map(service => ` grid.innerHTML = services.map(service => `
<div class="card p-4 ${!service.is_active ? 'opacity-60' : ''}"> <div class="card rounded-2xl p-4 ${!service.is_active ? 'opacity-60' : ''}">
<div class="flex justify-between items-start mb-3"> <div class="flex justify-between items-start mb-3">
<div class="text-3xl"> <div class="text-4xl">
${getCategoryIcon(service.category)} ${getCategoryIcon(service.category)}
</div> </div>
${service.is_featured ? '<span class="text-yellow-400">⭐</span>' : ''} ${service.is_featured ? '<span class="text-yellow-400 text-xl">⭐</span>' : ''}
</div> </div>
<h4 class="font-bold text-lg mb-2">${service.name}</h4> <h4 class="font-bold text-lg mb-2">${service.name}</h4>
@@ -336,15 +326,15 @@
<div class="flex gap-2 mt-4"> <div class="flex gap-2 mt-4">
<button onclick="generatePost(${service.id})" <button onclick="generatePost(${service.id})"
class="flex-1 btn-primary px-3 py-2 rounded text-sm"> class="flex-1 btn-primary px-3 py-2 rounded-xl text-sm">
Generar Post Generar Post
</button> </button>
<button onclick="editService(${service.id})" <button onclick="editService(${service.id})"
class="btn-secondary px-3 py-2 rounded text-sm"> class="btn-secondary px-3 py-2 rounded-xl text-sm">
Editar Editar
</button> </button>
<button onclick="deleteService(${service.id})" <button onclick="deleteService(${service.id})"
class="btn-danger px-3 py-2 rounded text-sm"> class="bg-red-500/20 text-red-400 px-3 py-2 rounded-xl text-sm hover:bg-red-500/30 transition-colors">
🗑 🗑
</button> </button>
</div> </div>
@@ -352,31 +342,17 @@
`).join(''); `).join('');
} }
function getCategoryIcon(category) {
const icons = {
'ai_automation': '🤖',
'consulting': '💼',
'development': '💻',
'infrastructure': '🖥️',
'support': '🛠️',
'training': '📚',
'3d_printing': '🖨️',
'data_analysis': '📊'
};
return icons[category] || '🛠️';
}
function openModal(mode, serviceId = null) { function openModal(mode, serviceId = null) {
document.getElementById('modal-title').textContent = document.getElementById('modal-title').textContent =
mode === 'add' ? 'Agregar Servicio' : 'Editar Servicio'; mode === 'add' ? 'Agregar Servicio' : 'Editar Servicio';
document.getElementById('service-form').reset(); document.getElementById('service-form').reset();
document.getElementById('service-id').value = ''; document.getElementById('service-id').value = '';
document.getElementById('service-active').checked = true; document.getElementById('service-active').checked = true;
document.getElementById('service-modal').classList.add('active'); document.getElementById('service-modal').classList.remove('hidden');
} }
function closeModal() { function closeModal() {
document.getElementById('service-modal').classList.remove('active'); document.getElementById('service-modal').classList.add('hidden');
} }
function editService(id) { function editService(id) {
@@ -395,7 +371,7 @@
document.getElementById('service-active').checked = service.is_active; document.getElementById('service-active').checked = service.is_active;
document.getElementById('service-featured').checked = service.is_featured; document.getElementById('service-featured').checked = service.is_featured;
document.getElementById('service-modal').classList.add('active'); document.getElementById('service-modal').classList.remove('hidden');
} }
async function saveService(event) { async function saveService(event) {
@@ -430,13 +406,14 @@
if (response.ok) { if (response.ok) {
closeModal(); closeModal();
loadServices(); loadServices();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Servicio guardado</p></div>');
} else { } else {
const error = await response.json(); const error = await response.json();
alert(error.detail || 'Error al guardar'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>' + (error.detail || 'Error al guardar') + '</p></div>');
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Error de conexión'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
@@ -460,6 +437,8 @@
const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x'); const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x');
if (!platform) return; if (!platform) return;
showModal('Generando post con IA...', true);
try { try {
const response = await fetch('/api/generate/service', { const response = await fetch('/api/generate/service', {
method: 'POST', method: 'POST',
@@ -472,15 +451,14 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
alert('Post generado:\n\n' + data.content); showModal(`<div class="text-left"><p class="font-semibold mb-2">Post generado:</p><p class="bg-dark-800 p-4 rounded-xl text-sm whitespace-pre-wrap">${data.content}</p></div>`);
} else { } else {
alert('Error al generar post'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al generar post</p></div>');
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Error de conexión'); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
} }
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@@ -1,422 +1,343 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="es">
<head> {% block title %}Configuración{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>Configuración - Social Media Automation</title> <div class="animate-fade-in">
<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; }
.btn-secondary:hover { background-color: #4b5563; }
.status-ok { color: #10b981; }
.status-error { color: #ef4444; }
.status-warning { color: #f59e0b; }
</style>
</head>
<body class="min-h-screen">
<!-- Header --> <!-- Header -->
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4"> <div class="mb-8">
<div class="flex justify-between items-center"> <h1 class="text-3xl font-bold">Configuración</h1>
<h1 class="text-2xl font-bold"> <p class="text-gray-400 mt-1">Administra las conexiones y preferencias del sistema</p>
<span class="accent">Consultoría AS</span> - Social Media
</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">+ Crear</a>
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
<a href="/dashboard/settings" class="px-4 py-2 rounded bg-gray-800 accent">Config</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 max-w-4xl">
<h2 class="text-2xl font-bold mb-6">Configuración del Sistema</h2>
<!-- API Connections -->
<div class="card p-6 mb-6">
<h3 class="font-semibold text-lg mb-4">Conexiones de APIs</h3>
<div id="api-status" class="space-y-4">
<div class="text-gray-400">Verificando conexiones...</div>
</div>
<button onclick="testAllConnections()" class="btn-secondary px-4 py-2 rounded mt-4">
🔄 Verificar Todas
</button>
</div> </div>
<!-- AI Configuration --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card p-6 mb-6"> <!-- Platform Connections -->
<h3 class="font-semibold text-lg mb-4">Configuración de IA (DeepSeek)</h3> <div class="card rounded-2xl p-6">
<div id="ai-status" class="mb-4"> <h2 class="text-lg font-semibold mb-6 flex items-center gap-2">
<div class="text-gray-400">Verificando...</div> <span>🔗</span>
<span>Conexiones de Plataformas</span>
</h2>
<div class="space-y-4" id="platforms-container">
<div class="text-center py-4 text-gray-500">
<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full mx-auto"></div>
<p class="mt-2">Verificando conexiones...</p>
</div> </div>
<div class="bg-gray-800 rounded-lg p-4 text-sm">
<p class="text-gray-400 mb-2">Para configurar DeepSeek API:</p>
<ol class="list-decimal list-inside space-y-1 text-gray-300">
<li>Visita <a href="https://platform.deepseek.com/" target="_blank" class="text-blue-400 hover:underline">platform.deepseek.com</a></li>
<li>Crea una cuenta y genera una API Key</li>
<li>Agrega al archivo <code class="bg-gray-700 px-1 rounded">.env</code>: <code class="bg-gray-700 px-1 rounded">DEEPSEEK_API_KEY=tu_key</code></li>
<li>Reinicia la aplicación</li>
</ol>
</div> </div>
</div> </div>
<!-- Telegram Notifications --> <!-- Telegram Notifications -->
<div class="card p-6 mb-6"> <div class="card rounded-2xl p-6">
<h3 class="font-semibold text-lg mb-4">Notificaciones Telegram</h3> <h2 class="text-lg font-semibold mb-6 flex items-center gap-2">
<div id="telegram-status" class="mb-4"> <span>📱</span>
<div class="text-gray-400">Verificando...</div> <span>Notificaciones Telegram</span>
</h2>
<div id="telegram-status" class="mb-6">
<div class="text-center py-4 text-gray-500">
<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full mx-auto"></div>
<p class="mt-2">Verificando...</p>
</div> </div>
<div class="flex gap-2"> </div>
<button onclick="testTelegram()" class="btn-secondary px-4 py-2 rounded"> <div class="flex gap-3">
📱 Enviar Prueba <button onclick="testTelegram()" class="btn-primary px-4 py-2 rounded-lg flex items-center gap-2 transition-all">
<span>📤</span>
<span>Enviar Prueba</span>
</button> </button>
<button onclick="showTelegramGuide()" class="btn-secondary px-4 py-2 rounded"> <button onclick="toggleTelegramGuide()" class="btn-secondary px-4 py-2 rounded-lg flex items-center gap-2">
📖 Ver Guía <span>📖</span>
<span>Ver Guía</span>
</button> </button>
</div> </div>
<div id="telegram-guide" class="hidden mt-4 bg-gray-800 rounded-lg p-4 text-sm"> <div id="telegram-guide" class="hidden mt-6 bg-dark-800 rounded-xl p-4 text-sm space-y-3">
<!-- Guide loaded dynamically --> <h4 class="font-medium text-primary">Configurar Telegram Bot</h4>
<ol class="list-decimal list-inside space-y-2 text-gray-400">
<li>Busca <strong>@BotFather</strong> en Telegram</li>
<li>Envía <code class="bg-dark-700 px-1 rounded">/newbot</code> y sigue las instrucciones</li>
<li>Copia el <strong>Bot Token</strong> que te proporciona</li>
<li>Inicia chat con tu nuevo bot</li>
<li>Visita <code class="bg-dark-700 px-1 rounded">api.telegram.org/bot{TOKEN}/getUpdates</code></li>
<li>Copia tu <strong>Chat ID</strong> del resultado</li>
<li>Configura ambos valores en el archivo .env</li>
</ol>
</div>
</div>
<!-- Odoo Integration -->
<div class="card rounded-2xl p-6">
<h2 class="text-lg font-semibold mb-6 flex items-center gap-2">
<span>🏢</span>
<span>Integración Odoo</span>
</h2>
<div id="odoo-status" class="mb-6">
<div class="text-center py-4 text-gray-500">
<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full mx-auto"></div>
<p class="mt-2">Verificando...</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button onclick="syncOdooProducts()" class="btn-secondary px-4 py-2 rounded-lg flex items-center gap-2">
<span>📦</span>
<span>Sync Productos</span>
</button>
<button onclick="syncOdooServices()" class="btn-secondary px-4 py-2 rounded-lg flex items-center gap-2">
<span>🛠️</span>
<span>Sync Servicios</span>
</button>
<button onclick="syncOdooLeads()" class="btn-secondary px-4 py-2 rounded-lg flex items-center gap-2">
<span>🎯</span>
<span>Exportar Leads</span>
</button>
</div>
</div>
<!-- AI Generation -->
<div class="card rounded-2xl p-6">
<h2 class="text-lg font-semibold mb-6 flex items-center gap-2">
<span>🤖</span>
<span>Generación con IA</span>
</h2>
<div id="ai-status" class="mb-6">
<div class="text-center py-4 text-gray-500">
<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full mx-auto"></div>
<p class="mt-2">Verificando...</p>
</div>
</div>
<div class="bg-dark-800/50 rounded-xl p-4">
<h4 class="font-medium mb-2">Información del Negocio</h4>
<div class="space-y-2 text-sm text-gray-400">
<p><strong>Nombre:</strong> <span id="business-name">-</span></p>
<p><strong>Ubicación:</strong> <span id="business-location">-</span></p>
<p><strong>Tono:</strong> <span id="content-tone">-</span></p>
</div>
</div> </div>
</div> </div>
<!-- System Info --> <!-- System Info -->
<div class="card p-6 mb-6"> <div class="card rounded-2xl p-6 lg:col-span-2">
<h3 class="font-semibold text-lg mb-4">Información del Sistema</h3> <h2 class="text-lg font-semibold mb-6 flex items-center gap-2">
<div class="grid grid-cols-2 gap-4 text-sm"> <span></span>
<div> <span>Información del Sistema</span>
<span class="text-gray-400">Versión:</span> </h2>
<span class="ml-2">1.0.0</span> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-dark-800/50 rounded-xl p-4 text-center">
<p class="text-2xl font-bold text-primary">1.0.0</p>
<p class="text-sm text-gray-500">Versión</p>
</div> </div>
<div> <div class="bg-dark-800/50 rounded-xl p-4 text-center">
<span class="text-gray-400">Entorno:</span> <p class="text-2xl font-bold text-green-400" id="system-status"></p>
<span class="ml-2" id="app-env">-</span> <p class="text-sm text-gray-500">Estado</p>
</div> </div>
<div> <div class="bg-dark-800/50 rounded-xl p-4 text-center">
<span class="text-gray-400">Base de datos:</span> <p class="text-2xl font-bold" id="db-status">-</p>
<span class="ml-2" id="db-status">-</span> <p class="text-sm text-gray-500">Base de Datos</p>
</div> </div>
<div> <div class="bg-dark-800/50 rounded-xl p-4 text-center">
<span class="text-gray-400">Redis:</span> <p class="text-2xl font-bold" id="worker-status">-</p>
<span class="ml-2" id="redis-status">-</span> <p class="text-sm text-gray-500">Workers</p>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %}
<!-- Quick Actions --> {% block extra_scripts %}
<div class="card p-6 mb-6"> <script>
<h3 class="font-semibold text-lg mb-4">Acciones Rápidas</h3> // Load all statuses on page load
<div class="grid grid-cols-2 gap-4"> document.addEventListener('DOMContentLoaded', function() {
<button onclick="generateWeekContent()" class="btn-secondary p-4 rounded text-left"> loadPlatformStatus();
<div class="font-semibold">📅 Generar Contenido Semanal</div>
<div class="text-sm text-gray-400">Crear posts para los próximos 7 días</div>
</button>
<button onclick="syncAllInteractions()" class="btn-secondary p-4 rounded text-left">
<div class="font-semibold">🔄 Sincronizar Interacciones</div>
<div class="text-sm text-gray-400">Obtener comentarios y menciones</div>
</button>
<button onclick="viewApiDocs()" class="btn-secondary p-4 rounded text-left">
<div class="font-semibold">📚 Documentación API</div>
<div class="text-sm text-gray-400">Ver endpoints disponibles</div>
</button>
<button onclick="downloadLogs()" class="btn-secondary p-4 rounded text-left">
<div class="font-semibold">📋 Ver Logs</div>
<div class="text-sm text-gray-400">Registro de actividad</div>
</button>
</div>
</div>
<!-- Usage Stats -->
<div class="card p-6">
<h3 class="font-semibold text-lg mb-4">Estadísticas de Uso</h3>
<div id="usage-stats" class="grid grid-cols-4 gap-4">
<div class="text-center">
<div class="text-2xl font-bold accent" id="stat-posts-total">-</div>
<div class="text-sm text-gray-400">Posts Totales</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-400" id="stat-posts-month">-</div>
<div class="text-sm text-gray-400">Este Mes</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-blue-400" id="stat-interactions">-</div>
<div class="text-sm text-gray-400">Interacciones</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400" id="stat-leads">-</div>
<div class="text-sm text-gray-400">Leads</div>
</div>
</div>
</div>
</main>
<!-- Result Modal -->
<div id="result-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="card p-6 max-w-md w-full mx-4">
<div id="result-content"></div>
<button onclick="closeModal()" class="btn-primary w-full mt-4 py-2 rounded">Cerrar</button>
</div>
</div>
<script>
window.addEventListener('load', () => {
loadApiStatus();
loadAiStatus();
loadTelegramStatus(); loadTelegramStatus();
loadSystemInfo(); loadOdooStatus();
loadUsageStats(); loadAIStatus();
loadSystemStatus();
}); });
async function loadApiStatus() { async function loadPlatformStatus() {
const container = document.getElementById('api-status'); const container = document.getElementById('platforms-container');
try { try {
const response = await fetch('/api/publish/test'); const response = await fetch('/api/publish/test');
const data = await response.json(); const data = await response.json();
const platforms = ['x', 'threads', 'facebook', 'instagram']; container.innerHTML = '';
let html = ''; for (const [platform, status] of Object.entries(data)) {
const isConnected = status.configured && status.connected;
for (const platform of platforms) { container.innerHTML += `
const status = data.platforms[platform]; <div class="flex items-center justify-between bg-dark-800/50 rounded-xl p-4">
const icon = status.connected ? '✅' : (status.configured ? '⚠️' : '❌');
const statusClass = status.connected ? 'status-ok' : (status.configured ? 'status-warning' : 'status-error');
const statusText = status.connected ? 'Conectado' : (status.configured ? 'Configurado (error de conexión)' : 'No configurado');
html += `
<div class="flex items-center justify-between p-3 bg-gray-800 rounded-lg">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-xl">${icon}</span> <span class="text-2xl">${getPlatformIcon(platform)}</span>
<div> <div>
<div class="font-medium">${platform.charAt(0).toUpperCase() + platform.slice(1)}</div> <p class="font-medium capitalize">${platform}</p>
<div class="text-sm ${statusClass}">${statusText}</div> ${status.details?.username ? `<p class="text-xs text-gray-500">@${status.details.username}</p>` : ''}
</div> </div>
</div> </div>
${status.details ? ` <span class="px-3 py-1 rounded-full text-sm ${isConnected ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
<div class="text-sm text-gray-400"> ${isConnected ? '✓ Conectado' : '✗ Desconectado'}
${status.details.username ? '@' + status.details.username : ''} </span>
${status.details.name ? status.details.name : ''}
</div>
` : ''}
</div> </div>
`; `;
} }
container.innerHTML = html;
} catch (error) { } catch (error) {
container.innerHTML = '<div class="text-red-400">Error al verificar APIs</div>'; container.innerHTML = '<div class="text-center text-red-400 py-4">Error cargando estado</div>';
}
}
async function loadAiStatus() {
const container = document.getElementById('ai-status');
try {
const response = await fetch('/api/generate/status');
const data = await response.json();
const icon = data.status === 'connected' ? '✅' : (data.configured ? '⚠️' : '❌');
const statusClass = data.status === 'connected' ? 'status-ok' : (data.configured ? 'status-warning' : 'status-error');
const statusText = data.status === 'connected' ? 'Conectado y funcionando' : (data.configured ? 'Error: ' + (data.error || 'desconocido') : 'No configurado');
container.innerHTML = `
<div class="flex items-center gap-3 p-3 bg-gray-800 rounded-lg">
<span class="text-xl">${icon}</span>
<div>
<div class="font-medium">DeepSeek API</div>
<div class="text-sm ${statusClass}">${statusText}</div>
</div>
</div>
`;
} catch (error) {
container.innerHTML = '<div class="text-red-400">Error al verificar IA</div>';
} }
} }
async function loadTelegramStatus() { async function loadTelegramStatus() {
const container = document.getElementById('telegram-status'); const container = document.getElementById('telegram-status');
try { try {
const response = await fetch('/api/notifications/status'); const response = await fetch('/api/notifications/status');
const data = await response.json(); const data = await response.json();
const icon = data.telegram_configured ? '✅' : '❌'; const isConfigured = data.telegram_configured;
const statusClass = data.telegram_configured ? 'status-ok' : 'status-error';
const statusText = data.telegram_configured ? 'Configurado' : 'No configurado';
container.innerHTML = ` container.innerHTML = `
<div class="flex items-center gap-3 p-3 bg-gray-800 rounded-lg"> <div class="flex items-center justify-between bg-dark-800/50 rounded-xl p-4">
<span class="text-xl">${icon}</span> <div class="flex items-center gap-3">
<span class="text-2xl">📱</span>
<div> <div>
<div class="font-medium">Telegram Bot</div> <p class="font-medium">Telegram Bot</p>
<div class="text-sm ${statusClass}">${statusText}</div> <p class="text-xs text-gray-500">${data.bot_token_set ? 'Token configurado' : 'Token no configurado'}</p>
</div> </div>
</div> </div>
<span class="px-3 py-1 rounded-full text-sm ${isConfigured ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
${isConfigured ? '✓ Activo' : '✗ Inactivo'}
</span>
</div>
`; `;
} catch (error) { } catch (error) {
container.innerHTML = '<div class="text-red-400">Error al verificar Telegram</div>'; container.innerHTML = '<div class="text-center text-red-400 py-4">Error verificando Telegram</div>';
} }
} }
async function loadSystemInfo() { async function loadOdooStatus() {
const container = document.getElementById('odoo-status');
try {
const response = await fetch('/api/odoo/status');
const data = await response.json();
container.innerHTML = `
<div class="flex items-center justify-between bg-dark-800/50 rounded-xl p-4">
<div class="flex items-center gap-3">
<span class="text-2xl">🏢</span>
<div>
<p class="font-medium">Odoo ERP</p>
<p class="text-xs text-gray-500">${data.version ? 'v' + data.version : (data.error || 'No configurado')}</p>
</div>
</div>
<span class="px-3 py-1 rounded-full text-sm ${data.connected ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}">
${data.connected ? '✓ Conectado' : '⚠ ' + (data.configured ? 'Error' : 'No configurado')}
</span>
</div>
`;
} catch (error) {
container.innerHTML = '<div class="text-center text-red-400 py-4">Error verificando Odoo</div>';
}
}
async function loadAIStatus() {
const container = document.getElementById('ai-status');
try {
const response = await fetch('/api/generate/status');
const data = await response.json();
document.getElementById('business-name').textContent = data.business_name || '-';
document.getElementById('business-location').textContent = data.business_location || '-';
document.getElementById('content-tone').textContent = data.content_tone || '-';
container.innerHTML = `
<div class="flex items-center justify-between bg-dark-800/50 rounded-xl p-4">
<div class="flex items-center gap-3">
<span class="text-2xl">🤖</span>
<div>
<p class="font-medium">DeepSeek API</p>
<p class="text-xs text-gray-500">${data.provider || 'DeepSeek'}</p>
</div>
</div>
<span class="px-3 py-1 rounded-full text-sm ${data.configured ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
${data.configured ? '✓ Configurado' : '✗ No configurado'}
</span>
</div>
`;
} catch (error) {
container.innerHTML = '<div class="text-center text-red-400 py-4">Error verificando IA</div>';
}
}
async function loadSystemStatus() {
try { try {
const response = await fetch('/api/health'); const response = await fetch('/api/health');
const data = await response.json(); const data = await response.json();
document.getElementById('app-env').textContent = 'Desarrollo'; document.getElementById('system-status').textContent = data.status === 'healthy' ? '● Online' : '● Offline';
document.getElementById('db-status').innerHTML = '<span class="status-ok">Conectada</span>'; document.getElementById('system-status').className = data.status === 'healthy' ? 'text-2xl font-bold text-green-400' : 'text-2xl font-bold text-red-400';
document.getElementById('redis-status').innerHTML = '<span class="status-warning">No verificado</span>'; document.getElementById('db-status').textContent = '✓ OK';
document.getElementById('db-status').className = 'text-2xl font-bold text-green-400';
document.getElementById('worker-status').textContent = '✓ OK';
document.getElementById('worker-status').className = 'text-2xl font-bold text-green-400';
} catch (error) { } catch (error) {
document.getElementById('db-status').innerHTML = '<span class="status-error">Error</span>'; document.getElementById('system-status').textContent = '● Error';
document.getElementById('system-status').className = 'text-2xl font-bold text-red-400';
} }
} }
async function loadUsageStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
document.getElementById('stat-posts-total').textContent = data.posts_month || 0;
document.getElementById('stat-posts-month').textContent = data.posts_week || 0;
document.getElementById('stat-interactions').textContent = data.interactions_pending || 0;
document.getElementById('stat-leads').textContent = 0;
} catch (error) {
console.error('Error loading stats:', error);
}
}
async function testAllConnections() {
showModal('Verificando conexiones...', true);
await loadApiStatus();
await loadAiStatus();
await loadTelegramStatus();
closeModal();
}
async function testTelegram() { async function testTelegram() {
showModal('Enviando mensaje de prueba...', true); showModal('Enviando mensaje de prueba...', true);
try { try {
const response = await fetch('/api/notifications/test', { method: 'POST' }); const response = await fetch('/api/notifications/test', {
const data = await response.json();
if (data.success) {
showModal('✅ Mensaje enviado. Revisa tu Telegram.');
} else {
showModal('❌ Error: ' + (data.detail || 'No se pudo enviar'));
}
} catch (error) {
showModal('❌ Error de conexión');
}
}
async function showTelegramGuide() {
const container = document.getElementById('telegram-guide');
if (!container.classList.contains('hidden')) {
container.classList.add('hidden');
return;
}
try {
const response = await fetch('/api/notifications/setup-guide');
const data = await response.json();
let html = `<h4 class="font-semibold mb-3">${data.title}</h4>`;
data.steps.forEach(step => {
html += `
<div class="mb-3">
<div class="font-medium text-amber-400">Paso ${step.step}: ${step.title}</div>
<ul class="list-disc list-inside text-gray-300 ml-2">
${step.instructions.map(i => `<li>${i}</li>`).join('')}
</ul>
</div>
`;
});
container.innerHTML = html;
container.classList.remove('hidden');
} catch (error) {
container.innerHTML = '<div class="text-red-400">Error al cargar guía</div>';
container.classList.remove('hidden');
}
}
async function generateWeekContent() {
showModal('Generando contenido para la semana...', true);
try {
const response = await fetch('/api/generate/batch', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({})
platforms: ['x', 'threads'],
days: 7
})
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
showModal(`✅ Generados ${data.total_generated} posts para la semana`); showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p class="text-lg">¡Mensaje enviado!</p><p class="text-gray-400 mt-2">Revisa tu Telegram</p></div>');
} else { } else {
showModal('❌ Error: ' + (data.error || 'No se pudo generar')); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p class="text-lg">Error al enviar</p><p class="text-gray-400 mt-2">' + (data.detail || 'Verifica la configuración') + '</p></div>');
} }
} catch (error) { } catch (error) {
showModal('❌ Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p class="text-lg">Error de conexión</p></div>');
} }
} }
async function syncAllInteractions() { function toggleTelegramGuide() {
showModal('Sincronizando interacciones...', true); const guide = document.getElementById('telegram-guide');
guide.classList.toggle('hidden');
}
async function syncOdooProducts() {
showModal('Sincronizando productos...', true);
try { try {
const response = await fetch('/api/interactions/sync', { method: 'POST' }); const response = await fetch('/api/odoo/sync/products', { method: 'POST' });
const data = await response.json(); const data = await response.json();
showModal(`✅ Sincronización completada`); showModal(`<div class="text-center"><span class="text-4xl mb-4 block">📦</span><p class="text-lg">Sincronización completada</p><p class="text-gray-400 mt-2">Creados: ${data.created} | Actualizados: ${data.updated}</p></div>`);
} catch (error) { } catch (error) {
showModal('❌ Error: ' + error.message); showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando</p></div>');
} }
} }
function viewApiDocs() { async function syncOdooServices() {
window.open('/api/docs', '_blank'); showModal('Sincronizando servicios...', true);
try {
const response = await fetch('/api/odoo/sync/services', { method: 'POST' });
const data = await response.json();
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">🛠️</span><p class="text-lg">Sincronización completada</p><p class="text-gray-400 mt-2">Creados: ${data.created} | Actualizados: ${data.updated}</p></div>`);
} catch (error) {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error sincronizando</p></div>');
}
} }
function downloadLogs() { async function syncOdooLeads() {
showModal('Función de logs próximamente disponible'); showModal('Exportando leads...', true);
try {
const response = await fetch('/api/odoo/sync/leads', { method: 'POST' });
const data = await response.json();
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">🎯</span><p class="text-lg">Exportación completada</p><p class="text-gray-400 mt-2">Exportados: ${data.created}</p></div>`);
} catch (error) {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error exportando</p></div>');
}
} }
function showModal(content, loading = false) { function getPlatformIcon(platform) {
const modal = document.getElementById('result-modal'); const icons = { x: '𝕏', facebook: '📘', instagram: '📸', threads: '🧵' };
const contentDiv = document.getElementById('result-content'); return icons[platform] || '📱';
contentDiv.innerHTML = loading
? `<div class="text-center py-4"><div class="text-2xl mb-2">⏳</div>${content}</div>`
: `<div class="text-center py-4">${content}</div>`;
modal.classList.remove('hidden');
modal.classList.add('flex');
} }
</script>
function closeModal() { {% endblock %}
document.getElementById('result-modal').classList.add('hidden');
document.getElementById('result-modal').classList.remove('flex');
}
</script>
</body>
</html>

View File

@@ -16,6 +16,7 @@ services:
- .env - .env
volumes: volumes:
- ./app:/app/app - ./app:/app/app
- ./dashboard:/app/dashboard
- ./templates:/app/templates - ./templates:/app/templates
- ./data:/app/data - ./data:/app/data
- uploaded_images:/app/uploads - uploaded_images:/app/uploads