PDF PEDIDOS

This commit is contained in:
2026-06-09 10:05:00 -05:00
parent b19deda02c
commit d0581d36d1
9 changed files with 896 additions and 6 deletions
+157
View File
@@ -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
// ════════════════════════════════