/* ════════════════════════════════ INFINITY — APP.JS Carga datos desde el backend (/api/*) ════════════════════════════════ */ // ════════════════════════════════ // ESTADO // ════════════════════════════════ let categorias = []; let productos = []; function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } let catActiva = 'todos'; let subActiva = []; // [nivel0, nivel1, ...] — null = "Todos" en ese nivel let busqueda = ''; let cargando = true; // ════════════════════════════════ // CARGA DE DATOS // ════════════════════════════════ async function cargarRedes() { try { const res = await fetch('/api/videos'); const videos = await res.json(); const grid = document.getElementById('redesGrid'); if (videos.length === 0) { grid.innerHTML = ''; return; } grid.innerHTML = videos.map(v => `
`).join(''); } catch (err) { console.error('Error cargando videos:', err); } } async function cargarDatos() { try { const [resCats, resProds] = await Promise.all([ fetch('/api/categorias'), fetch('/api/productos') ]); if (!resCats.ok) throw new Error('No se pudieron cargar las categorías'); if (!resProds.ok) throw new Error('No se pudieron cargar los productos'); categorias = await resCats.json(); productos = await resProds.json(); cargando = false; renderTags(); renderProductos(); } catch (err) { console.error('Error cargando datos:', err); cargando = false; const grid = document.getElementById('productos'); const cont = document.getElementById('contador'); cont.textContent = ''; grid.innerHTML = `

😕 No pudimos cargar el catálogo

${err.message}

