VENDEDOR EN PEDIDO Y EN ADMINISTRADOR

This commit is contained in:
2026-06-11 14:08:03 -05:00
parent ad3911d3c3
commit e4588b75db
14 changed files with 758 additions and 187 deletions
+136 -17
View File
@@ -379,6 +379,23 @@ async function requireProductosManagement(req, res, next) {
next();
}
async function requirePedidosManagement(req, res, next) {
const session = await getSessionUser(req);
if (!session) return res.status(401).json({ error: 'No autorizado' });
const user = await getUserData(session.usuario);
if (!user) return res.status(401).json({ error: 'No autorizado' });
const esAdmin = user.rol === 'admin';
const tienePermiso = (user.permisos || []).includes('pedidos');
if (!esAdmin && !tienePermiso) {
return res.status(403).json({ error: 'No tienes permiso para gestionar pedidos' });
}
req.user = user;
next();
}
// Permite acceder si es admin O tiene el permiso 'descargas'
async function requireDescargasAccess(req, res, next) {
const session = await getSessionUser(req);
@@ -1889,10 +1906,104 @@ app.post('/api/descargas/fotos', requireDescargasAccess, async (req, res) => {
});
// ════════════════════════════════
// PEDIDOS — generar PDF con pedido
// VENDEDORES — gestión (admin / permiso pedidos)
// ════════════════════════════════
const VENDEDORES_FILE = 'vendedores.json';
const PEDIDOS_META_FILE = 'pedidos-meta.json';
async function getVendedores() {
try {
return await readJson(VENDEDORES_FILE);
} catch {
return [];
}
}
async function getPedidosMeta() {
try {
return await readJson(PEDIDOS_META_FILE);
} catch {
return [];
}
}
app.get('/api/vendedores', async (req, res) => {
const vendedores = await getVendedores();
res.json(vendedores);
});
app.post('/api/vendedores', requirePedidosManagement, async (req, res) => {
const { nombre } = req.body || {};
if (!nombre || !nombre.trim()) {
return res.status(400).json({ error: 'El nombre del vendedor es obligatorio' });
}
const vendedores = await getVendedores();
const id = String(Date.now());
vendedores.push({ id, nombre: nombre.trim() });
await writeJson(VENDEDORES_FILE, vendedores);
res.json(vendedores);
});
app.delete('/api/vendedores/:id', requirePedidosManagement, async (req, res) => {
let vendedores = await getVendedores();
vendedores = vendedores.filter(v => v.id !== req.params.id);
await writeJson(VENDEDORES_FILE, vendedores);
res.json(vendedores);
});
// ════════════════════════════════
// PEDIDOS — generar PDF, listar y eliminar metadatos
// ════════════════════════════════
// Listar pedidos (admin / permiso pedidos)
app.get('/api/pedidos', requirePedidosManagement, async (req, res) => {
const { vendedor } = req.query;
let pedidos = await getPedidosMeta();
if (vendedor) {
pedidos = pedidos.filter(p => p.vendedor === vendedor);
}
res.json(pedidos);
});
// Eliminar pedido (admin / permiso pedidos)
app.delete('/api/pedidos/:filename', requirePedidosManagement, async (req, res) => {
const { filename } = req.params;
// Validar que el filename sea seguro (solo letras, números, guiones, puntos)
if (!/^[\w.-]+\.pdf$/.test(filename)) {
return res.status(400).json({ error: 'Nombre de archivo inválido' });
}
try {
// Eliminar archivo PDF
const filepath = path.join(PEDIDOS_DIR, filename);
try {
await fs.unlink(filepath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
// Si no existe el archivo, igual seguimos para limpiar metadatos
}
// Eliminar metadatos
const pedidosMeta = await getPedidosMeta();
const nuevosMeta = pedidosMeta.filter(p => p.filename !== filename);
if (nuevosMeta.length === pedidosMeta.length) {
return res.status(404).json({ error: 'Pedido no encontrado' });
}
await writeJson(PEDIDOS_META_FILE, nuevosMeta);
res.json({ ok: true });
} catch (err) {
console.error('Delete pedido error:', err);
res.status(500).json({ error: 'Error al eliminar el pedido: ' + err.message });
}
});
app.post('/api/pedidos/generar-pdf', async (req, res) => {
const { items, cliente } = req.body || {};
const { items, cliente, vendedor } = req.body || {};
if (!Array.isArray(items) || items.length === 0) {
return res.status(400).json({ error: 'El pedido debe tener al menos un producto' });
@@ -1905,7 +2016,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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({
@@ -1925,7 +2035,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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 });
@@ -1942,18 +2051,23 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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 });
if (vendedor) {
doc.fontSize(10).fillColor('#8b5cf6').font('Helvetica-Bold')
.text(`Vendedor: ${vendedor}`, MARGIN_X, 168, { width: pageW - 2 * MARGIN_X });
}
doc.fontSize(14).fillColor('#1a1a2e').font('Helvetica-Bold')
.text('Datos del cliente', MARGIN_X, vendedor ? 192 : 185, { width: pageW - 2 * MARGIN_X });
const clientY0 = vendedor ? 217 : 210;
const clientFields = [
['Nombre', cliente.nombre],
['Ciudad', cliente.ciudad],
['Nombre', cliente.nombre],
['Ciudad', cliente.ciudad],
['Teléfono', cliente.telefono],
['Dirección', cliente.direccion]
];
let clientY = 210;
let clientY = clientY0;
doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
for (const [label, value] of clientFields) {
doc.text(`${label}: `, MARGIN_X, clientY, { continued: true });
@@ -1961,7 +2075,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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();
@@ -1980,8 +2093,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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);
@@ -2005,7 +2116,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
.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();
@@ -2016,7 +2126,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
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);
@@ -2027,13 +2136,23 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
doc.end();
// Esperar a que termine de escribir
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
// Enviar el archivo
// Guardar metadatos del pedido
const pedidosMeta = await getPedidosMeta();
pedidosMeta.push({
filename,
fecha: new Date().toISOString(),
cliente,
items: items.map(i => ({ ref: i.ref, nombre: i.nombre, precio: i.precio, cantidad: i.cantidad })),
vendedor: vendedor || null,
total: totalGeneral
});
await writeJson(PEDIDOS_META_FILE, pedidosMeta);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.sendFile(filepath);