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

427 lines
19 KiB
HTML
Raw Permalink 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 %}Productos{% 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">Productos</h1>
<p class="text-gray-400 mt-1">Catálogo de productos 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 Producto</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 Productos</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="loadProducts()" 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="loadProducts()" 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="loadProducts()" 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="loadProducts()" placeholder="Nombre del producto..."
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>
<!-- 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 rounded-2xl 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-3 rounded-xl">
+ Agregar Producto
</button>
</div>
</div>
<!-- Add/Edit Modal -->
<div id="product-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 Producto</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white text-xl"></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-2">Nombre *</label>
<input type="text" id="product-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>
<label class="text-sm text-gray-400 block mb-2">Categoría *</label>
<select id="product-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="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-2">Precio (MXN) *</label>
<input type="number" id="product-price" step="0.01" min="0" 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">Descripción</label>
<textarea id="product-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>
<label class="text-sm text-gray-400 block mb-2">Stock</label>
<input type="number" id="product-stock" min="0" value="0"
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="product-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="col-span-2">
<label class="text-sm text-gray-400 block mb-2">Puntos Destacados (uno por línea)</label>
<textarea id="product-highlights" rows="3" placeholder="Alta velocidad&#10;Diseño compacto&#10;Garantía 2 años"
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 flex items-center gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="product-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="product-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 %}
<script>
let products = [];
let categories = [];
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();
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 rounded-2xl 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-xl mb-4">` :
`<div class="w-full h-40 bg-dark-800 rounded-xl 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 text-primary 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 mb-4">
<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">
<button onclick="generatePost(${product.id})"
class="flex-1 btn-primary px-3 py-2 rounded-xl text-sm">
Generar Post
</button>
<button onclick="editProduct(${product.id})"
class="btn-secondary px-3 py-2 rounded-xl text-sm">
Editar
</button>
<button onclick="deleteProduct(${product.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, 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.remove('hidden');
}
function closeModal() {
document.getElementById('product-modal').classList.add('hidden');
}
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-active').checked = product.is_active;
document.getElementById('product-featured').checked = product.is_featured;
document.getElementById('product-modal').classList.remove('hidden');
}
async function saveProduct(event) {
event.preventDefault();
const id = document.getElementById('product-id').value;
const highlightsText = document.getElementById('product-highlights').value;
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()) : [],
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();
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Producto 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 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;
showModal('Generando post con IA...', true);
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();
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>
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
{% endblock %}