feat: Add missing products and services dashboard templates
- Add products.html with full CRUD, filtering, stats, and AI post generation - Add services.html with full CRUD, filtering, stats, and AI post generation - Both templates follow existing design patterns (Tailwind CSS, dark theme) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
467
dashboard/templates/products.html
Normal file
467
dashboard/templates/products.html
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Productos - 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; }
|
||||||
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 50; }
|
||||||
|
.modal.active { display: flex; justify-content: center; align-items: center; }
|
||||||
|
</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 hover:bg-gray-800">Calendario</a>
|
||||||
|
<a href="/dashboard/products" class="px-4 py-2 rounded bg-gray-800 accent">Productos</a>
|
||||||
|
<a href="/dashboard/services" class="px-4 py-2 rounded hover:bg-gray-800">Servicios</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">
|
||||||
|
<!-- Header & Actions -->
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold">Catálogo de Productos</h2>
|
||||||
|
<button onclick="openModal('add')" class="btn-primary px-4 py-2 rounded">
|
||||||
|
+ Agregar Producto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">Categoría</label>
|
||||||
|
<select id="filter-category" onchange="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<option value="">Todas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Estado</label>
|
||||||
|
<select id="filter-active" onchange="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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="loadProducts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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="loadProducts()" placeholder="Nombre del producto..."
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold accent" id="stat-total">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Total Productos</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-green-400" id="stat-active">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Activos</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-yellow-400" id="stat-featured">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Destacados</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-blue-400" id="stat-categories">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Categorías</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Products Grid -->
|
||||||
|
<div id="products-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Products loaded dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div id="empty-state" class="card p-12 text-center hidden">
|
||||||
|
<div class="text-6xl mb-4">📦</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">No hay productos</h3>
|
||||||
|
<p class="text-gray-400 mb-4">Agrega tu primer producto para empezar a generar contenido</p>
|
||||||
|
<button onclick="openModal('add')" class="btn-primary px-6 py-2 rounded">
|
||||||
|
+ Agregar Producto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<div id="product-modal" class="modal">
|
||||||
|
<div class="card p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold" id="modal-title">Agregar Producto</h3>
|
||||||
|
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-2xl">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="product-form" onsubmit="saveProduct(event)">
|
||||||
|
<input type="hidden" id="product-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-1">Nombre *</label>
|
||||||
|
<input type="text" id="product-name" required
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Categoría *</label>
|
||||||
|
<select id="product-category" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<option value="laptops">Laptops</option>
|
||||||
|
<option value="desktops">Equipos de Escritorio</option>
|
||||||
|
<option value="impresoras_3d">Impresoras 3D</option>
|
||||||
|
<option value="componentes">Componentes</option>
|
||||||
|
<option value="perifericos">Periféricos</option>
|
||||||
|
<option value="redes">Redes</option>
|
||||||
|
<option value="software">Software</option>
|
||||||
|
<option value="accesorios">Accesorios</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Precio (MXN) *</label>
|
||||||
|
<input type="number" id="product-price" step="0.01" min="0" required
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Descripción</label>
|
||||||
|
<textarea id="product-description" rows="3"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Stock</label>
|
||||||
|
<input type="number" id="product-stock" min="0" value="0"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">URL Imagen</label>
|
||||||
|
<input type="url" id="product-image"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Puntos Destacados (uno por línea)</label>
|
||||||
|
<textarea id="product-highlights" rows="3" placeholder="Alta velocidad Diseño compacto Garantía 2 años"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Especificaciones (JSON)</label>
|
||||||
|
<textarea id="product-specs" rows="3" placeholder='{"procesador": "Intel i5", "ram": "16GB"}'
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 font-mono text-sm"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="product-active" checked class="w-4 h-4">
|
||||||
|
<span>Activo</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="product-featured" class="w-4 h-4">
|
||||||
|
<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">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn-primary px-4 py-2 rounded">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let products = [];
|
||||||
|
let categories = [];
|
||||||
|
|
||||||
|
// Load products on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadCategories();
|
||||||
|
loadProducts();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/products/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 = cat.charAt(0).toUpperCase() + cat.slice(1).replace('_', ' ');
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
document.getElementById('stat-categories').textContent = categories.length;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading categories:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProducts() {
|
||||||
|
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/products/?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) {
|
||||||
|
products = await response.json();
|
||||||
|
|
||||||
|
// Client-side search filter
|
||||||
|
if (search) {
|
||||||
|
products = products.filter(p =>
|
||||||
|
p.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProducts();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading products:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('stat-total').textContent = products.length;
|
||||||
|
document.getElementById('stat-active').textContent = products.filter(p => p.is_active).length;
|
||||||
|
document.getElementById('stat-featured').textContent = products.filter(p => p.is_featured).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProducts() {
|
||||||
|
const grid = document.getElementById('products-grid');
|
||||||
|
const empty = document.getElementById('empty-state');
|
||||||
|
|
||||||
|
if (products.length === 0) {
|
||||||
|
grid.classList.add('hidden');
|
||||||
|
empty.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.classList.remove('hidden');
|
||||||
|
empty.classList.add('hidden');
|
||||||
|
|
||||||
|
grid.innerHTML = products.map(product => `
|
||||||
|
<div class="card p-4 ${!product.is_active ? 'opacity-60' : ''}">
|
||||||
|
${product.image_url ?
|
||||||
|
`<img src="${product.image_url}" alt="${product.name}" class="w-full h-40 object-cover rounded mb-4">` :
|
||||||
|
`<div class="w-full h-40 bg-gray-800 rounded mb-4 flex items-center justify-center text-4xl">📦</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<h4 class="font-bold text-lg">${product.name}</h4>
|
||||||
|
${product.is_featured ? '<span class="text-yellow-400">⭐</span>' : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-400 mb-2">
|
||||||
|
${product.category.replace('_', ' ')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xl font-bold accent mb-3">
|
||||||
|
$${product.price.toLocaleString('es-MX')} MXN
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${product.description ?
|
||||||
|
`<p class="text-sm text-gray-400 mb-3 line-clamp-2">${product.description}</p>` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="${product.stock > 0 ? 'text-green-400' : 'text-red-400'}">
|
||||||
|
${product.stock > 0 ? `${product.stock} en stock` : 'Sin stock'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<button onclick="generatePost(${product.id})"
|
||||||
|
class="flex-1 btn-primary px-3 py-2 rounded text-sm">
|
||||||
|
Generar Post
|
||||||
|
</button>
|
||||||
|
<button onclick="editProduct(${product.id})"
|
||||||
|
class="btn-secondary px-3 py-2 rounded text-sm">
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteProduct(${product.id})"
|
||||||
|
class="btn-danger px-3 py-2 rounded text-sm">
|
||||||
|
🗑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(mode, productId = null) {
|
||||||
|
document.getElementById('modal-title').textContent =
|
||||||
|
mode === 'add' ? 'Agregar Producto' : 'Editar Producto';
|
||||||
|
document.getElementById('product-form').reset();
|
||||||
|
document.getElementById('product-id').value = '';
|
||||||
|
document.getElementById('product-active').checked = true;
|
||||||
|
document.getElementById('product-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('product-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editProduct(id) {
|
||||||
|
const product = products.find(p => p.id === id);
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
document.getElementById('modal-title').textContent = 'Editar Producto';
|
||||||
|
document.getElementById('product-id').value = product.id;
|
||||||
|
document.getElementById('product-name').value = product.name;
|
||||||
|
document.getElementById('product-category').value = product.category;
|
||||||
|
document.getElementById('product-price').value = product.price;
|
||||||
|
document.getElementById('product-description').value = product.description || '';
|
||||||
|
document.getElementById('product-stock').value = product.stock || 0;
|
||||||
|
document.getElementById('product-image').value = product.image_url || '';
|
||||||
|
document.getElementById('product-highlights').value = (product.highlights || []).join('\n');
|
||||||
|
document.getElementById('product-specs').value = product.specs ? JSON.stringify(product.specs, null, 2) : '';
|
||||||
|
document.getElementById('product-active').checked = product.is_active;
|
||||||
|
document.getElementById('product-featured').checked = product.is_featured;
|
||||||
|
|
||||||
|
document.getElementById('product-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProduct(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const id = document.getElementById('product-id').value;
|
||||||
|
const highlightsText = document.getElementById('product-highlights').value;
|
||||||
|
const specsText = document.getElementById('product-specs').value;
|
||||||
|
|
||||||
|
let specs = {};
|
||||||
|
if (specsText) {
|
||||||
|
try {
|
||||||
|
specs = JSON.parse(specsText);
|
||||||
|
} catch (e) {
|
||||||
|
alert('JSON de especificaciones inválido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: document.getElementById('product-name').value,
|
||||||
|
category: document.getElementById('product-category').value,
|
||||||
|
price: parseFloat(document.getElementById('product-price').value),
|
||||||
|
description: document.getElementById('product-description').value,
|
||||||
|
stock: parseInt(document.getElementById('product-stock').value) || 0,
|
||||||
|
image_url: document.getElementById('product-image').value || null,
|
||||||
|
highlights: highlightsText ? highlightsText.split('\n').filter(h => h.trim()) : [],
|
||||||
|
specs: specs,
|
||||||
|
is_active: document.getElementById('product-active').checked,
|
||||||
|
is_featured: document.getElementById('product-featured').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = id ? `/api/products/${id}` : '/api/products/';
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeModal();
|
||||||
|
loadProducts();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(error.detail || 'Error al guardar');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProduct(id) {
|
||||||
|
if (!confirm('¿Eliminar este producto?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/products/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePost(productId) {
|
||||||
|
const product = products.find(p => p.id === productId);
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
const platform = prompt('¿Para qué plataforma? (x, threads, instagram, facebook)', 'x');
|
||||||
|
if (!platform) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/generate/product', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
product_id: productId,
|
||||||
|
platform: platform
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
alert('Post generado:\n\n' + data.content);
|
||||||
|
} else {
|
||||||
|
alert('Error al generar post');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
486
dashboard/templates/services.html
Normal file
486
dashboard/templates/services.html
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Servicios - 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; }
|
||||||
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 50; }
|
||||||
|
.modal.active { display: flex; justify-content: center; align-items: center; }
|
||||||
|
.tag { background-color: #374151; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
|
||||||
|
</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 hover:bg-gray-800">Calendario</a>
|
||||||
|
<a href="/dashboard/products" class="px-4 py-2 rounded hover:bg-gray-800">Productos</a>
|
||||||
|
<a href="/dashboard/services" class="px-4 py-2 rounded bg-gray-800 accent">Servicios</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">
|
||||||
|
<!-- Header & Actions -->
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold">Catálogo de Servicios</h2>
|
||||||
|
<button onclick="openModal('add')" class="btn-primary px-4 py-2 rounded">
|
||||||
|
+ Agregar Servicio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">Categoría</label>
|
||||||
|
<select id="filter-category" onchange="loadServices()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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-gray-800 border border-gray-700 rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold accent" id="stat-total">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Total Servicios</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-green-400" id="stat-active">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Activos</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-yellow-400" id="stat-featured">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Destacados</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<div class="text-2xl font-bold text-blue-400" id="stat-categories">0</div>
|
||||||
|
<div class="text-gray-400 text-sm">Categorías</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 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-2 rounded">
|
||||||
|
+ Agregar Servicio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<div id="service-modal" class="modal">
|
||||||
|
<div class="card p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold" id="modal-title">Agregar Servicio</h3>
|
||||||
|
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-2xl">×</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-1">Nombre *</label>
|
||||||
|
<input type="text" id="service-name" required
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Categoría *</label>
|
||||||
|
<select id="service-category" required class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
<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-1">Descripción</label>
|
||||||
|
<textarea id="service-description" rows="3"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Sectores Objetivo (uno por línea)</label>
|
||||||
|
<textarea id="service-sectors" rows="3" placeholder="Retail Manufactura Servicios"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Beneficios (uno por línea)</label>
|
||||||
|
<textarea id="service-benefits" rows="3" placeholder="Reduce costos operativos Aumenta productividad Operación 24/7"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">Call to Action</label>
|
||||||
|
<input type="text" id="service-cta" placeholder="Agenda una demo gratuita"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-400 block mb-1">URL Imagen</label>
|
||||||
|
<input type="url" id="service-image"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="service-active" checked class="w-4 h-4">
|
||||||
|
<span>Activo</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="service-featured" class="w-4 h-4">
|
||||||
|
<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">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn-primary px-4 py-2 rounded">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let services = [];
|
||||||
|
let categories = [];
|
||||||
|
|
||||||
|
// Load services on page load
|
||||||
|
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('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Client-side search filter
|
||||||
|
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 p-4 ${!service.is_active ? 'opacity-60' : ''}">
|
||||||
|
<div class="flex justify-between items-start mb-3">
|
||||||
|
<div class="text-3xl">
|
||||||
|
${getCategoryIcon(service.category)}
|
||||||
|
</div>
|
||||||
|
${service.is_featured ? '<span class="text-yellow-400">⭐</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 text-sm">
|
||||||
|
Generar Post
|
||||||
|
</button>
|
||||||
|
<button onclick="editService(${service.id})"
|
||||||
|
class="btn-secondary px-3 py-2 rounded text-sm">
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteService(${service.id})"
|
||||||
|
class="btn-danger px-3 py-2 rounded text-sm">
|
||||||
|
🗑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryIcon(category) {
|
||||||
|
const icons = {
|
||||||
|
'ai_automation': '🤖',
|
||||||
|
'consulting': '💼',
|
||||||
|
'development': '💻',
|
||||||
|
'infrastructure': '🖥️',
|
||||||
|
'support': '🛠️',
|
||||||
|
'training': '📚',
|
||||||
|
'3d_printing': '🖨️',
|
||||||
|
'data_analysis': '📊'
|
||||||
|
};
|
||||||
|
return icons[category] || '🛠️';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('service-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
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.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(error.detail || 'Error al guardar');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
alert('Post generado:\n\n' + data.content);
|
||||||
|
} else {
|
||||||
|
alert('Error al generar post');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user