diff --git a/backend/server.js b/backend/server.js index 395e866..b622eee 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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); diff --git a/data/pedidos-meta.json b/data/pedidos-meta.json new file mode 100644 index 0000000..6a78426 --- /dev/null +++ b/data/pedidos-meta.json @@ -0,0 +1,28 @@ +[ + { + "filename": "pedido-1781199786075.pdf", + "fecha": "2026-06-11T17:43:06.095Z", + "cliente": { + "nombre": "juan", + "ciudad": "medellin", + "telefono": "4444", + "direccion": "44444" + }, + "items": [ + { + "ref": "AF100-198", + "nombre": "BOLSO", + "precio": 10000, + "cantidad": 1 + }, + { + "ref": "AF100-197", + "nombre": "BOLSO", + "precio": 10000, + "cantidad": 1 + } + ], + "vendedor": "JUAN PABLO", + "total": 20000 + } +] diff --git a/data/sesiones.json b/data/sesiones.json index fe51488..9ba12ee 100644 --- a/data/sesiones.json +++ b/data/sesiones.json @@ -1 +1,110 @@ -[] +[ + { + "token": "079fdd39b3668e3b9e2fddd3226282aa3a5f45f0162db16b95746203dbe8af74", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781197392273 + }, + { + "token": "b34f81ffe57e3870875439696e6150eb5a1ea900ad610a25da409dc6b576e9ed", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781197414777 + }, + { + "token": "db6d37e43fe99bb6adb3f824425df3c3c5ef1cb638695578e1807659ad35e102", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781197734902 + }, + { + "token": "33bd4d4276521f5f800455a1a29ca5b4b9108a746ac602399aba623dc2c55d9d", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781197806825 + }, + { + "token": "63d52b8966cbf5df5449d8b0ae33efbbb4c75f8e1542e1e1ddde46d5c3e9cae1", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198005875 + }, + { + "token": "2784a09fe817c7654283e0ad7dfb68f6d80737b95f38fa863d76dcc7c8b7eb05", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198034642 + }, + { + "token": "c3c5b6b92ad256ea628da9c18ba298b2f95c637443dab0c64f3586eba400fb62", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198193732 + }, + { + "token": "fc8fcf45847ebdc70f89667a2cf462a2c8ed5a456e80afba7dacf9dfc6b8e151", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198211090 + }, + { + "token": "58217cadc5e85afb045d9c0d3dbfa40504916c2a4029b8598b0f1596036e3662", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198211127 + }, + { + "token": "012f430c03a3e605ef51f4c58dde55bf0b914c37c53e2f5bf123a79c0cd68eb7", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198383754 + }, + { + "token": "85e084066b269d9b5601f9ee721b45decbafebfc13ea083f27e3161d72777700", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198491124 + }, + { + "token": "25a5413011b12ba2508f960f6f743ff7c18868544a83479d6b1ecccf20d838a9", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198723225 + }, + { + "token": "1d0ebe5a9496bd1df9cfc8e7d49f10048b317a4833441bd0effc57c3225efa69", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781198967207 + }, + { + "token": "269e3c40159d2faeb0767216feae6e3e0d88c9d1c8ca9ab9d2497986ff1621b7", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781199074685 + }, + { + "token": "1bc1e7c6c2a505208e3165753de8abd2609c34590375e48ef1abfa587b58e64a", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781199372671 + }, + { + "token": "4dfe860e82bf21f1216ac252fa4ae378dee65914827df3c326b5812c4f69ff93", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781199472733 + }, + { + "token": "f8255e39ecef32f69c94e723ff4c7a06976ebfa4cbe92c8600f9644de97c4d0a", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781199500791 + }, + { + "token": "88000048fc7e0116dcf7101416ff75e119513a2e18ab9e7f87273b070ebbe0e2", + "usuario": "admin", + "rol": "admin", + "createdAt": 1781199793144 + } +] diff --git a/data/vendedores.json b/data/vendedores.json new file mode 100644 index 0000000..6992ed9 --- /dev/null +++ b/data/vendedores.json @@ -0,0 +1,6 @@ +[ + { + "id": "1781199158204", + "nombre": "JUAN PABLO" + } +] diff --git a/frontend/administrativo/admin.css b/frontend/administrativo/admin.css index 2a60200..45f561b 100644 --- a/frontend/administrativo/admin.css +++ b/frontend/administrativo/admin.css @@ -2522,3 +2522,180 @@ select { min-width: 0; } } + +/* ════════════════════════════════ + PEDIDOS VIEW + ════════════════════════════════ */ +.view-section-group { + display: flex; + flex-direction: column; + gap: 20px; +} + +.view-section-title { + font-size: 1rem; + font-weight: 700; + color: #1a1a2e; + margin: 0 0 4px; +} + +.view-section-desc { + font-size: 0.82rem; + color: #8b8ba7; + margin: 0 0 16px; +} + +/* Vendedores */ +.vendedores-list { + margin-bottom: 12px; +} + +.vendedor-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: #f8f7fc; + border-radius: 8px; + margin-bottom: 6px; +} + +.vendedor-nombre { + font-size: 0.9rem; + font-weight: 600; + color: #1a1a2e; +} + +.vendedor-remove { + background: none; + border: none; + color: #ef4444; + font-size: 0.9rem; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.15s; +} + +.vendedor-remove:hover { + background: #fef2f2; +} + +.vendedores-add { + display: flex; + gap: 8px; +} + +/* Filtro */ +.pedidos-filter-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; +} + +.pedidos-filter-bar label { + font-size: 0.85rem; + font-weight: 600; + color: #4a4a68; + white-space: nowrap; +} + +/* Tabla de pedidos */ +.pedidos-table-wrap { + overflow-x: auto; +} + +.pedidos-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; +} + +.pedidos-table th { + text-align: left; + padding: 10px 12px; + font-weight: 700; + color: #4a4a68; + border-bottom: 2px solid #e8e6f0; + white-space: nowrap; +} + +.pedidos-table td { + padding: 10px 12px; + border-bottom: 1px solid #e8e6f0; + color: #1a1a2e; +} + +.pedidos-table tbody tr:hover { + background: #f8f7fc; +} + +.pedidos-total { + font-weight: 700; + color: #8b5cf6; + white-space: nowrap; +} + +.pedidos-actions-col { + text-align: left; + width: 200px; +} + +.pedidos-table td:last-child, +.pedidos-table th:last-child { + white-space: nowrap; +} + +.pedidos-actions-group { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.pedido-delete { + background: none; + border: 1.5px solid #e0dce8; + color: #b91c1c; + padding: 6px 10px; + border-radius: 8px; + font-size: 0.75rem; + font-family: inherit; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; + line-height: 1; +} + +.pedido-delete:hover { + background: #fef2f2; + border-color: #b91c1c; +} + +.pedidos-empty { + text-align: center; + color: #8b8ba7; + padding: 24px 0; + font-size: 0.85rem; +} + +.btn-sm { + padding: 6px 10px; + font-size: 0.75rem; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 4px; +} + +@media (max-width: 600px) { + .vendedores-add { + flex-direction: column; + } + .pedidos-filter-bar { + flex-direction: column; + align-items: stretch; + } + .pedidos-filter-bar .form-group { + margin-bottom: 0; + } +} diff --git a/frontend/administrativo/admin.js b/frontend/administrativo/admin.js index 1f44386..6330d7e 100644 --- a/frontend/administrativo/admin.js +++ b/frontend/administrativo/admin.js @@ -12,7 +12,8 @@ const TABS = [ { id: 'agotados', label: 'Agotados', icon: '🚫' }, { id: 'descargas', label: 'Descargas', icon: '⬇️' }, { id: 'categorias', label: 'Categorías', icon: '🗂️' }, - { id: 'usuarios', label: 'Usuarios', icon: '👥' } + { id: 'usuarios', label: 'Usuarios', icon: '👥' }, + { id: 'pedidos', label: 'Pedidos', icon: '📋' } ]; const ROLES = { @@ -166,6 +167,10 @@ function cambiarVista(view) { if (view === 'descargas') { cargarDescargas(); } + + if (view === 'pedidos') { + cargarPedidos(); + } } async function cargarEstadisticas() { @@ -2163,6 +2168,203 @@ document.addEventListener('DOMContentLoaded', () => { if (btn) btn.addEventListener('click', confirmarEliminarProductoDef); }); +// ════════════════════════════════ +// PEDIDOS — vendedores + listado de pedidos +// ════════════════════════════════ +async function cargarPedidos() { + console.log('[pedidos] cargando…'); + setupVendedores(); + setupPedidosFiltro(); + try { await actualizarSelectVendedores(); } catch (e) { console.error('[pedidos] select error:', e); } + await Promise.all([ + cargarVendedores().catch(e => console.error('[pedidos] vendedores error:', e)), + cargarListadoPedidos().catch(e => console.error('[pedidos] listado error:', e)) + ]); +} + +// ─── VENDEDORES ─── +async function cargarVendedores() { + const listEl = document.getElementById('vendedoresList'); + const errEl = document.getElementById('vendedorError'); + errEl.classList.remove('show'); + + try { + const res = await fetch('/api/vendedores', { credentials: 'include' }); + if (!res.ok) throw new Error('Error al cargar vendedores'); + const vendedores = await res.json(); + + if (vendedores.length === 0) { + listEl.innerHTML = '
No hay vendedores. Agrega el primero.
'; + } else { + listEl.innerHTML = vendedores.map(v => ` +Error: ${err.message}
`; + } +} + +async function actualizarSelectVendedores() { + // Actualiza el select del modal público y el filtro de pedidos + try { + const res = await fetch('/api/vendedores'); + const vendedores = await res.json(); + const selects = ['pedidosFilterVendedor']; + selects.forEach(id => { + const sel = document.getElementById(id); + if (!sel) return; + const actual = sel.value; + sel.innerHTML = '' + + vendedores.map(v => ``).join(''); + if (actual) sel.value = actual; + }); + } catch {} +} + +function setupVendedores() { + const btn = document.getElementById('btnAgregarVendedor'); + const input = document.getElementById('vendedorInput'); + const errEl = document.getElementById('vendedorError'); + + if (!btn || btn.dataset.vendWired) return; + btn.dataset.vendWired = '1'; + + async function agregar() { + const nombre = input.value.trim(); + if (!nombre) { + errEl.textContent = 'Escribe un nombre de vendedor'; + errEl.classList.add('show'); + return; + } + errEl.classList.remove('show'); + try { + const res = await fetch('/api/vendedores', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ nombre }) + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Error al agregar'); + } + input.value = ''; + await cargarVendedores(); + await actualizarSelectVendedores(); + } catch (err) { + errEl.textContent = err.message; + errEl.classList.add('show'); + } + } + + btn.addEventListener('click', agregar); + input.addEventListener('keydown', e => { if (e.key === 'Enter') agregar(); }); +} + +// ─── LISTADO DE PEDIDOS ─── +async function cargarListadoPedidos() { + const tbody = document.getElementById('pedidosListBody'); + const filter = document.getElementById('pedidosFilterVendedor'); + const vendedor = filter ? filter.value : ''; + + try { + let url = '/api/pedidos'; + if (vendedor) url += `?vendedor=${encodeURIComponent(vendedor)}`; + const res = await fetch(url, { credentials: 'include' }); + if (!res.ok) { + tbody.innerHTML = 'Gestiona los vendedores y revisa los pedidos generados.
+Agrega o quita los vendedores que aparecerán en el formulario de pedido público.
+ +Cargando vendedores…
+| Fecha | +Cliente | +Vendedor | +Productos | +Total | +Acción | +
|---|---|---|---|---|---|
| No hay pedidos aún. | |||||