PDF PEDIDOS
This commit is contained in:
@@ -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
|
||||
// ════════════════════════════════
|
||||
|
||||
+12
-4
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+256
@@ -275,10 +275,21 @@ function renderProductos() {
|
||||
<h3 class="producto-nombre">${escapeHtml(nombre)}</h3>
|
||||
${desc ? `<p class="producto-desc">${escapeHtml(desc)}</p>` : ''}
|
||||
<div class="producto-precio">${precio}</div>
|
||||
<div class="producto-card-add">
|
||||
<input type="number" class="producto-card-add-qty" value="1" min="1" max="99"
|
||||
data-ref="${p.ref}" aria-label="Cantidad">
|
||||
<button class="producto-card-add-btn" data-ref="${p.ref}"
|
||||
data-nombre="${escapeHtml(nombre)}"
|
||||
data-precio="${p.precio ?? ''}">
|
||||
+ Agregar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).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 = '<p class="cart-empty">El carrito está vacío.</p>';
|
||||
actualizarTotal();
|
||||
return;
|
||||
}
|
||||
|
||||
const totalGeneral = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0);
|
||||
|
||||
body.innerHTML = `<div class="cart-items">
|
||||
${carrito.map((item, idx) => {
|
||||
const subtotal = item.precio * item.cantidad;
|
||||
return `<div class="cart-item">
|
||||
<div class="cart-item-info">
|
||||
<div class="cart-item-name">${escapeHtml(item.nombre)}</div>
|
||||
<div class="cart-item-ref">${escapeHtml(item.ref)} — $${(item.precio || 0).toLocaleString('es-CO')} c/u</div>
|
||||
</div>
|
||||
<div class="cart-item-controls">
|
||||
<button class="cart-item-qty-btn" data-cart-dec="${idx}">−</button>
|
||||
<span class="cart-item-qty">${item.cantidad}</span>
|
||||
<button class="cart-item-qty-btn" data-cart-inc="${idx}">+</button>
|
||||
</div>
|
||||
<div class="cart-item-subtotal">$${subtotal.toLocaleString('es-CO')}</div>
|
||||
<button class="cart-item-remove" data-cart-remove="${idx}" title="Eliminar">✕</button>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+78
-2
@@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="estilos.css?v=5">
|
||||
<link rel="stylesheet" href="estilos.css?v=6">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -102,6 +102,82 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js?v=5"></script>
|
||||
<!-- CARRITO FLOTANTE -->
|
||||
<button class="cart-fab" id="cartFab" aria-label="Abrir carrito">
|
||||
<span class="cart-fab-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="cart-fab-badge" id="cartBadge">0</span>
|
||||
</button>
|
||||
|
||||
<!-- MODAL DEL CARRITO -->
|
||||
<div class="modal" id="cartModal">
|
||||
<div class="modal-backdrop" data-cart-close></div>
|
||||
<div class="modal-content cart-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>🛒 Carrito</h2>
|
||||
<button class="modal-close" data-cart-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body" id="cartBody">
|
||||
<p class="cart-empty">El carrito está vacío.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="cart-total" id="cartTotal">
|
||||
<span>Total:</span>
|
||||
<strong>$0</strong>
|
||||
</div>
|
||||
<div class="cart-actions">
|
||||
<button class="btn btn-ghost" id="cartClearBtn">Vaciar carrito</button>
|
||||
<button class="btn btn-primary" id="cartPdfBtn" disabled>📄 Generar PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL FORMULARIO DE PEDIDO -->
|
||||
<div class="modal" id="orderModal">
|
||||
<div class="modal-backdrop" data-order-close></div>
|
||||
<div class="modal-content order-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>📄 Datos del pedido</h2>
|
||||
<button class="modal-close" data-order-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="orderForm" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="orderNombre">Nombre completo</label>
|
||||
<input type="text" id="orderNombre" required placeholder="Tu nombre">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="orderCiudad">Ciudad</label>
|
||||
<input type="text" id="orderCiudad" required placeholder="Tu ciudad">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="orderTelefono">Teléfono</label>
|
||||
<input type="tel" id="orderTelefono" required placeholder="Tu teléfono">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="orderDireccion">Dirección</label>
|
||||
<input type="text" id="orderDireccion" required placeholder="Tu dirección">
|
||||
</div>
|
||||
<div class="form-error" id="orderError" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="cart-actions">
|
||||
<button class="btn btn-ghost" data-order-close>Cancelar</button>
|
||||
<button class="btn btn-primary" id="orderSubmitBtn">
|
||||
<span class="btn-text">✅ Generar pedido</span>
|
||||
<span class="btn-spinner" style="display:none">⏳</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=7"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user