- 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>
437 lines
19 KiB
HTML
437 lines
19 KiB
HTML
<!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>
|