Files
social-media-automation/dashboard/templates/posts.html
Consultoría AS 354270be98 feat(phase-5): Complete dashboard UI templates
- Add posts.html: Post management with filtering by status/platform/type,
  stats display, pagination, edit modal, and actions (approve, reject,
  publish now, schedule, edit, delete)
- Add calendar.html: Visual calendar with month/week views, drag-and-drop
  rescheduling, platform filtering with color-coded status
- Add interactions.html: Interactions management with filtering, detail
  panel for responding, AI response suggestions, lead marking
- Add settings.html: API connection status, DeepSeek config, Telegram
  notifications setup, system info, and quick actions
- Update dashboard.py with settings route

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 02:03:28 +00:00

450 lines
20 KiB
HTML

<!DOCTYPE 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">
<!-- Filters -->
<div class="card p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center">
<div>
<label class="text-sm text-gray-400 block mb-1">Estado</label>
<select id="filter-status" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
<option value="">Todos</option>
<option value="published">Publicados</option>
<option value="scheduled">Programados</option>
<option value="pending_approval">Pendientes</option>
<option value="draft">Borradores</option>
<option value="failed">Fallidos</option>
</select>
</div>
<div>
<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="threads">Threads</option>
<option value="instagram">Instagram</option>
<option value="facebook">Facebook</option>
</select>
</div>
<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>
<!-- Posts List -->
<div class="card">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h2 class="text-xl font-semibold">Posts</h2>
<span class="text-gray-400 text-sm" id="posts-count">0 posts</span>
</div>
<div id="posts-list" class="divide-y divide-gray-700">
<!-- Posts loaded dynamically -->
<div class="p-8 text-center text-gray-500">
Cargando posts...
</div>
</div>
</div>
<!-- Pagination -->
<div class="flex justify-center gap-2 mt-6" id="pagination">
<!-- 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() {
const status = document.getElementById('filter-status').value;
const platform = document.getElementById('filter-platform').value;
const type = document.getElementById('filter-type').value;
let filtered = allPosts;
if (status) {
filtered = filtered.filter(p => p.status === status);
}
if (platform) {
filtered = filtered.filter(p => p.platforms && p.platforms.includes(platform));
}
if (type) {
filtered = filtered.filter(p => p.content_type === type);
}
renderPosts(filtered);
}
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) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Actions
async function approvePost(id) {
if (!confirm('¿Aprobar este post para publicación?')) return;
try {
await fetch(`/api/posts/${id}/approve`, { 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();
alert(`Programado para: ${formatDate(data.scheduled_at)}`);
loadPosts();
} catch (error) {
alert('Error: ' + error.message);
}
}
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) {
if (!confirm('¿Eliminar este post permanentemente?')) return;
try {
await fetch(`/api/posts/${id}`, { method: 'DELETE' });
loadPosts();
} catch (error) {
alert('Error al eliminar: ' + error.message);
}
}
</script>
</body>
</html>