`; } } // ════════════════════════════════ // HELPERS DE FILTRADO POR SUBRAMA // ════════════════════════════════ // Devuelve los productos que matchean la rama actual (cat + sub[niveles anteriores]) // y cuyo sub[i] === id. Sirve para contar/mostrar subcats con productos. function productosEnNivel(catId, nivelesPrevios, idNivel) { return productos.filter(p => { if (!p || p.cat !== catId) return false; if (p.img && p.img.startsWith('agotados/')) return false; // El sub del producto debe tener al menos la longitud de nivelesPrevios + 1 const sub = Array.isArray(p.sub) ? p.sub : []; if (sub.length < nivelesPrevios.length + 1) return false; // Coincidir con los niveles previos for (let i = 0; i < nivelesPrevios.length; i++) { if (sub[i] !== nivelesPrevios[i]) return false; } // El nivel actual debe ser este id return sub[nivelesPrevios.length] === idNivel; }); } // Devuelve el nodo del árbol (categoría o subcat) según el path function getNodo(catId, niveles) { const cat = categorias.find(c => c.id === catId); if (!cat) return null; let nodo = cat; for (const id of niveles) { if (!Array.isArray(nodo.subcategorias)) return null; const sig = nodo.subcategorias.find(s => s.id === id); if (!sig) return null; nodo = sig; } return nodo; } // ════════════════════════════════ // RENDER TAGS FILTROS (multinivel) // ════════════════════════════════ function renderTags() { const cont = document.getElementById('filtrosTags'); cont.innerHTML = ''; // ——— Nivel 0: Categorías ——— const catsVisibles = categorias.filter(c => c.id !== 'sin-categoria'); const nivel0 = document.createElement('div'); nivel0.className = 'filtros-tags filtros-nivel'; nivel0.innerHTML = `` + catsVisibles.map(cat => { const tieneSubcats = Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0; // Solo mostrar la categoría como tag si tiene productos directos O tiene subcats con productos if (tieneSubcats) { // No renderizar la categoría como "hoja" — sus subcats la cubren // PERO si alguna subcat es hoja con productos, sí tiene sentido mostrarla igual // porque al hacer click se entra al nivel 1 } return ``; }).join(''); cont.appendChild(nivel0); // ——— Niveles 1+ : subcategorías (solo si catActiva está elegida) ——— if (catActiva !== 'todos') { const cat = categorias.find(c => c.id === catActiva); if (cat && Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0) { renderNivelSub(cont, cat, [], 1); } } // Wire click handlers cont.querySelectorAll('.tag').forEach(tag => { tag.addEventListener('click', () => onTagClick(parseInt(tag.dataset.nivel, 10), tag.dataset.id)); }); } // Renderiza un nivel de subcategorías. `previos` = niveles ya elegidos arriba. function renderNivelSub(cont, nodo, previos, nivel) { if (!Array.isArray(nodo.subcategorias) || nodo.subcategorias.length === 0) return; // Subcats que tienen productos en este nivel o cuyas sub-subs tienen productos const subsVisibles = nodo.subcategorias.filter(s => { const tieneSubSub = Array.isArray(s.subcategorias) && s.subcategorias.length > 0; if (tieneSubSub) { // Esta subcat NO recibe productos directamente. Mostrar solo si ALGUNA // de sus sub-subs tiene productos. return s.subcategorias.some(ss => productosEnNivel(catActiva, [...previos, s.id], ss.id).length > 0 ); } else { // Hoja: recibe productos. Mostrar si tiene al menos uno. return productosEnNivel(catActiva, [...previos], s.id).length > 0; } }); if (subsVisibles.length === 0) return; const nivelEl = document.createElement('div'); nivelEl.className = 'filtros-tags filtros-nivel'; const idTodos = `__all_${nivel}`; nivelEl.innerHTML = `${escapeHtml(nodo.nombre || '')}` + `` + subsVisibles.map(s => { const esActivo = subActiva[nivel - 1] === s.id; return ``; }).join(''); cont.appendChild(nivelEl); // Si la subcat activa tiene sub-subs, renderizar el siguiente nivel if (subActiva[nivel - 1]) { const subActivaNodo = nodo.subcategorias.find(s => s.id === subActiva[nivel - 1]); if (subActivaNodo && Array.isArray(subActivaNodo.subcategorias) && subActivaNodo.subcategorias.length > 0) { renderNivelSub(cont, subActivaNodo, [...previos, subActiva[nivel - 1]], nivel + 1); } } } function onTagClick(nivel, id) { if (nivel === 0) { if (id === 'todos') { catActiva = 'todos'; } else { catActiva = id; } subActiva = []; // reset sub al cambiar categoría } else { if (id.startsWith('__all_')) { subActiva[nivel - 1] = null; } else { subActiva[nivel - 1] = id; } // Resetear niveles más profundos subActiva = subActiva.slice(0, nivel); // Rellenar con null hasta el nivel actual while (subActiva.length < nivel) subActiva.push(null); } renderTags(); renderProductos(); } function sincronizarTag(cat) { // Legacy — mantenido por compatibilidad, pero ya no se usa document.querySelectorAll('.tag').forEach(t => { t.classList.toggle('active', t.dataset.cat === cat); }); } // ════════════════════════════════ // RENDER PRODUCTOS // ════════════════════════════════ function renderProductos() { const grid = document.getElementById('productos'); const empty = document.getElementById('emptyState'); const contador = document.getElementById('contador'); let filtrados = productos.filter(p => p.cat && p.cat !== 'sin-categoria' && (!p.img || !p.img.startsWith('agotados/')) ); if (catActiva !== 'todos') { filtrados = filtrados.filter(p => { if (p.cat !== catActiva) return false; // Validar que la cadena de sub coincida const sub = Array.isArray(p.sub) ? p.sub : []; for (let i = 0; i < subActiva.length; i++) { if (subActiva[i] != null && sub[i] !== subActiva[i]) return false; } return true; }); } if (busqueda.trim()) { const q = busqueda.toLowerCase(); filtrados = filtrados.filter(p => (p.nombre || '').toLowerCase().includes(q) || (p.desc || '').toLowerCase().includes(q) || (p.cat || '').toLowerCase().includes(q) || (p.ref || '').toLowerCase().includes(q) ); } contador.textContent = filtrados.length === 1 ? 'Mostrando 1 producto' : `Mostrando ${filtrados.length} productos`; if (filtrados.length === 0) { grid.innerHTML = ''; empty.style.display = 'block'; return; } empty.style.display = 'none'; grid.innerHTML = filtrados.map((p, i) => { const cat = categorias.find(c => c.id === p.cat); const nombre = p.nombre || p.ref || 'Sin nombre'; const desc = p.desc || ''; const precio = (p.precio !== undefined && p.precio !== null && p.precio !== '') ? `$${Number(p.precio).toLocaleString('es-CO')}` : '—'; const imgSrc = p.img && p.img.startsWith('/') ? p.img : `/Imagenes/${p.img}`; return `
${cat ? cat.nombre : p.cat} ${escapeHtml(nombre)} ${cat ? cat.icono : '📦'}

