VENDEDOR EN PEDIDO Y EN ADMINISTRADOR
This commit is contained in:
+136
-17
@@ -379,6 +379,23 @@ async function requireProductosManagement(req, res, next) {
|
|||||||
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'
|
// Permite acceder si es admin O tiene el permiso 'descargas'
|
||||||
async function requireDescargasAccess(req, res, next) {
|
async function requireDescargasAccess(req, res, next) {
|
||||||
const session = await getSessionUser(req);
|
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) => {
|
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) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
return res.status(400).json({ error: 'El pedido debe tener al menos un producto' });
|
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 filename = `pedido-${Date.now()}.pdf`;
|
||||||
const filepath = path.join(PEDIDOS_DIR, filename);
|
const filepath = path.join(PEDIDOS_DIR, filename);
|
||||||
|
|
||||||
// Asegurar que la carpeta existe
|
|
||||||
await fs.mkdir(PEDIDOS_DIR, { recursive: true });
|
await fs.mkdir(PEDIDOS_DIR, { recursive: true });
|
||||||
|
|
||||||
const doc = new PDFDocument({
|
const doc = new PDFDocument({
|
||||||
@@ -1925,7 +2035,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
|
|||||||
const MARGIN_X = 50;
|
const MARGIN_X = 50;
|
||||||
const pageH = doc.page.height;
|
const pageH = doc.page.height;
|
||||||
|
|
||||||
// === ENCABEZADO ===
|
|
||||||
doc.fontSize(36).fillColor('#8b5cf6').font('Helvetica-Bold')
|
doc.fontSize(36).fillColor('#8b5cf6').font('Helvetica-Bold')
|
||||||
.text('∞ Infinity', MARGIN_X, 50, { width: pageW - 2 * MARGIN_X });
|
.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')
|
doc.fontSize(10).fillColor('#8b8ba7').font('Helvetica')
|
||||||
.text(fechaStr, MARGIN_X, 150, { width: pageW - 2 * MARGIN_X });
|
.text(fechaStr, MARGIN_X, 150, { width: pageW - 2 * MARGIN_X });
|
||||||
|
|
||||||
// === DATOS DEL CLIENTE ===
|
if (vendedor) {
|
||||||
doc.fontSize(14).fillColor('#1a1a2e').font('Helvetica-Bold')
|
doc.fontSize(10).fillColor('#8b5cf6').font('Helvetica-Bold')
|
||||||
.text('Datos del cliente', MARGIN_X, 185, { width: pageW - 2 * MARGIN_X });
|
.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 = [
|
const clientFields = [
|
||||||
['Nombre', cliente.nombre],
|
['Nombre', cliente.nombre],
|
||||||
['Ciudad', cliente.ciudad],
|
['Ciudad', cliente.ciudad],
|
||||||
['Teléfono', cliente.telefono],
|
['Teléfono', cliente.telefono],
|
||||||
['Dirección', cliente.direccion]
|
['Dirección', cliente.direccion]
|
||||||
];
|
];
|
||||||
|
|
||||||
let clientY = 210;
|
let clientY = clientY0;
|
||||||
doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
|
doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
|
||||||
for (const [label, value] of clientFields) {
|
for (const [label, value] of clientFields) {
|
||||||
doc.text(`${label}: `, MARGIN_X, clientY, { continued: true });
|
doc.text(`${label}: `, MARGIN_X, clientY, { continued: true });
|
||||||
@@ -1961,7 +2075,6 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
|
|||||||
clientY += 18;
|
clientY += 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === TABLA DE PRODUCTOS ===
|
|
||||||
let tableY = clientY + 20;
|
let tableY = clientY + 20;
|
||||||
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
|
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
|
||||||
.strokeColor('#e8e6f0').lineWidth(0.5).stroke();
|
.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('Precio', colPrecio, tableY, { align: 'right', width: 60 });
|
||||||
doc.text('Total', colSubtotal, tableY, { align: 'right', width: 60 });
|
doc.text('Total', colSubtotal, tableY, { align: 'right', width: 60 });
|
||||||
|
|
||||||
const headerY = tableY;
|
|
||||||
|
|
||||||
let totalGeneral = 0;
|
let totalGeneral = 0;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const subtotal = (item.precio || 0) * (item.cantidad || 1);
|
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 });
|
.text(`$${subtotal.toLocaleString('es-CO')}`, colSubtotal, tableY, { align: 'right', width: 60 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Línea final
|
|
||||||
tableY += 20;
|
tableY += 20;
|
||||||
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
|
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
|
||||||
.strokeColor('#e8e6f0').lineWidth(0.5).stroke();
|
.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')
|
doc.fontSize(16).fillColor('#8b5cf6').font('Helvetica-Bold')
|
||||||
.text(` $${totalGeneral.toLocaleString('es-CO')}`, { width: pageW - 2 * MARGIN_X });
|
.text(` $${totalGeneral.toLocaleString('es-CO')}`, { width: pageW - 2 * MARGIN_X });
|
||||||
|
|
||||||
// === FOOTER con número de página ===
|
|
||||||
const range = doc.bufferedPageRange();
|
const range = doc.bufferedPageRange();
|
||||||
for (let i = range.start; i < range.start + range.count; i++) {
|
for (let i = range.start; i < range.start + range.count; i++) {
|
||||||
doc.switchToPage(i);
|
doc.switchToPage(i);
|
||||||
@@ -2027,13 +2136,23 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
|
|||||||
|
|
||||||
doc.end();
|
doc.end();
|
||||||
|
|
||||||
// Esperar a que termine de escribir
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
writeStream.on('finish', resolve);
|
writeStream.on('finish', resolve);
|
||||||
writeStream.on('error', reject);
|
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-Type', 'application/pdf');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
res.sendFile(filepath);
|
res.sendFile(filepath);
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
+110
-1
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1781199158204",
|
||||||
|
"nombre": "JUAN PABLO"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -2522,3 +2522,180 @@ select {
|
|||||||
min-width: 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const TABS = [
|
|||||||
{ id: 'agotados', label: 'Agotados', icon: '🚫' },
|
{ id: 'agotados', label: 'Agotados', icon: '🚫' },
|
||||||
{ id: 'descargas', label: 'Descargas', icon: '⬇️' },
|
{ id: 'descargas', label: 'Descargas', icon: '⬇️' },
|
||||||
{ id: 'categorias', label: 'Categorías', icon: '🗂️' },
|
{ id: 'categorias', label: 'Categorías', icon: '🗂️' },
|
||||||
{ id: 'usuarios', label: 'Usuarios', icon: '👥' }
|
{ id: 'usuarios', label: 'Usuarios', icon: '👥' },
|
||||||
|
{ id: 'pedidos', label: 'Pedidos', icon: '📋' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
@@ -166,6 +167,10 @@ function cambiarVista(view) {
|
|||||||
if (view === 'descargas') {
|
if (view === 'descargas') {
|
||||||
cargarDescargas();
|
cargarDescargas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (view === 'pedidos') {
|
||||||
|
cargarPedidos();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cargarEstadisticas() {
|
async function cargarEstadisticas() {
|
||||||
@@ -2163,6 +2168,203 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (btn) btn.addEventListener('click', confirmarEliminarProductoDef);
|
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 = '<p class="pedidos-empty" style="text-align:left;padding:8px 0">No hay vendedores. Agrega el primero.</p>';
|
||||||
|
} else {
|
||||||
|
listEl.innerHTML = vendedores.map(v => `
|
||||||
|
<div class="vendedor-item" data-id="${v.id}">
|
||||||
|
<span class="vendedor-nombre">${escapeHtml(v.nombre)}</span>
|
||||||
|
<button class="vendedor-remove" data-id="${v.id}" title="Eliminar vendedor">✕</button>
|
||||||
|
</div>
|
||||||
|
`).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 = `<p class="pedidos-empty" style="text-align:left;padding:8px 0">Error: ${err.message}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<option value="">Todos los vendedores</option>' +
|
||||||
|
vendedores.map(v => `<option value="${escapeHtml(v.nombre)}">${escapeHtml(v.nombre)}</option>`).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 = '<tr><td colspan="6" class="pedidos-empty">No autorizado</td></tr>';
|
||||||
|
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 = '<tr><td colspan="6" class="pedidos-empty">No hay pedidos aún.</td></tr>';
|
||||||
|
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 `<tr>
|
||||||
|
<td>${escapeHtml(fecha)}</td>
|
||||||
|
<td>${escapeHtml(clienteNombre)}</td>
|
||||||
|
<td>${escapeHtml(vendedorNombre)}</td>
|
||||||
|
<td>${itemsCount}</td>
|
||||||
|
<td class="pedidos-total">$${total.toLocaleString('es-CO')}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pedidos-actions-group">
|
||||||
|
<a href="/pedidos/${p.filename}" class="btn btn-ghost btn-sm" target="_blank" download>📄 Ver PDF</a>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm pedido-delete" data-filename="${p.filename}" title="Eliminar pedido">Eliminar</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
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 = `<tr><td colspan="6" class="pedidos-empty">Error: ${err.message}</td></tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// DESCARGAS — gestor genérico de árbol de selección
|
||||||
// Usado por los subtabs PDF y Fotos.
|
// Usado por los subtabs PDF y Fotos.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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="admin.css?v=17">
|
<link rel="stylesheet" href="admin.css?v=22">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -85,6 +85,10 @@
|
|||||||
<span class="nav-icon">👥</span>
|
<span class="nav-icon">👥</span>
|
||||||
<span class="nav-label">Usuarios</span>
|
<span class="nav-label">Usuarios</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="nav-item" data-view="pedidos">
|
||||||
|
<span class="nav-icon">📋</span>
|
||||||
|
<span class="nav-label">Pedidos</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -425,6 +429,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- PEDIDOS VIEW -->
|
||||||
|
<section class="view" data-view="pedidos">
|
||||||
|
<header class="view-header view-header-with-action">
|
||||||
|
<div>
|
||||||
|
<h1 class="view-title">📋 Pedidos</h1>
|
||||||
|
<p class="view-subtitle">Gestiona los vendedores y revisa los pedidos generados.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="view-section-group">
|
||||||
|
<!-- Gestión de vendedores -->
|
||||||
|
<div class="view-content-card">
|
||||||
|
<h3 class="view-section-title">Vendedores</h3>
|
||||||
|
<p class="view-section-desc">Agrega o quita los vendedores que aparecerán en el formulario de pedido público.</p>
|
||||||
|
|
||||||
|
<div class="vendedores-list" id="vendedoresList">
|
||||||
|
<p class="pdf-selector-loading">Cargando vendedores…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vendedores-add">
|
||||||
|
<div class="form-group" style="flex:1;margin-bottom:0">
|
||||||
|
<input type="text" id="vendedorInput" placeholder="Nombre del vendedor" maxlength="60">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="btnAgregarVendedor">+ Agregar</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-error" id="vendedorError" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listado de pedidos -->
|
||||||
|
<div class="view-content-card">
|
||||||
|
<div class="pedidos-filter-bar">
|
||||||
|
<label for="pedidosFilterVendedor">Filtrar por vendedor:</label>
|
||||||
|
<div class="form-group" style="flex:1;margin-bottom:0">
|
||||||
|
<select id="pedidosFilterVendedor">
|
||||||
|
<option value="">Todos los vendedores</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pedidos-table-wrap">
|
||||||
|
<table class="pedidos-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Cliente</th>
|
||||||
|
<th>Vendedor</th>
|
||||||
|
<th>Productos</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th class="pedidos-actions-col">Acción</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="pedidosListBody">
|
||||||
|
<tr><td colspan="6" class="pedidos-empty">No hay pedidos aún.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- BOTÓN FIJO: Cerrar sesión (esquina superior derecha) -->
|
<!-- BOTÓN FIJO: Cerrar sesión (esquina superior derecha) -->
|
||||||
@@ -677,6 +741,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
<script src="admin.js?v=19"></script>
|
<script src="admin.js?v=23"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+22
-3
@@ -457,10 +457,27 @@ function setupCartModals() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Generar PDF → abrir formulario
|
// Generar PDF → abrir formulario
|
||||||
document.getElementById('cartPdfBtn').addEventListener('click', () => {
|
document.getElementById('cartPdfBtn').addEventListener('click', async () => {
|
||||||
cerrarModal('cartModal');
|
cerrarModal('cartModal');
|
||||||
document.getElementById('orderForm').reset();
|
document.getElementById('orderForm').reset();
|
||||||
document.getElementById('orderError').classList.remove('show');
|
document.getElementById('orderError').classList.remove('show');
|
||||||
|
|
||||||
|
// Cargar vendedores en el select
|
||||||
|
const sel = document.getElementById('orderVendedor');
|
||||||
|
sel.innerHTML = '<option value="">Selecciona un vendedor</option>';
|
||||||
|
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');
|
abrirModal('orderModal');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -479,9 +496,10 @@ async function enviarPedido() {
|
|||||||
const ciudad = document.getElementById('orderCiudad').value.trim();
|
const ciudad = document.getElementById('orderCiudad').value.trim();
|
||||||
const telefono = document.getElementById('orderTelefono').value.trim();
|
const telefono = document.getElementById('orderTelefono').value.trim();
|
||||||
const direccion = document.getElementById('orderDireccion').value.trim();
|
const direccion = document.getElementById('orderDireccion').value.trim();
|
||||||
|
const vendedor = document.getElementById('orderVendedor').value;
|
||||||
const errEl = document.getElementById('orderError');
|
const errEl = document.getElementById('orderError');
|
||||||
|
|
||||||
if (!nombre || !ciudad || !telefono || !direccion) {
|
if (!nombre || !ciudad || !telefono || !direccion || !vendedor) {
|
||||||
errEl.textContent = 'Todos los campos son obligatorios';
|
errEl.textContent = 'Todos los campos son obligatorios';
|
||||||
errEl.classList.add('show');
|
errEl.classList.add('show');
|
||||||
return;
|
return;
|
||||||
@@ -502,7 +520,8 @@ async function enviarPedido() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
items: carrito.map(i => ({ ref: i.ref, nombre: i.nombre, precio: i.precio, cantidad: i.cantidad })),
|
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
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1158,7 +1158,8 @@ section {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1.5px solid #d4d0e0;
|
border: 1.5px solid #d4d0e0;
|
||||||
|
|||||||
+8
-2
@@ -9,7 +9,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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=6">
|
<link rel="stylesheet" href="estilos.css?v=7">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -163,6 +163,12 @@
|
|||||||
<label for="orderDireccion">Dirección</label>
|
<label for="orderDireccion">Dirección</label>
|
||||||
<input type="text" id="orderDireccion" required placeholder="Tu dirección">
|
<input type="text" id="orderDireccion" required placeholder="Tu dirección">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="orderVendedor">Vendedor</label>
|
||||||
|
<select id="orderVendedor" required>
|
||||||
|
<option value="">Selecciona un vendedor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-error" id="orderError" role="alert"></div>
|
<div class="form-error" id="orderError" role="alert"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,6 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js?v=7"></script>
|
<script src="app.js?v=8"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
xœÍX˪ãFÝë+ú¦§ž§Ô`¼˜É²$×»�…G– �É|ý¸#9ö�16ÆÆjšÒéSuªª‹%Jï8Qj�s[R÷ܼÿ®ÿç�®ÿõÇ©ûÜPvs*-iv´�DYAÑš²†¸ªKäm+êRŒ%}îþjþnxê
V»uNN)€L&iõܼÿA’"†æ·…÷BVШ¿%,Ê2ÑïiõSóýªù¥qJL”ž7ÏÒnþ<ÆÿñéíøŸ>þÜpöôoóô%L\NL¤™µ£×†9_A
|
|
||||||
!S±RDFRœ–I)-à0†B.pÝÄ*Gà/G‰Örð’Ó”¾„@1 ƒ‡.{ZH'¤¬$ƒa^ýŠáeEEëÛ�uïzÜõØÕB¿L ´X6õÿ™£ßÞE0äVÛ±�§7‡aO…NºLRÝÖ¡ÀчÁÏ¢—‡Òä’�D1\^4—"{×ñ|�H\×˺s!$tOªKä°9ÀÛbô•cpÈ2½«¡†î•2.#…|’E‰*8†=)š-d16Ûáè¶×_O…·1eØm™XÆÔõØ¥£=†‡á¦äŸ;‚úøy'(“áa¨ÉΡP”AÑ?
|
|
||||||
L–
<>a�ŠOèÆ§×UVIU¶>|YeËIU «%õô$^—Œ9•V·U–fªì«—g“}/»sÕÑÁ:kŽjR’Q$�×UûÿÍkTæyæ¦5†]iÔ<r ÎàÞÆt¹>“Še+eΰ-S»¥#jFíîÚÀX)ˆ¼'”
µñP’�²-wU±Îâ¬sµó]à�ñi+9¢=Ÿw·Þ³Ñ1—n3ô‰m»Ë¾:ZÏ'êÛÞ�x¥¡ô·£ü¶j‰¸D-íÝÕ2�ó±ÔrŒñ ~µLÒ÷pj™$ðëP~[µ¸_¢–¸»Z¦q>–ZŽ1>˜Z&é{8µLxe
àãÙšMÎÖnÐõß|ôa<y:\mw�i;ælõNw/'2K–àdŒl´Ÿ”òˤ”„Ä4nŸ÷þ«·¶ë
|
|
||||||
endstream
|
|
||||||
endobj
|
|
||||||
12 0 obj
|
|
||||||
<<
|
|
||||||
/Type /Page
|
|
||||||
/Parent 1 0 R
|
|
||||||
/MediaBox [0 0 595.28 841.89]
|
|
||||||
/Contents 10 0 R
|
|
||||||
/Resources 11 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
11 0 obj
|
|
||||||
<<
|
|
||||||
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
|
|
||||||
/Font <<
|
|
||||||
/F1 9 0 R
|
|
||||||
>>
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
10 0 obj
|
|
||||||
<<
|
|
||||||
/Length 131
|
|
||||||
/Filter /FlateDecode
|
|
||||||
>>
|
|
||||||
stream
|
|
||||||
xœmÍ»
|
|
||||||
Â@Ð~¾âþ€›™}Ì΀X`§l'VK$à÷ÛX¦=Í0;òsô…†ãô}õé~ÑWâPra7N¥¨iÄ&iÉÎâÊÕr’T±ö7}H¶†±ý]̓E5
’m¡á,0´™û$‘ã9ÉüD»Ò©Ñ�~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
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user