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>
This commit is contained in:
2026-01-28 02:03:28 +00:00
parent edc0e5577b
commit 354270be98
5 changed files with 1744 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
<!DOCTYPE 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">
<!-- Calendar Controls -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-4">
<button onclick="prevMonth()" class="btn-secondary px-3 py-2 rounded">
← Anterior
</button>
<h2 class="text-2xl font-bold" id="current-month">Enero 2025</h2>
<button onclick="nextMonth()" class="btn-secondary px-3 py-2 rounded">
Siguiente →
</button>
</div>
<div class="flex gap-2">
<button onclick="setView('month')" id="btn-month" class="btn-primary px-4 py-2 rounded">
Mes
</button>
<button onclick="setView('week')" id="btn-week" class="btn-secondary px-4 py-2 rounded">
Semana
</button>
<button onclick="goToToday()" class="btn-secondary px-4 py-2 rounded">
Hoy
</button>
</div>
</div>
<!-- Platform Filters -->
<div class="card p-4 mb-6">
<div class="flex gap-4 items-center">
<span class="text-gray-400">Filtrar:</span>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="x" onchange="filterCalendar()">
<span class="bg-gray-800 px-2 py-1 rounded text-sm">X</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="threads" onchange="filterCalendar()">
<span class="bg-gray-800 px-2 py-1 rounded text-sm">Threads</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="instagram" onchange="filterCalendar()">
<span class="bg-pink-900 px-2 py-1 rounded text-sm">Instagram</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter" value="facebook" onchange="filterCalendar()">
<span class="bg-blue-900 px-2 py-1 rounded text-sm">Facebook</span>
</label>
<div class="flex-1"></div>
<div class="flex gap-2 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="post-dot status-pending"></span> Pendiente</span>
<span class="flex items-center gap-1"><span class="post-dot status-published"></span> Publicado</span>
</div>
</div>
</div>
<!-- Calendar Grid -->
<div class="card overflow-hidden">
<!-- Day Headers -->
<div class="grid grid-cols-7 bg-gray-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-gray-700">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-gray-700">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-gray-700 text-blue-400">Sáb</div>
<div class="p-3 text-center font-semibold text-blue-400">Dom</div>
</div>
<!-- Calendar Days -->
<div id="calendar-grid" class="grid grid-cols-7">
<!-- Days filled dynamically -->
</div>
</div>
<!-- Week View (hidden by default) -->
<div id="week-view" class="card hidden mt-6">
<div id="week-grid" class="divide-y divide-gray-700">
<!-- Week days filled dynamically -->
</div>
</div>
</main>
<!-- Post Detail Modal -->
<div id="post-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="card p-6 max-w-lg w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Detalle del Post</h3>
<button onclick="closePostModal()" class="text-gray-400 hover:text-white"></button>
</div>
<div id="post-modal-content">
<!-- Content loaded dynamically -->
</div>
</div>
</div>
<script>
let currentDate = new Date();
let currentView = 'month';
let calendarData = {};
let allPosts = [];
// Initialize
window.addEventListener('load', () => {
loadCalendarData();
});
async function loadCalendarData() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Get first and last day of visible calendar (including overflow days)
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Adjust for Monday start
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1);
const endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (7 - endDate.getDay()) % 7);
try {
const response = await fetch(
`/api/calendar/posts/view?start_date=${formatDateISO(startDate)}&days=42`
);
const data = await response.json();
calendarData = data.calendar || {};
renderCalendar();
} catch (error) {
console.error('Error loading calendar:', error);
}
}
function renderCalendar() {
if (currentView === 'month') {
renderMonthView();
} else {
renderWeekView();
}
// Update month title
const months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
document.getElementById('current-month').textContent =
`${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
}
function renderMonthView() {
const grid = document.getElementById('calendar-grid');
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Start from Monday
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1);
const today = new Date();
today.setHours(0, 0, 0, 0);
const platforms = getSelectedPlatforms();
let html = '';
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
const dateKey = formatDateISO(date);
const isToday = date.getTime() === today.getTime();
const isOtherMonth = date.getMonth() !== month;
const dayPosts = (calendarData[dateKey] || []).filter(post =>
post.platforms && post.platforms.some(p => platforms.includes(p))
);
html += `
<div class="calendar-day p-2 border-r border-b border-gray-700 ${isToday ? 'today' : ''} ${isOtherMonth ? 'other-month' : ''}"
ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="font-semibold text-sm mb-1 ${isToday ? 'accent' : ''}">${date.getDate()}</div>
<div class="space-y-1">
${dayPosts.slice(0, 4).map(post => `
<div class="post-item status-${post.status} truncate"
draggable="true"
ondragstart="dragPost(event, ${post.id})"
onclick="showPostDetail(${post.id})">
${post.platforms ? post.platforms[0].charAt(0).toUpperCase() : '?'}:
${escapeHtml(post.content || '').substring(0, 20)}
</div>
`).join('')}
${dayPosts.length > 4 ? `<div class="text-xs text-gray-400">+${dayPosts.length - 4} más</div>` : ''}
</div>
</div>
`;
}
grid.innerHTML = html;
}
function renderWeekView() {
const container = document.getElementById('week-grid');
const weekStart = new Date(currentDate);
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1); // Monday
const platforms = getSelectedPlatforms();
const days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
const today = new Date();
today.setHours(0, 0, 0, 0);
let html = '';
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(date.getDate() + i);
const dateKey = formatDateISO(date);
const isToday = date.getTime() === today.getTime();
const dayPosts = (calendarData[dateKey] || []).filter(post =>
post.platforms && post.platforms.some(p => platforms.includes(p))
);
html += `
<div class="p-4 ${isToday ? 'bg-gray-800' : ''}" ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="flex items-center gap-4 mb-3">
<div class="font-semibold ${isToday ? 'accent' : ''}">${days[i]}</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>' : ''}
</div>
<div class="grid grid-cols-4 gap-2">
${dayPosts.map(post => `
<div class="post-item status-${post.status} p-2"
draggable="true"
ondragstart="dragPost(event, ${post.id})"
onclick="showPostDetail(${post.id})">
<div class="flex gap-1 mb-1">
${post.platforms ? post.platforms.map(p => `
<span class="text-xs opacity-75">${p}</span>
`).join('') : ''}
</div>
<div class="text-xs truncate">${escapeHtml(post.content || '').substring(0, 50)}</div>
<div class="text-xs text-gray-400 mt-1">
${post.scheduled_at ? new Date(post.scheduled_at).toLocaleTimeString('es-MX', {hour: '2-digit', minute: '2-digit'}) : ''}
</div>
</div>
`).join('') || '<div class="text-gray-500 text-sm">Sin posts</div>'}
</div>
</div>
`;
}
container.innerHTML = html;
}
function setView(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-week').className = view === 'week' ? 'btn-primary px-4 py-2 rounded' : 'btn-secondary px-4 py-2 rounded';
document.getElementById('calendar-grid').parentElement.classList.toggle('hidden', view !== 'month');
document.getElementById('week-view').classList.toggle('hidden', view !== 'week');
renderCalendar();
}
function prevMonth() {
if (currentView === 'month') {
currentDate.setMonth(currentDate.getMonth() - 1);
} else {
currentDate.setDate(currentDate.getDate() - 7);
}
loadCalendarData();
}
function nextMonth() {
if (currentView === 'month') {
currentDate.setMonth(currentDate.getMonth() + 1);
} else {
currentDate.setDate(currentDate.getDate() + 7);
}
loadCalendarData();
}
function goToToday() {
currentDate = new Date();
loadCalendarData();
}
function getSelectedPlatforms() {
return Array.from(document.querySelectorAll('.platform-filter:checked')).map(el => el.value);
}
function filterCalendar() {
renderCalendar();
}
// Drag and drop
let draggedPostId = null;
function dragPost(event, postId) {
draggedPostId = postId;
event.dataTransfer.setData('text/plain', postId);
}
function allowDrop(event) {
event.preventDefault();
}
async function dropPost(event, dateKey) {
event.preventDefault();
if (!draggedPostId) return;
const newDate = new Date(dateKey + 'T12:00:00');
try {
await fetch(`/api/calendar/posts/${draggedPostId}/reschedule?scheduled_at=${newDate.toISOString()}`, {
method: 'POST'
});
loadCalendarData();
} catch (error) {
alert('Error al reprogramar: ' + error.message);
}
draggedPostId = null;
}
// Post detail modal
function showPostDetail(postId) {
// Find post in calendar data
let post = null;
for (const dayPosts of Object.values(calendarData)) {
post = dayPosts.find(p => p.id === postId);
if (post) break;
}
if (!post) return;
const content = `
<div class="space-y-4">
<div>
<span class="status-${post.status} px-2 py-1 rounded 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('') : ''}
</div>
<p class="text-gray-300">${escapeHtml(post.content || '')}</p>
<div class="text-gray-400 text-sm">
${post.scheduled_at ? `Programado: ${new Date(post.scheduled_at).toLocaleString('es-MX')}` : ''}
</div>
<div class="flex gap-2 pt-4 border-t border-gray-700">
<a href="/dashboard/posts" class="btn-secondary px-4 py-2 rounded text-sm">Ver en Posts</a>
${post.status === 'scheduled' ? `
<button onclick="publishNowFromModal(${post.id})" class="btn-primary px-4 py-2 rounded text-sm">Publicar Ahora</button>
` : ''}
</div>
</div>
`;
document.getElementById('post-modal-content').innerHTML = content;
document.getElementById('post-modal').classList.remove('hidden');
document.getElementById('post-modal').classList.add('flex');
}
function closePostModal() {
document.getElementById('post-modal').classList.add('hidden');
document.getElementById('post-modal').classList.remove('flex');
}
async function publishNowFromModal(postId) {
try {
await fetch(`/api/calendar/posts/${postId}/publish-now`, { method: 'POST' });
closePostModal();
loadCalendarData();
alert('Post enviado a publicación');
} catch (error) {
alert('Error: ' + error.message);
}
}
// Utilities
function formatDateISO(date) {
return date.toISOString().split('T')[0];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>