Files
Consultoría AS e32885afc5 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>
2026-01-28 21:13:58 +00:00

396 lines
17 KiB
HTML

{% extends "base.html" %}
{% block title %}Calendario{% endblock %}
{% block content %}
<div class="animate-fade-in">
<!-- Header -->
<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>
<h2 class="text-xl font-bold px-4 py-2" id="current-month">Enero 2025</h2>
<button onclick="nextMonth()" class="btn-secondary px-4 py-2 rounded-xl flex items-center gap-2">
Siguiente <span></span>
</button>
</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">
<button onclick="setView('month')" id="btn-month" class="btn-primary px-4 py-2 rounded-xl">
Mes
</button>
<button onclick="setView('week')" id="btn-week" class="btn-secondary px-4 py-2 rounded-xl">
Semana
</button>
<button onclick="goToToday()" class="btn-secondary px-4 py-2 rounded-xl">
Hoy
</button>
</div>
<div class="flex gap-4 items-center">
<span class="text-gray-400 text-sm">Filtrar:</span>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="x" onchange="filterCalendar()">
<span class="text-sm bg-dark-700 px-2 py-1 rounded-lg">X</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="facebook" onchange="filterCalendar()">
<span class="text-sm bg-blue-600/30 text-blue-400 px-2 py-1 rounded-lg">Facebook</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="instagram" onchange="filterCalendar()">
<span class="text-sm bg-pink-600/30 text-pink-400 px-2 py-1 rounded-lg">Instagram</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked class="platform-filter w-4 h-4 rounded" value="threads" onchange="filterCalendar()">
<span class="text-sm bg-dark-700 px-2 py-1 rounded-lg">Threads</span>
</label>
</div>
<div class="flex gap-3 text-xs">
<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="w-2 h-2 rounded-full bg-yellow-500"></span> Pendiente</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>
<!-- Calendar Grid -->
<div id="month-view" class="card rounded-2xl overflow-hidden">
<!-- Day Headers -->
<div class="grid grid-cols-7 bg-dark-800">
<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-dark-600">Mar</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-dark-600">Jue</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-dark-600 text-primary">Sáb</div>
<div class="p-3 text-center font-semibold text-primary">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 rounded-2xl hidden">
<div id="week-grid" class="divide-y divide-dark-600">
<!-- Week days filled dynamically -->
</div>
</div>
</div>
<!-- Post Detail Modal -->
<div id="post-modal" class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="card rounded-2xl 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 text-xl"></button>
</div>
<div id="post-modal-content">
<!-- Content loaded dynamically -->
</div>
</div>
</div>
{% endblock %}
{% 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 currentView = 'month';
let calendarData = {};
window.addEventListener('load', () => {
loadCalendarData();
});
async function loadCalendarData() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - (startDate.getDay() || 7) + 1);
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();
}
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 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-dark-600 ${isToday ? 'today' : ''} ${isOtherMonth ? 'other-month' : ''}"
ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="font-semibold text-sm mb-1 ${isToday ? 'text-primary' : ''}">${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);
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-primary/10' : ''}" ondrop="dropPost(event, '${dateKey}')" ondragover="allowDrop(event)">
<div class="flex items-center gap-4 mb-3">
<div class="font-semibold ${isToday ? 'text-primary' : ''}">${days[i]}</div>
<div class="text-gray-400">${date.getDate()}/${date.getMonth() + 1}</div>
${isToday ? '<span class="bg-primary text-white text-xs px-2 py-1 rounded-full">Hoy</span>' : ''}
</div>
<div class="grid grid-cols-4 gap-2">
${dayPosts.map(post => `
<div class="post-item status-${post.status} p-2 rounded-lg"
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-xl' : 'btn-secondary px-4 py-2 rounded-xl';
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('month-view').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();
}
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;
}
function showPostDetail(postId) {
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-lg text-xs">${post.status}</span>
${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>
<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-dark-600">
<a href="/posts" class="btn-secondary px-4 py-2 rounded-xl text-sm">Ver en Posts</a>
${post.status === 'scheduled' ? `
<button onclick="publishNowFromModal(${post.id})" class="btn-primary px-4 py-2 rounded-xl 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();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p>Post enviado a publicación</p></div>');
} catch (error) {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ' + error.message + '</p></div>');
}
}
function formatDateISO(date) {
return date.toISOString().split('T')[0];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}