From d0581d36d1f06827fae3860715a72d6eda4cab76 Mon Sep 17 00:00:00 2001 From: Juan Pablo Restrepo Date: Tue, 9 Jun 2026 10:05:00 -0500 Subject: [PATCH] PDF PEDIDOS --- backend/server.js | 157 +++++++++++++ data/productos.json | 16 +- docker-compose.yml | 2 + frontend/app.js | 256 ++++++++++++++++++++ frontend/estilos.css | 391 +++++++++++++++++++++++++++++++ frontend/index.html | 80 ++++++- pedidos/pedido-1781012420688.pdf | Bin 0 -> 2868 bytes pedidos/pedido-1781012802257.pdf | Bin 0 -> 2640 bytes pedidos/pedido-1781017023680.pdf | Bin 0 -> 2738 bytes 9 files changed, 896 insertions(+), 6 deletions(-) create mode 100644 pedidos/pedido-1781012420688.pdf create mode 100644 pedidos/pedido-1781012802257.pdf create mode 100644 pedidos/pedido-1781017023680.pdf diff --git a/backend/server.js b/backend/server.js index d827b1a..395e866 100644 --- a/backend/server.js +++ b/backend/server.js @@ -22,6 +22,7 @@ const SECRET = process.env.SESSION_SECRET || 'infinity-cambia-esta-clave-en-prod const DATA_DIR = path.join(__dirname, 'data'); const FRONTEND_DIR = path.join(__dirname, 'frontend'); const IMG_DIR = path.join(__dirname, 'Imagenes'); +const PEDIDOS_DIR = path.join(__dirname, 'pedidos'); // ════════════════════════════════ // MIDDLEWARES @@ -30,6 +31,7 @@ app.use(express.json()); app.use(cookieParser(SECRET)); app.use(express.static(FRONTEND_DIR)); app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' })); +app.use('/pedidos', express.static(PEDIDOS_DIR)); // Evita el 404 de favicon.ico app.get('/favicon.ico', (req, res) => res.status(204).end()); @@ -1886,6 +1888,161 @@ app.post('/api/descargas/fotos', requireDescargasAccess, async (req, res) => { } }); +// ════════════════════════════════ +// PEDIDOS — generar PDF con pedido +// ════════════════════════════════ +app.post('/api/pedidos/generar-pdf', async (req, res) => { + const { items, cliente } = req.body || {}; + + if (!Array.isArray(items) || items.length === 0) { + return res.status(400).json({ error: 'El pedido debe tener al menos un producto' }); + } + if (!cliente || !cliente.nombre || !cliente.ciudad || !cliente.telefono || !cliente.direccion) { + return res.status(400).json({ error: 'Todos los datos del cliente son obligatorios' }); + } + + try { + const filename = `pedido-${Date.now()}.pdf`; + const filepath = path.join(PEDIDOS_DIR, filename); + + // Asegurar que la carpeta existe + await fs.mkdir(PEDIDOS_DIR, { recursive: true }); + + const doc = new PDFDocument({ + size: 'A4', + margins: { top: 50, bottom: 50, left: 50, right: 50 }, + info: { + Title: `Pedido - ${cliente.nombre}`, + Author: 'Infinity', + Subject: 'Pedido de productos' + } + }); + + const writeStream = fsSync.createWriteStream(filepath); + doc.pipe(writeStream); + + const pageW = doc.page.width; + const MARGIN_X = 50; + const pageH = doc.page.height; + + // === ENCABEZADO === + doc.fontSize(36).fillColor('#8b5cf6').font('Helvetica-Bold') + .text('∞ Infinity', MARGIN_X, 50, { width: pageW - 2 * MARGIN_X }); + + doc.moveTo(MARGIN_X, 100).lineTo(pageW - MARGIN_X, 100) + .strokeColor('#8b5cf6').lineWidth(1.5).stroke(); + + doc.fontSize(22).fillColor('#1a1a2e').font('Helvetica-Bold') + .text('Pedido', MARGIN_X, 120, { width: pageW - 2 * MARGIN_X }); + + const fechaStr = new Date().toLocaleDateString('es-CO', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', + hour: '2-digit', minute: '2-digit' + }); + doc.fontSize(10).fillColor('#8b8ba7').font('Helvetica') + .text(fechaStr, MARGIN_X, 150, { width: pageW - 2 * MARGIN_X }); + + // === DATOS DEL CLIENTE === + doc.fontSize(14).fillColor('#1a1a2e').font('Helvetica-Bold') + .text('Datos del cliente', MARGIN_X, 185, { width: pageW - 2 * MARGIN_X }); + + const clientFields = [ + ['Nombre', cliente.nombre], + ['Ciudad', cliente.ciudad], + ['Teléfono', cliente.telefono], + ['Dirección', cliente.direccion] + ]; + + let clientY = 210; + doc.fontSize(11).fillColor('#4a4a68').font('Helvetica'); + for (const [label, value] of clientFields) { + doc.text(`${label}: `, MARGIN_X, clientY, { continued: true }); + doc.fillColor('#1a1a2e').text(value, { width: pageW - 2 * MARGIN_X }); + clientY += 18; + } + + // === TABLA DE PRODUCTOS === + let tableY = clientY + 20; + doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY) + .strokeColor('#e8e6f0').lineWidth(0.5).stroke(); + + tableY += 12; + const colRef = MARGIN_X; + const colNombre = MARGIN_X + 80; + const colCant = pageW - MARGIN_X - 180; + const colPrecio = pageW - MARGIN_X - 120; + const colSubtotal = pageW - MARGIN_X - 60; + + doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica-Bold'); + doc.text('REF', colRef, tableY); + doc.text('Producto', colNombre, tableY); + doc.text('Cant', colCant, tableY, { align: 'center', width: 40 }); + doc.text('Precio', colPrecio, tableY, { align: 'right', width: 60 }); + doc.text('Total', colSubtotal, tableY, { align: 'right', width: 60 }); + + const headerY = tableY; + + let totalGeneral = 0; + for (const item of items) { + const subtotal = (item.precio || 0) * (item.cantidad || 1); + totalGeneral += subtotal; + + tableY += 22; + if (tableY > pageH - 90) { + doc.addPage(); + tableY = 60; + } + + doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica') + .text(String(item.ref || '—'), colRef, tableY, { width: 76 }); + doc.fillColor('#1a1a2e').font('Helvetica') + .text(String(item.nombre || item.ref || '—'), colNombre, tableY, { width: colCant - colNombre - 10 }); + doc.fillColor('#1a1a2e').font('Helvetica-Bold') + .text(String(item.cantidad || 1), colCant, tableY, { align: 'center', width: 40 }); + doc.fillColor('#4a4a68').font('Helvetica') + .text(`$${(item.precio || 0).toLocaleString('es-CO')}`, colPrecio, tableY, { align: 'right', width: 60 }); + doc.fillColor('#8b5cf6').font('Helvetica-Bold') + .text(`$${subtotal.toLocaleString('es-CO')}`, colSubtotal, tableY, { align: 'right', width: 60 }); + } + + // Línea final + tableY += 20; + doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY) + .strokeColor('#e8e6f0').lineWidth(0.5).stroke(); + + tableY += 14; + doc.fontSize(11).fillColor('#4a4a68').font('Helvetica'); + doc.text('Total pedido:', MARGIN_X, tableY, { continued: true }); + doc.fontSize(16).fillColor('#8b5cf6').font('Helvetica-Bold') + .text(` $${totalGeneral.toLocaleString('es-CO')}`, { width: pageW - 2 * MARGIN_X }); + + // === FOOTER con número de página === + const range = doc.bufferedPageRange(); + for (let i = range.start; i < range.start + range.count; i++) { + doc.switchToPage(i); + doc.fontSize(8).fillColor('#8b8ba7').font('Helvetica') + .text(`${i + 1} / ${range.count}`, + MARGIN_X, pageH - 40, { width: pageW - 2 * MARGIN_X, align: 'center' }); + } + + doc.end(); + + // Esperar a que termine de escribir + await new Promise((resolve, reject) => { + writeStream.on('finish', resolve); + writeStream.on('error', reject); + }); + + // Enviar el archivo + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.sendFile(filepath); + } catch (err) { + console.error('Pedido PDF error:', err); + res.status(500).json({ error: 'Error al generar el pedido: ' + err.message }); + } +}); + // ════════════════════════════════ // START // ════════════════════════════════ diff --git a/data/productos.json b/data/productos.json index 64ac9f7..d5c8e58 100644 --- a/data/productos.json +++ b/data/productos.json @@ -57,7 +57,9 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/AF100-197.webp" + "img": "bolsos/dama/aaa/AF100-197.webp", + "nombre": "BOLSO", + "precio": 10000 }, { "id": "AF100-198", @@ -67,7 +69,9 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/AF100-198.webp" + "img": "bolsos/dama/aaa/AF100-198.webp", + "nombre": "BOLSO", + "precio": 10000 }, { "id": "AF100-199", @@ -77,7 +81,9 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/AF100-199.webp" + "img": "bolsos/dama/aaa/AF100-199.webp", + "nombre": "BOLSO", + "precio": 10000 }, { "id": "AF100-201", @@ -87,7 +93,9 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/AF100-201.webp" + "img": "bolsos/dama/aaa/AF100-201.webp", + "nombre": "BOLSO", + "precio": 10000 }, { "id": "AF100-202", diff --git a/docker-compose.yml b/docker-compose.yml index 70904ab..98a78aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: # Persistencia: los cambios en JSON se guardan en el host - ./data:/app/data - ./Imagenes:/app/Imagenes + - ./pedidos:/app/pedidos + - ./frontend:/app/frontend environment: - NODE_ENV=production - PORT=3000 diff --git a/frontend/app.js b/frontend/app.js index cfd9baf..69451db 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -275,10 +275,21 @@ function renderProductos() {

