Files
social-media-automation/dashboard/templates/services.html
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

465 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Servicios{% 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">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>
</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 -->
<div class="card rounded-2xl p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center">
<div>
<label class="text-sm text-gray-400 block mb-1">Categoría</label>
<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>
</select>
</div>
<div>
<label class="text-sm text-gray-400 block mb-1">Estado</label>
<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="true">Activos</option>
<option value="false">Inactivos</option>
</select>
</div>
<div>
<label class="text-sm text-gray-400 block mb-1">Destacados</label>
<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="true">Destacados</option>
</select>
</div>
<div class="flex-1">
<label class="text-sm text-gray-400 block mb-1">Buscar</label>
<input type="text" id="filter-search" onkeyup="loadServices()" placeholder="Nombre del servicio..."
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>
<!-- Services Grid -->
<div id="services-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Services loaded dynamically -->
</div>
<!-- Empty State -->
<div id="empty-state" class="card rounded-2xl p-12 text-center hidden">
<div class="text-6xl mb-4">🛠️</div>
<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>
<button onclick="openModal('add')" class="btn-primary px-6 py-3 rounded-xl">
+ Agregar Servicio
</button>
</div>
</div>
<!-- Add/Edit 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 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">
<h3 class="text-xl font-semibold" id="modal-title">Agregar Servicio</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-xl"></button>
</div>
<form id="service-form" onsubmit="saveService(event)">
<input type="hidden" id="service-id">
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-span-2">
<label class="text-sm text-gray-400 block mb-2">Nombre *</label>
<input type="text" id="service-name" required
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div>
<div class="col-span-2">
<label class="text-sm text-gray-400 block mb-2">Categoría *</label>
<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="consulting">Consultoría TI</option>
<option value="development">Desarrollo de Software</option>
<option value="infrastructure">Infraestructura</option>
<option value="support">Soporte Técnico</option>
<option value="training">Capacitación</option>
<option value="3d_printing">Impresión 3D</option>
<option value="data_analysis">Análisis de Datos</option>
</select>
</div>
<div class="col-span-2">
<label class="text-sm text-gray-400 block mb-2">Descripción</label>
<textarea id="service-description" rows="3"
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="col-span-2">
<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"
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="col-span-2">
<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"
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="col-span-2">
<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"
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div>
<div>
<label class="text-sm text-gray-400 block mb-2">URL Imagen</label>
<input type="url" id="service-image"
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 focus:border-primary focus:outline-none">
</div>
<div class="flex items-center gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="service-active" checked class="w-4 h-4 rounded">
<span>Activo</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="service-featured" class="w-4 h-4 rounded">
<span>Destacado</span>
</label>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button type="button" onclick="closeModal()" class="btn-secondary px-4 py-2 rounded-xl">
Cancelar
</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-xl">
Guardar
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% 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 categories = [];
document.addEventListener('DOMContentLoaded', () => {
loadCategories();
loadServices();
});
async function loadCategories() {
try {
const response = await fetch('/api/services/categories');
if (response.ok) {
categories = await response.json();
const select = document.getElementById('filter-category');
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat;
option.textContent = formatCategory(cat);
select.appendChild(option);
});
document.getElementById('stat-categories').textContent = categories.length;
}
} catch (error) {
console.error('Error loading categories:', error);
}
}
function formatCategory(cat) {
const names = {
'ai_automation': 'Automatización con IA',
'consulting': 'Consultoría TI',
'development': 'Desarrollo de Software',
'infrastructure': 'Infraestructura',
'support': 'Soporte Técnico',
'training': 'Capacitación',
'3d_printing': 'Impresión 3D',
'data_analysis': 'Análisis de Datos'
};
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() {
const category = document.getElementById('filter-category').value;
const active = document.getElementById('filter-active').value;
const featured = document.getElementById('filter-featured').value;
const search = document.getElementById('filter-search').value;
let url = '/api/services/?limit=100';
if (category) url += `&category=${category}`;
if (active) url += `&is_active=${active}`;
if (featured === 'true') url += `&is_featured=true`;
try {
const response = await fetch(url);
if (response.ok) {
services = await response.json();
if (search) {
services = services.filter(s =>
s.name.toLowerCase().includes(search.toLowerCase())
);
}
renderServices();
updateStats();
}
} catch (error) {
console.error('Error loading services:', error);
}
}
function updateStats() {
document.getElementById('stat-total').textContent = services.length;
document.getElementById('stat-active').textContent = services.filter(s => s.is_active).length;
document.getElementById('stat-featured').textContent = services.filter(s => s.is_featured).length;
}
function renderServices() {
const grid = document.getElementById('services-grid');
const empty = document.getElementById('empty-state');
if (services.length === 0) {
grid.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
grid.classList.remove('hidden');
empty.classList.add('hidden');
grid.innerHTML = services.map(service => `
<div class="card rounded-2xl p-4 ${!service.is_active ? 'opacity-60' : ''}">
<div class="flex justify-between items-start mb-3">
<div class="text-4xl">
${getCategoryIcon(service.category)}
</div>
${service.is_featured ? '<span class="text-yellow-400 text-xl">⭐</span>' : ''}
</div>
<h4 class="font-bold text-lg mb-2">${service.name}</h4>
<div class="text-sm text-gray-400 mb-3">
${formatCategory(service.category)}
</div>
${service.description ?
`<p class="text-sm text-gray-400 mb-3 line-clamp-2">${service.description}</p>` : ''
}
${service.target_sectors && service.target_sectors.length ?
`<div class="mb-3">
<div class="text-xs text-gray-500 mb-1">Sectores:</div>
<div class="flex flex-wrap gap-1">
${service.target_sectors.slice(0, 3).map(s => `<span class="tag">${s}</span>`).join('')}
${service.target_sectors.length > 3 ? `<span class="tag">+${service.target_sectors.length - 3}</span>` : ''}
</div>
</div>` : ''
}
${service.benefits && service.benefits.length ?
`<div class="mb-3">
<div class="text-xs text-gray-500 mb-1">Beneficios:</div>
<ul class="text-sm text-gray-400">
${service.benefits.slice(0, 2).map(b => `<li>✓ ${b}</li>`).join('')}
</ul>
</div>` : ''
}
<div class="flex gap-2 mt-4">
<button onclick="generatePost(${service.id})"
class="flex-1 btn-primary px-3 py-2 rounded-xl text-sm">
Generar Post
</button>
<button onclick="editService(${service.id})"
class="btn-secondary px-3 py-2 rounded-xl text-sm">
Editar
</button>
<button onclick="deleteService(${service.id})"
class="bg-red-500/20 text-red-400 px-3 py-2 rounded-xl text-sm hover:bg-red-500/30 transition-colors">
🗑
</button>
</div>
</div>
`).join('');
}
function openModal(mode, serviceId = null) {
document.getElementById('modal-title').textContent =
mode === 'add' ? 'Agregar Servicio' : 'Editar Servicio';
document.getElementById('service-form').reset();
document.getElementById('service-id').value = '';
document.getElementById('service-active').checked = true;
document.getElementById('service-modal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('service-modal').classList.add('hidden');
}
function editService(id) {
const service = services.find(s => s.id === id);
if (!service) return;
document.getElementById('modal-title').textContent = 'Editar Servicio';
document.getElementById('service-id').value = service.id;
document.getElementById('service-name').value = service.name;
document.getElementById('service-category').value = service.category;
document.getElementById('service-description').value = service.description || '';
document.getElementById('service-sectors').value = (service.target_sectors || []).join('\n');
document.getElementById('service-benefits').value = (service.benefits || []).join('\n');
document.getElementById('service-cta').value = service.call_to_action || '';
document.getElementById('service-image').value = service.image_url || '';
document.getElementById('service-active').checked = service.is_active;
document.getElementById('service-featured').checked = service.is_featured;
document.getElementById('service-modal').classList.remove('hidden');
}
async function saveService(event) {
event.preventDefault();
const id = document.getElementById('service-id').value;
const sectorsText = document.getElementById('service-sectors').value;
const benefitsText = document.getElementById('service-benefits').value;
const data = {
name: document.getElementById('service-name').value,
category: document.getElementById('service-category').value,
description: document.getElementById('service-description').value,
target_sectors: sectorsText ? sectorsText.split('\n').filter(s => s.trim()) : [],
benefits: benefitsText ? benefitsText.split('\n').filter(b => b.trim()) : [],
call_to_action: document.getElementById('service-cta').value || null,
image_url: document.getElementById('service-image').value || null,
is_active: document.getElementById('service-active').checked,
is_featured: document.getElementById('service-featured').checked
};
try {
const url = id ? `/api/services/${id}` : '/api/services/';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal();
loadServices();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Servicio guardado</p></div>');
} else {
const error = await response.json();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>' + (error.detail || 'Error al guardar') + '</p></div>');
}
} catch (error) {
console.error('Error:', error);
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
}
}
async function deleteService(id) {
if (!confirm('¿Eliminar este servicio?')) return;
try {
const response = await fetch(`/api/services/${id}`, { method: 'DELETE' });
if (response.ok) {
loadServices();
}
} catch (error) {
console.error('Error:', error);
}
}
async function generatePost(serviceId) {
const service = services.find(s => s.id === serviceId);
if (!service) return;
const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x');
if (!platform) return;
showModal('Generando post con IA...', true);
try {
const response = await fetch('/api/generate/service', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service_id: serviceId,
platform: platform
})
});
if (response.ok) {
const data = await response.json();
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 {
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al generar post</p></div>');
}
} catch (error) {
console.error('Error:', error);
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
}
}
</script>
{% endblock %}