From e4588b75db311e67f5c27317e3ff5b37fbfa847d Mon Sep 17 00:00:00 2001 From: Juan Pablo Restrepo Date: Thu, 11 Jun 2026 14:08:03 -0500 Subject: [PATCH] VENDEDOR EN PEDIDO Y EN ADMINISTRADOR --- backend/server.js | 153 +++++++++++-- data/pedidos-meta.json | 28 +++ data/sesiones.json | 111 +++++++++- data/vendedores.json | 6 + frontend/administrativo/admin.css | 177 +++++++++++++++ frontend/administrativo/admin.js | 204 +++++++++++++++++- frontend/administrativo/index.html | 68 +++++- frontend/app.js | 25 ++- frontend/estilos.css | 3 +- frontend/index.html | 10 +- pedidos/pedido-1781012420688.pdf | Bin 2868 -> 0 bytes pedidos/pedido-1781017023680.pdf | Bin 2738 -> 0 bytes pedidos/pedido-1781097001875.pdf | 160 -------------- ...012802257.pdf => pedido-1781199786075.pdf} | Bin 2640 -> 2737 bytes 14 files changed, 758 insertions(+), 187 deletions(-) create mode 100644 data/pedidos-meta.json create mode 100644 data/vendedores.json delete mode 100644 pedidos/pedido-1781012420688.pdf delete mode 100644 pedidos/pedido-1781017023680.pdf delete mode 100644 pedidos/pedido-1781097001875.pdf rename pedidos/{pedido-1781012802257.pdf => pedido-1781199786075.pdf} (56%) 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 => ` +
+ ${escapeHtml(v.nombre)} + +
+ `).join(''); + } + + // Wire delete + listEl.querySelectorAll('.vendedor-remove').forEach(btn => { + btn.addEventListener('click', async () => { + const id = btn.dataset.id; + try { + const delRes = await fetch(`/api/vendedores/${id}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!delRes.ok) throw new Error('Error al eliminar'); + await cargarVendedores(); + actualizarSelectVendedores(); + } catch (err) { + errEl.textContent = err.message; + errEl.classList.add('show'); + } + }); + }); + } catch (err) { + listEl.innerHTML = `

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 = 'No autorizado'; + return; + } + const pedidos = await res.json(); + + // Ordenar por fecha descendente + pedidos.sort((a, b) => new Date(b.fecha) - new Date(a.fecha)); + + if (pedidos.length === 0) { + tbody.innerHTML = 'No hay pedidos aún.'; + return; + } + + const renderedRows = pedidos.map(p => { + const fecha = new Date(p.fecha).toLocaleDateString('es-CO', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit' + }); + const itemsCount = Array.isArray(p.items) ? p.items.reduce((s, i) => s + (i.cantidad || 1), 0) : 0; + const total = p.total || 0; + const clienteNombre = p.cliente ? p.cliente.nombre : '—'; + const vendedorNombre = p.vendedor || '—'; + return ` + ${escapeHtml(fecha)} + ${escapeHtml(clienteNombre)} + ${escapeHtml(vendedorNombre)} + ${itemsCount} + $${total.toLocaleString('es-CO')} + + + 📄 Ver PDF + + + + `; + }); + tbody.innerHTML = renderedRows.join(''); + + // Wire delete buttons + tbody.querySelectorAll('.pedido-delete').forEach(btn => { + btn.addEventListener('click', async () => { + const filename = btn.dataset.filename; + if (!confirm(`¿Eliminar el pedido "${filename}"? Esta acción no se puede deshacer.`)) return; + try { + const res = await fetch(`/api/pedidos/${encodeURIComponent(filename)}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Error al eliminar'); + } + await cargarListadoPedidos(); + } catch (err) { + alert('Error: ' + err.message); + } + }); + }); + } catch (err) { + tbody.innerHTML = `Error: ${err.message}`; + } +} + +function setupPedidosFiltro() { + const filter = document.getElementById('pedidosFilterVendedor'); + if (!filter || filter.dataset.pedWired) return; + filter.dataset.pedWired = '1'; + filter.addEventListener('change', cargarListadoPedidos); +} + // ════════════════════════════════ // DESCARGAS — gestor genérico de árbol de selección // Usado por los subtabs PDF y Fotos. diff --git a/frontend/administrativo/index.html b/frontend/administrativo/index.html index 9ce6a42..d13f390 100644 --- a/frontend/administrativo/index.html +++ b/frontend/administrativo/index.html @@ -7,7 +7,7 @@ - + @@ -85,6 +85,10 @@ 👥 Usuarios + @@ -425,6 +429,66 @@ + +
+
+
+