${escapeHtml(nombre)}

${desc ? `

${escapeHtml(desc)}

` : ''}
${precio}
`; }).join(''); wireAddToCart(); } // ════════════════════════════════ // BUSCADOR // ════════════════════════════════ function setupSearch() { const input = document.getElementById('searchInput'); const clear = document.getElementById('searchClear'); input.addEventListener('input', e => { busqueda = e.target.value; clear.style.display = busqueda ? 'flex' : 'none'; renderProductos(); }); clear.addEventListener('click', () => { input.value = ''; busqueda = ''; clear.style.display = 'none'; renderProductos(); input.focus(); }); document.getElementById('btnLimpiar').addEventListener('click', () => { input.value = ''; busqueda = ''; catActiva = 'todos'; sincronizarTag('todos'); clear.style.display = 'none'; renderProductos(); }); } // ════════════════════════════════ // NAVBAR MOBILE // ════════════════════════════════ function setupNav() { const toggle = document.getElementById('navToggle'); const links = document.getElementById('navLinks'); toggle.addEventListener('click', () => { links.classList.toggle('open'); }); links.querySelectorAll('a').forEach(a => { a.addEventListener('click', () => links.classList.remove('open')); }); } // ════════════════════════════════ // CARRITO DE COMPRAS // ════════════════════════════════ let carrito = []; // { ref, nombre, precio, cantidad } function actualizarBadge() { const total = carrito.reduce((s, i) => s + i.cantidad, 0); const badge = document.getElementById('cartBadge'); badge.textContent = total; badge.style.display = total > 0 ? 'flex' : 'none'; } function actualizarTotal() { const total = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0); document.querySelector('#cartTotal strong').textContent = total > 0 ? `$${total.toLocaleString('es-CO')}` : '$0'; document.getElementById('cartPdfBtn').disabled = carrito.length === 0; } function renderCarritoModal() { const body = document.getElementById('cartBody'); if (carrito.length === 0) { body.innerHTML = '

El carrito está vacío.

'; actualizarTotal(); return; } const totalGeneral = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0); body.innerHTML = `
${carrito.map((item, idx) => { const subtotal = item.precio * item.cantidad; return `
${escapeHtml(item.nombre)}
${escapeHtml(item.ref)} — $${(item.precio || 0).toLocaleString('es-CO')} c/u
${item.cantidad}
$${subtotal.toLocaleString('es-CO')}
`; }).join('')}
`; body.querySelectorAll('[data-cart-inc]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.cartInc, 10); carrito[idx].cantidad++; renderCarritoModal(); actualizarBadge(); }); }); body.querySelectorAll('[data-cart-dec]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.cartDec, 10); if (carrito[idx].cantidad > 1) { carrito[idx].cantidad--; } else { carrito.splice(idx, 1); } renderCarritoModal(); actualizarBadge(); }); }); body.querySelectorAll('[data-cart-remove]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.cartRemove, 10); carrito.splice(idx, 1); renderCarritoModal(); actualizarBadge(); }); }); actualizarTotal(); } // Modales function abrirModal(id) { document.getElementById(id).classList.add('open'); document.body.style.overflow = 'hidden'; } function cerrarModal(id) { document.getElementById(id).classList.remove('open'); document.body.style.overflow = ''; } function setupCartModals() { // FAB → abrir carrito document.getElementById('cartFab').addEventListener('click', () => { renderCarritoModal(); abrirModal('cartModal'); }); // Cerrar carrito document.querySelectorAll('[data-cart-close]').forEach(el => { el.addEventListener('click', () => cerrarModal('cartModal')); }); // Cerrar pedido document.querySelectorAll('[data-order-close]').forEach(el => { el.addEventListener('click', () => cerrarModal('orderModal')); }); // Vaciar carrito document.getElementById('cartClearBtn').addEventListener('click', () => { carrito = []; renderCarritoModal(); actualizarBadge(); }); // Generar PDF → abrir formulario document.getElementById('cartPdfBtn').addEventListener('click', async () => { cerrarModal('cartModal'); document.getElementById('orderForm').reset(); document.getElementById('orderError').classList.remove('show'); // Cargar vendedores en el select const sel = document.getElementById('orderVendedor'); sel.innerHTML = ''; try { const res = await fetch('/api/vendedores'); if (res.ok) { const vendedores = await res.json(); vendedores.forEach(v => { const opt = document.createElement('option'); opt.value = v.nombre; opt.textContent = v.nombre; sel.appendChild(opt); }); } } catch {} abrirModal('orderModal'); }); // Enviar pedido document.getElementById('orderSubmitBtn').addEventListener('click', enviarPedido); // Enter en el formulario también envía document.getElementById('orderForm').addEventListener('submit', e => { e.preventDefault(); enviarPedido(); }); } async function enviarPedido() { const nombre = document.getElementById('orderNombre').value.trim(); const ciudad = document.getElementById('orderCiudad').value.trim(); const telefono = document.getElementById('orderTelefono').value.trim(); const direccion = document.getElementById('orderDireccion').value.trim(); const vendedor = document.getElementById('orderVendedor').value; const errEl = document.getElementById('orderError'); if (!nombre || !ciudad || !telefono || !direccion || !vendedor) { errEl.textContent = 'Todos los campos son obligatorios'; errEl.classList.add('show'); return; } errEl.classList.remove('show'); const btn = document.getElementById('orderSubmitBtn'); const textEl = btn.querySelector('.btn-text'); const spin = btn.querySelector('.btn-spinner'); btn.disabled = true; textEl.textContent = 'Generando…'; spin.style.display = 'inline-block'; try { const res = await fetch('/api/pedidos/generar-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: carrito.map(i => ({ ref: i.ref, nombre: i.nombre, precio: i.precio, cantidad: i.cantidad })), cliente: { nombre, ciudad, telefono, direccion }, vendedor }) }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Error al generar el pedido'); } // Descargar PDF const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const dispo = res.headers.get('Content-Disposition') || ''; const match = dispo.match(/filename="(.+)"/); a.download = match ? match[1] : `pedido-${Date.now()}.pdf`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); // Limpiar carrito y cerrar modal carrito = []; actualizarBadge(); cerrarModal('orderModal'); } catch (err) { errEl.textContent = err.message; errEl.classList.add('show'); } finally { btn.disabled = false; textEl.textContent = '✅ Generar pedido'; spin.style.display = 'none'; } } // Wire add-to-cart buttons después de renderizar productos function wireAddToCart() { document.querySelectorAll('.producto-card-add-btn').forEach(btn => { if (btn.dataset.cartWired) return; btn.dataset.cartWired = '1'; btn.addEventListener('click', () => { const ref = btn.dataset.ref; const nombre = btn.dataset.nombre; const precio = parseFloat(btn.dataset.precio); const qtyInput = document.querySelector(`.producto-card-add-qty[data-ref="${ref}"]`); const cantidad = qtyInput ? parseInt(qtyInput.value, 10) : 1; if (!cantidad || cantidad < 1) return; // Mostrar feedback si el producto no tiene precio if (!precio || isNaN(precio)) { const orig = btn.textContent; btn.textContent = '⚠ Sin precio'; btn.style.opacity = '0.7'; setTimeout(() => { btn.textContent = orig; btn.style.opacity = '1'; }, 1500); return; } const existente = carrito.find(i => i.ref === ref); if (existente) { existente.cantidad += cantidad; } else { carrito.push({ ref, nombre, precio, cantidad }); } if (qtyInput) qtyInput.value = 1; actualizarBadge(); const original = btn.textContent; btn.textContent = '✓ Agregado'; setTimeout(() => { btn.textContent = original; }, 1200); }); }); } // ════════════════════════════════ // INIT // ════════════════════════════════ document.addEventListener('DOMContentLoaded', () => { setupSearch(); setupNav(); setupCartModals(); cargarDatos(); cargarRedes(); });