${escapeHtml(nombre)}

${desc ? `

${escapeHtml(desc)}

` : ''}
${precio}
+
+ + +
`; }).join(''); + + wireAddToCart(); } // ════════════════════════════════ @@ -328,11 +339,256 @@ function setupNav() { }); } +// ════════════════════════════════ +// 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', () => { + cerrarModal('cartModal'); + document.getElementById('orderForm').reset(); + document.getElementById('orderError').classList.remove('show'); + 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 errEl = document.getElementById('orderError'); + + if (!nombre || !ciudad || !telefono || !direccion) { + 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 } + }) + }); + + 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(); }); + + diff --git a/frontend/estilos.css b/frontend/estilos.css index 83174b2..ef35eca 100644 --- a/frontend/estilos.css +++ b/frontend/estilos.css @@ -887,3 +887,394 @@ section { font-size: 2rem; } } + +/* ════════════════════════════════ + CARRITO DE COMPRAS + ════════════════════════════════ */ + +/* Botón flotante del carrito */ +.cart-fab { + position: fixed; + bottom: 28px; + right: 28px; + z-index: 900; + width: 60px; + height: 60px; + border-radius: 50%; + border: none; + background: linear-gradient(135deg, #8b5cf6, #a78bfa); + color: #fff; + font-size: 1.5rem; + cursor: pointer; + box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4); + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +.cart-fab:hover { + transform: scale(1.08); + box-shadow: 0 6px 28px rgba(139, 92, 246, 0.55); +} + +.cart-fab:active { + transform: scale(0.95); +} + +.cart-fab-badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 22px; + height: 22px; + padding: 0 6px; + border-radius: 11px; + background: #ef4444; + color: #fff; + font-size: 0.7rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Plus Jakarta Sans', sans-serif; + box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4); +} + +/* Modal genérico (compartido con carrito y pedido) */ +.modal { + display: none; + position: fixed; + inset: 0; + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.open { + display: flex; +} + +.modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); + backdrop-filter: blur(4px); +} + +.modal-content { + position: relative; + background: #fff; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); + max-width: 520px; + width: 90vw; + max-height: 85vh; + display: flex; + flex-direction: column; + animation: modalIn 0.25s ease-out; +} + +@keyframes modalIn { + from { opacity: 0; transform: translateY(20px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 0; +} + +.modal-header h2 { + font-family: 'DM Serif Display', serif; + font-size: 1.4rem; + color: #1a1a2e; + margin: 0; +} + +.modal-close { + background: none; + border: none; + font-size: 1.3rem; + color: #8b8ba7; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + transition: background 0.15s; +} + +.modal-close:hover { + background: #f3f0ff; + color: #1a1a2e; +} + +.modal-body { + padding: 20px 24px; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + padding: 16px 24px 20px; + border-top: 1px solid #e8e6f0; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Carrito — lista de items */ +.cart-items { + display: flex; + flex-direction: column; + gap: 10px; +} + +.cart-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: #f8f7fc; + border-radius: 10px; +} + +.cart-item-info { + flex: 1; + min-width: 0; +} + +.cart-item-name { + font-size: 0.85rem; + font-weight: 600; + color: #1a1a2e; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cart-item-ref { + font-size: 0.72rem; + color: #8b8ba7; +} + +.cart-item-controls { + display: flex; + align-items: center; + gap: 6px; +} + +.cart-item-qty-btn { + width: 28px; + height: 28px; + border: 1px solid #d4d0e0; + border-radius: 6px; + background: #fff; + color: #1a1a2e; + font-size: 1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; +} + +.cart-item-qty-btn:hover { + background: #f3f0ff; +} + +.cart-item-qty { + font-size: 0.9rem; + font-weight: 600; + color: #1a1a2e; + min-width: 24px; + text-align: center; +} + +.cart-item-subtotal { + font-size: 0.85rem; + font-weight: 700; + color: #8b5cf6; + min-width: 70px; + text-align: right; +} + +.cart-item-remove { + background: none; + border: none; + color: #ef4444; + cursor: pointer; + font-size: 1rem; + padding: 4px; + border-radius: 4px; + transition: background 0.15s; +} + +.cart-item-remove:hover { + background: #fef2f2; +} + +.cart-empty { + text-align: center; + color: #8b8ba7; + padding: 32px 0; + font-size: 0.95rem; +} + +.cart-total { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 1.05rem; + color: #1a1a2e; +} + +.cart-total strong { + font-size: 1.3rem; + color: #8b5cf6; +} + +.cart-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +/* Formulario de pedido */ +.order-modal-content { + max-width: 440px; +} + +.form-group { + margin-bottom: 14px; +} + +.form-group label { + display: block; + font-size: 0.82rem; + font-weight: 600; + color: #4a4a68; + margin-bottom: 4px; +} + +.form-group input { + width: 100%; + padding: 10px 14px; + border: 1.5px solid #d4d0e0; + border-radius: 10px; + font-size: 0.9rem; + font-family: 'Plus Jakarta Sans', sans-serif; + color: #1a1a2e; + background: #fafaff; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} + +.form-group input:focus { + outline: none; + border-color: #8b5cf6; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); +} + +.form-error { + color: #ef4444; + font-size: 0.82rem; + display: none; + margin-bottom: 8px; +} + +.form-error.show { + display: block; +} + +/* Botón "Agregar" en tarjetas de producto */ +.producto-card-add { + display: flex; + align-items: center; + gap: 6px; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #e8e6f0; +} + +.producto-card-add-qty { + width: 48px; + padding: 6px 8px; + border: 1.5px solid #d4d0e0; + border-radius: 8px; + font-size: 0.82rem; + font-weight: 600; + text-align: center; + font-family: 'Plus Jakarta Sans', sans-serif; + color: #1a1a2e; + background: #fff; + transition: border-color 0.2s; + box-sizing: border-box; +} + +.producto-card-add-qty:focus { + outline: none; + border-color: #8b5cf6; +} + +.producto-card-add-btn { + flex: 1; + padding: 7px 12px; + border: none; + border-radius: 8px; + background: linear-gradient(135deg, #8b5cf6, #a78bfa); + color: #fff; + font-size: 0.78rem; + font-weight: 600; + font-family: 'Plus Jakarta Sans', sans-serif; + cursor: pointer; + transition: opacity 0.2s, transform 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.producto-card-add-btn:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.producto-card-add-btn:active { + transform: scale(0.97); +} + +/* Spinner en botones */ +.btn-spinner { + margin-left: 6px; +} + +/* Responsive carrito */ +@media (max-width: 480px) { + .cart-fab { + width: 52px; + height: 52px; + bottom: 20px; + right: 20px; + font-size: 1.3rem; + } + + .modal-content { + width: 95vw; + border-radius: 12px; + } + + .cart-item { + flex-wrap: wrap; + } + + .cart-actions { + flex-direction: column; + } + + .cart-actions .btn { + width: 100%; + justify-content: center; + } +} diff --git a/frontend/index.html b/frontend/index.html index bb3a3b1..8efb504 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,7 +9,7 @@ - + @@ -102,6 +102,82 @@ - + + + + + + + + + + diff --git a/pedidos/pedido-1781012420688.pdf b/pedidos/pedido-1781012420688.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fb25417923e3cd024029e54669d9d3f930b901f3 GIT binary patch literal 2868 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 Qr6I-OFO{2H;7dWs-(mHB$N&HU literal 0 HcmV?d00001 diff --git a/pedidos/pedido-1781017023680.pdf b/pedidos/pedido-1781017023680.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c5566fd22c2194079be7f98084e28e493661cebd GIT binary patch literal 2738 zcmbtWeN+=y7FQQ`yF;mp%a^P6c_e(;hWQAS34Wkr$jZkG1d1M0tdnF2W5Wz46Gbfn zss(i`OIdZHwRAn`f}$>Hm0F^$)%5@^u=u$yw%v%U)}Css!BDiiZ$e^_^qlVL&L7En z_ucz`ciz4C_ZvxuQYFP>Wr*Z4d=Ls?fY+`>=FCCR?6rjqKr?6qgTP-C!wCR~+iC>; zJ)>voM7{`UFlZp-$yh=G6mfX0B0d*E7xJ6{oy~w8_Es}y-eS@*W*~PN5)u%G(~BtL z+Jly1;&oY!05mAf2%yDi&?>XNKA?DzL zNMksIkPj$11>;zwz?cB4GSUL0WOTfqL12_wFfnuiQna&cY2TK9)r8_pOB~~kd~UjB z!*{2+*s6;8#J!NaSbLBkQPy1b(3<}}aex1{H~&2FudqJ#^q1b7W%=g1a`V~Qi??lA z6-y5EeE#50dC8-j6|a;$tQtz6%1{l<7Bw~3&kth!lW-<*RnvK8aLBbin?B6eCBHZH z(T5++uG830^)K%3i1DxLh?twO=1xh~?nQspIr~yJ{%U$t*qHafoE&0#_omY^nC1LI z=S;n^ttNWz8m?|n8dWl_|MB3r{~3Hf;n9^<_Z__R+i*$R%jwy>{V(?(qPs(@65F#2 z(zmql_)UXFo$`X>+2Hc;Hdav|aH@@GPEH(btyi{8vH5pwW&?tLTi8D9xGbd!E4n$W zrD40{Y)E9-%g?0SyAFgogMtfQN$WY%6lDqAm7mkh-DpXANj4+SIMlG@*j;&DyJ4;Re11%O zbDKT+@Eb2qKa)2zywZw%VmHmSo!+T!wP-Kz*llh6_~87lVb~Xe6#2Ws>R<1>m2zv@ z_9OX?drQX7YdpM5+bDhXmyGk7_?3W|v>kyqzbUrIw%f;2?w&RGb={cTTIZ-~zPKX& zt@V?_%{E8wyX#`5jw`P^iO}xIignA^*rnIY1M15xnN$CMLbbKIFyD6D{`hap>+zm| z)S9EOhppbFzSmx+*sy+!ecjsN;7@bDuGVTAh~9|@-o8&wHSE6_y6H8rEwD2s}qa#(UzdRfiYN$)A{yr8MXPv`?~PuJvq)z)ULm1Lx~lpgBBGb^vxM1PrAd2Hy5 z?l*hJ-)hU5{Y~w2H=Ze&d3^h%T1jdlA-#OFFzEDyKY^g2b01hge=vT`n1IRRe7YyW zJ*hY`VvhcAV!%b(fIQ-527H)VymRX^cn0_9a>I`@f(zVpvYQfc8UE9hP|($ed@_H6 z|39oJu0AS#r?&XB?$V`x&#!1OM&+cA3z-=4Z)e}qzs~KNh=t7_d+Ly%+3`kszoU5I zD{FY~lB*denp!+n}2}c%#wDTpr7@!dmZ^ zNXf1(_fdKV6q zQ_yTyFv69XISg4~5%R?dw=`r~v@01Ml&>cO;cjbBc3!y?g#|$qX*1)J7<3V1T+Ilq zjz&;;ir|t&vHxMeBymtRSk3^@B`mjqGqWCpC?swG&&~V~0MbO>sQ-B=o-TkWUg(i` zIy|upX@NHK22sroP>tPUzL+k63vs^HBcjDk&yaZM-7KoR1xhc0psB3h3^c9_2QGS0 z=Z=UU$rno4FlJG$0YHb*G>FUBn{1JZ$wM$eUiHauPdYyGqhBM5kLqii?rA;(c)mxlevLPWjp} zIjq;H^W<`$obfoy7b7m-H$Rf_^&?4NTsh`*2e@1YtMAE0Fwv}$F}c17v)FZvSR+); z^8yeX49*&yc^=@Ng%W7y#8HF-eho4VPHOd99ih`HaFWz9lvbf*NIjhZe%i&=A7+6z S39i;4a0UECl_aIBkpBYw-K8l2 literal 0 HcmV?d00001