📋 Pedidos

+

Gestiona los vendedores y revisa los pedidos generados.

+
+
+ +
+ +
+

Vendedores

+

Agrega o quita los vendedores que aparecerán en el formulario de pedido público.

+ +
+

Cargando vendedores…

+
+ +
+
+ +
+ +
+ +
+ + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + +
FechaClienteVendedorProductosTotalAcción
No hay pedidos aún.
+
+
+
+
+ @@ -677,6 +741,6 @@ - + diff --git a/frontend/app.js b/frontend/app.js index 69451db..bbfd19c 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -457,10 +457,27 @@ function setupCartModals() { }); // Generar PDF → abrir formulario - document.getElementById('cartPdfBtn').addEventListener('click', () => { + document.getElementById('cartPdfBtn').addEventListener('click', async () => { cerrarModal('cartModal'); document.getElementById('orderForm').reset(); document.getElementById('orderError').classList.remove('show'); + + // Cargar vendedores en el select + const sel = document.getElementById('orderVendedor'); + sel.innerHTML = ''; + try { + const res = await fetch('/api/vendedores'); + if (res.ok) { + const vendedores = await res.json(); + vendedores.forEach(v => { + const opt = document.createElement('option'); + opt.value = v.nombre; + opt.textContent = v.nombre; + sel.appendChild(opt); + }); + } + } catch {} + abrirModal('orderModal'); }); @@ -479,9 +496,10 @@ async function enviarPedido() { const ciudad = document.getElementById('orderCiudad').value.trim(); const telefono = document.getElementById('orderTelefono').value.trim(); const direccion = document.getElementById('orderDireccion').value.trim(); + const vendedor = document.getElementById('orderVendedor').value; const errEl = document.getElementById('orderError'); - if (!nombre || !ciudad || !telefono || !direccion) { + if (!nombre || !ciudad || !telefono || !direccion || !vendedor) { errEl.textContent = 'Todos los campos son obligatorios'; errEl.classList.add('show'); return; @@ -502,7 +520,8 @@ async function enviarPedido() { 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 } + cliente: { nombre, ciudad, telefono, direccion }, + vendedor }) }); diff --git a/frontend/estilos.css b/frontend/estilos.css index ef35eca..8f5b452 100644 --- a/frontend/estilos.css +++ b/frontend/estilos.css @@ -1158,7 +1158,8 @@ section { margin-bottom: 4px; } -.form-group input { +.form-group input, +.form-group select { width: 100%; padding: 10px 14px; border: 1.5px solid #d4d0e0; diff --git a/frontend/index.html b/frontend/index.html index 8efb504..1bd5724 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,7 +9,7 @@ - + @@ -163,6 +163,12 @@ +
+ + +
@@ -178,6 +184,6 @@ - + diff --git a/pedidos/pedido-1781012420688.pdf b/pedidos/pedido-1781012420688.pdf deleted file mode 100644 index fb25417923e3cd024029e54669d9d3f930b901f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2868 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 S39i;4a0UECl_aIBkpBYw-K8l2 diff --git a/pedidos/pedido-1781097001875.pdf b/pedidos/pedido-1781097001875.pdf deleted file mode 100644 index 77b257c..0000000 --- a/pedidos/pedido-1781097001875.pdf +++ /dev/null @@ -1,160 +0,0 @@ -%PDF-1.3 -% -7 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [0 0 595.28 841.89] -/Contents 5 0 R -/Resources 6 0 R ->> -endobj -6 0 obj -<< -/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] -/Font << -/F2 8 0 R -/F1 9 0 R ->> ->> -endobj -5 0 obj -<< -/Length 800 -/Filter /FlateDecode ->> -stream -xX˪F+`$׻G |#916jSu%J8Qjs[Rܼ珮Pvs*-ivDYAњKm+R%}jnx VuNN)L&iܼA"淅BV%,2iSqJL7n<>po%L\NL׆9_A -!SRDFRI)-0B.p*G/Gr@1.{ZH'$ a^eEE۝uzBLX6E0V7aONLR֡чϢ䒝D1\^4"{|H\˺s!$tOK9bcp22.#|E*8= )-d16_O1emXإ=;y'(aΡPA? -L <>aOƧUVIU>|YeIU %$^9VUf쭫 g}/sÕ:kjRQ$UkTy5]ie+eΰ-S#jFX)' P-wUs]i+9=Ÿw޳1n3m˾:Z'ށxjD-2r~Lpj$P~[_Zq>Z1>Z&{8Lxe ٚMn|a> -endobj -11 0 obj -<< -/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] -/Font << -/F1 9 0 R ->> ->> -endobj -10 0 obj -<< -/Length 131 -/Filter /FlateDecode ->> -stream -xmͻ -@~}΀X`l'VK$X=0;òs}~WPra7Ni&irT7}H]̓E5 m,0$9Dҩэ~c& -endstream -endobj -14 0 obj -(PDFKit) -endobj -15 0 obj -(PDFKit) -endobj -16 0 obj -(D:20260610131001Z) -endobj -17 0 obj -(Pedido - juan carlo) -endobj -18 0 obj -(Infinity) -endobj -19 0 obj -(Pedido de productos) -endobj -13 0 obj -<< -/Producer 14 0 R -/Creator 15 0 R -/CreationDate 16 0 R -/Title 17 0 R -/Author 18 0 R -/Subject 19 0 R ->> -endobj -9 0 obj -<< -/Type /Font -/BaseFont /Helvetica -/Subtype /Type1 -/Encoding /WinAnsiEncoding ->> -endobj -8 0 obj -<< -/Type /Font -/BaseFont /Helvetica-Bold -/Subtype /Type1 -/Encoding /WinAnsiEncoding ->> -endobj -4 0 obj -<< ->> -endobj -3 0 obj -<< -/Type /Catalog -/Pages 1 0 R -/Names 2 0 R ->> -endobj -1 0 obj -<< -/Type /Pages -/Count 2 -/Kids [7 0 R 12 0 R] ->> -endobj -2 0 obj -<< -/Dests << - /Names [ -] ->> ->> -endobj -xref -0 20 -0000000000 65535 f -0000002095 00000 n -0000002159 00000 n -0000002033 00000 n -0000002012 00000 n -0000000224 00000 n -0000000125 00000 n -0000000015 00000 n -0000001910 00000 n -0000001813 00000 n -0000001299 00000 n -0000001209 00000 n -0000001096 00000 n -0000001692 00000 n -0000001503 00000 n -0000001528 00000 n -0000001553 00000 n -0000001589 00000 n -0000001627 00000 n -0000001654 00000 n -trailer -<< -/Size 20 -/Root 3 0 R -/Info 13 0 R -/ID [<6916e8768e29c3bfeec0704a55d5562e> <6916e8768e29c3bfeec0704a55d5562e>] ->> -startxref -2206 -%%EOF diff --git a/pedidos/pedido-1781012802257.pdf b/pedidos/pedido-1781199786075.pdf similarity index 56% rename from pedidos/pedido-1781012802257.pdf rename to pedidos/pedido-1781199786075.pdf index f98b63d6c308a7e5c30115797d9a5777d9af4ff4..f57985aa783cfedb2a7dcbeea084b07f482941d5 100644 GIT binary patch delta 1142 zcmca0vQc!xXJ&H?qsiM@lwZ~y;aL7sez%DW z3W^^JW&E#;JX!e0rRcH1^$lVx>!W*^BbNSM+`S{d2Yx&n5DI@ahh>=VPI0lt9_>TrDs(idRvvPUT@G+ z;JxV)Y%Fn_|M8~r@fNgTJiqf*X=ECnT-EizUrUtfBkIxF#!%iIq6{UImc(O z|NZ0rJNv%>|K441n{?3r>|bdwj+HVy8dz`NOLp${Q((1CyqF#|Lp_KmTA*d>?A7Ao z@_ZbX?yu5(;w*14|F4@e&y>|yfs;dh!amOW;@J&t+Yf{>wr_D@(|#w>+9DBqSK|29 zz2frobPomXw_E38c(bF+z>Rlb)r;tYmaA-k7skzZJFxuPy4~+N9>_92%l*4KZRL8! zt2V5sm$AA=7`{4|6SZ)OZSvWywRfvd-+K9R$-MnPV!TdGDo=bVGHq4xm4K8}Q_6ev zbPIU5eh#WX%)54`ebYRCuU^?S->M19T6IhnpF+6acTL*%pj!UVLCYI!(+(Z;F^HS= zN=UPW^TQ4CC+%PV>aneP63KSN`%09`_0@?Eli2hXzqM~HKeKz|9y=%3W0uF4=?eJE z-uim9);g5^eeDjZJg=?|O9fn(pJ%ryoTtnGHE8zXqeo9G+^di07HpYyC6n`qvGDpG z1}q0ZDe=8J8(kQ{x2^rcE@>I1?O8#KDsHNzh_LTa;5)bXrBr6nJ>Tb>bTh+(?Ket! zh}}*6P_~JGU83H{sGL9Bt2v6$5$-L7iUzEze6f{jHo4ay} z@++3NdaHJC`M~XROVI08y;D=u9|aZHPp-Se#0od@-I^H__T1x{$V7g_cb9Za97Pu& z)ASL4@%WX{&h?$s9ykBo^J#ioz^CU6{@lsqtnuG|v^rhpSa5`BXuj(cTJz%P z2`HMux_w7-9wnmgr)JmKGM1J2`xrEiB9?ALUR@H?qJm%h1pgQ_R5J0>dmb zGfXiP3k-J{ni?8RUdSoTXgXPwQ&rr|z!=jcV}r?CIqhPTlg-TyjZ7^pERvE9(^5>$ yEDcNzEt7$PkZf#`Xke#cV?jtsESH@fS8+*VQAtHnY8sc3p@k8bs;aBM8y5iONZBU< delta 1098 zcmdledO>8uXJ#`C)5#L7O7&;MPiIL-3f%o3eue+ZvU$sYHN9|^6F#9dA!2u;*s{A# zjUNvPc+}PJn3NOQ88t0Qrc!aCUR0_7+^RXdoP2!Nx6G)TekoikfA{tG%kt~{FHh&s z&-inzkduF%ZFDH-mw-n}58m885_DMGJh)Ze{r>9KF!819AMd|gKUc=;;n||>dU390 zEL^)UecD$2w|!FMwAJsk$#<6b?ZhVe_m=_toFRwAR=_H;RtWNO6h{|b6Y&zm+-1Ma?5U&JyXzPw9Ysx zYU8fWGopSc{FR%1W9v>w%jhd_|6g`Uh>9`JyY*gqqJ4ea1C703%O!>Gv%Kf?Jg${1 zEb5ke!{OE@vqee!lPwj+H1qc{Sr!RDqhJvQa2W?wR0WjDZ0(PWK6;a^Ew^r+@~LNv-FBB&ufo0ZmrK1{>#Hoz>2Pk0EWh)`&%E}b z`M1_tLHb^UtKRrnZ59_>voa# zrR;%a0=9<^>EGI5)}Oa8yx!R3_12Vcza8Ei=N29GJ?8A)cD`UmUgDa`TT?&%NT|DK z-6!L)Qhh;=v*m{b4fZ;N^8t0={$Bog!fN|m>D=%4@0QLw7pK*!6lKyXqx<3a?NxW~ zhDH9&{95+v#BceY%YnX|Rao9Kau`?|8k?Az7)_RBlVu4>EiTz?#a6)NU}$M>qF?|5 z3V8}#V1|K_fw2X;n4zVi5r&wBrQzgP9KMsiIYsQvEHUK`EilYBG_b@FGqAKUoP3K@ zwcgkQ(_RxpV+?yuu(;0z(;tSW24)y~O^uPvD=A9M%tYG9mbnrxPoVrrU_XkwmXY?*4DmTF+9U}Hf@Ni3J09anKlVo^y&QED2Ok%5IN Lm#V6(zZ(|-*S){s