From 7ad00e45bbfbb0b1b551c3c7c81c5c08ee42e8b3 Mon Sep 17 00:00:00 2001 From: Pablinux Date: Tue, 5 Aug 2025 02:08:31 -0500 Subject: [PATCH] Consulta Detalle de pedido --- .env | 13 + .env.example | 13 + package-lock.json | 12 + package.json | 1 + src/app.js | 5 + src/config.js | 17 +- src/controladores/controlador_General.js | 7 +- src/controladores/controlador_Items.js | 70 ++-- src/controladores/controlador_clientes_api.js | 39 +++ src/controladores/controlador_cloud.js | 13 +- src/controladores/controlador_items_api.js | 50 +++ src/controladores/controlador_pedidos_api.js | 85 +++++ src/public/css/app_pedidos_v2.css | 50 +++ src/public/js/app_pedidos_v2.js | 314 ++++++++++++++++++ src/rutas/rt_Generales.js | 2 +- src/rutas/rt_api_v2.js | 90 +++++ src/rutas/rt_items.js | 1 + src/scripts/helpers.js | 16 + src/swager_config.js | 1 + src/views/app_pedidos_v2.ejs | 170 ++++++++++ 20 files changed, 913 insertions(+), 56 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 src/controladores/controlador_clientes_api.js create mode 100644 src/controladores/controlador_items_api.js create mode 100644 src/controladores/controlador_pedidos_api.js create mode 100644 src/public/css/app_pedidos_v2.css create mode 100644 src/public/js/app_pedidos_v2.js create mode 100644 src/rutas/rt_api_v2.js create mode 100644 src/scripts/helpers.js create mode 100644 src/views/app_pedidos_v2.ejs diff --git a/.env b/.env new file mode 100644 index 0000000..4e1aa06 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# Server Configuration +PORT=3001 + +# Database Configuration +DB_HOST=192.168.10.150 +DB_PORT=3306 +DB_USER=admin +DB_PASSWORD='Dx.1706%' +DB_NAME=TELCOTRONICS + +# Security / Session +SESSION_SECRET='Microbot%' +JWT_SECRET='Microbot&' \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1dfc821 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Server Configuration +PORT=3001 + +# Database Configuration +DB_HOST= +DB_PORT=3306 +DB_USER= +DB_PASSWORD= +DB_NAME= + +# Security / Session +SESSION_SECRET= +JWT_SECRET= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 09e562c..af2108b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "cors": "^2.8.5", + "dotenv": "^17.2.1", "ejs": "^3.0.2", "express": "^4.18.2", "express-fileupload": "^1.3.1", @@ -1089,6 +1090,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index ad0ecc6..2c5a006 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "cors": "^2.8.5", + "dotenv": "^17.2.1", "ejs": "^3.0.2", "express": "^4.18.2", "express-fileupload": "^1.3.1", diff --git a/src/app.js b/src/app.js index 9655d35..6fe8605 100644 --- a/src/app.js +++ b/src/app.js @@ -7,6 +7,9 @@ const cloud_file = require('express-fileupload'); const myConecction = require('express-myconnection'); const cors_origins = require('cors'); +// Cargar variables de entorno desde el archivo .env +require('dotenv').config(); + //inicio de sessiones const session = require('express-session'); //const jwt = require('jwt'); @@ -25,6 +28,7 @@ const cloud_rutas = require('./rutas/rt_cloud'); const app_restaurant = require('./rutas/rt_apps'); const app_arduino = require('./rutas/rt_arduino'); const proyectos = require('./rutas/rt_proyectos'); +const apiV2Rutas = require('./rutas/rt_api_v2'); // NUEVA RUTA //configuraciones app.set('port',process.env.PORT||puerto); @@ -76,6 +80,7 @@ app.use('/', cloud_rutas); app.use('/', app_restaurant); app.use('/', app_arduino); app.use('/', proyectos); +app.use('/', apiV2Rutas); // AÑADIR NUEVA RUTA //prueba de json directa app.get('/pruebaJson',function(req,res){ diff --git a/src/config.js b/src/config.js index 2cd912d..a4afade 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,12 @@ const path = require('path'); + const config = { db:{ - host: '192.168.10.150', - port: 3306, - user: 'admin', - pswd: 'Dx.1706%', - db_a: 'TELCOTRONICS', + host: process.env.DB_HOST || '192.168.10.150', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'admin', + pswd: process.env.DB_PASSWORD || 'Dx.1706%', + db_a: process.env.DB_NAME || 'TELCOTRONICS', debg: false, sock: '/' }, @@ -22,7 +23,7 @@ const config = { role:"", }, sessionStorage:{ - secretSession: "Microbot%", + secretSession: process.env.SESSION_SECRET || "Microbot%", cookie: false // CORREGIDO }, origin:{ @@ -30,10 +31,10 @@ const config = { any:{}, }, secret:{ - key:"Microbot&" + key: process.env.JWT_SECRET || "Microbot&" }, server:{ - port:3001 + port: process.env.PORT || 3001 } } diff --git a/src/controladores/controlador_General.js b/src/controladores/controlador_General.js index c5bb265..77cbbbb 100644 --- a/src/controladores/controlador_General.js +++ b/src/controladores/controlador_General.js @@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken'); const config = require('../config'); const { base64encode, base64decode } = require('nodejs-base64'); const { v4: uuidv4 } = require('uuid'); +const { stringTo_md5 } = require('../scripts/helpers'); controlador.verVentasJson = (req, res) => { req.getConnection((err, conn) => { @@ -65,12 +66,6 @@ controlador.auth = (req, res) => { //res.render(''); }; -function stringTo_md5(data_string) { - var crypto = require('crypto'); - const md5 = crypto.createHash('md5').update(data_string).digest('hex'); - console.log("MD5: ", md5); - return md5; -} async function keygen(conection) { var key = ""; await conection.query(`SELECT * FROM empresa_datos`, (err, rows) => { diff --git a/src/controladores/controlador_Items.js b/src/controladores/controlador_Items.js index 43a41ca..41ff7f0 100644 --- a/src/controladores/controlador_Items.js +++ b/src/controladores/controlador_Items.js @@ -183,7 +183,7 @@ controlador.app_consultaItemsPrecios = (req, res) => { precio, img as imagen FROM ver_inventario_precios_app - where grupo_precio = ? and (nombre like ? or descripcion like ?)` + where grupo_precio = ? and (nombre like ? or descripcion like ?) LIMIT 30` , [grupo, item, item], (err, rows) => { //res.json(rows); //if(err) return res.status(500).send("Error en Consulta de Items"); @@ -268,7 +268,7 @@ controlador.item_xCat = (req, res) => { }; controlador.app_itemsTab = (req, res) => { - var items=""; + var items = ""; var btnAdd = ` @@ -292,18 +292,18 @@ controlador.app_itemsTab = (req, res) => { try { if (rows.length > 0) { for (var i = 0; i < rows.length; i++) { - items += ` + items += ` ${rows[i].codigoProducto} ${rows[i].cant_pdcto} ${rows[i].descipcion} ${rows[i].costo} - ${(rows[i].cant_pdcto*rows[i].costo)} + ${(rows[i].cant_pdcto * rows[i].costo)} `; - console.log(rows[0].codigoProducto); + console.log(rows[0].codigoProducto); } - - res.send(items+btnAdd+detalle); + + res.send(items + btnAdd + detalle); } else { res.json({ auth: false, message: 'Unauthorized' }); } @@ -314,21 +314,21 @@ controlador.app_itemsTab = (req, res) => { }); }; - // --- Nueva Función para Productos Ensamblados --- +// --- Nueva Función para Productos Ensamblados --- - /** - * @function listarProductosEnsamblados - * @description Lista todos los productos ensamblados disponibles. - * @param {Object} req - Objeto de solicitud de Express. - * @param {Object} res - Objeto de respuesta de Express. - */ - controlador.listarProductosEnsamblados = (req, res) => { - req.getConnection((err, connection) => { - if (err) { - console.error('Error al obtener conexión para listar productos ensamblados:', err); - return res.status(500).json({ mensaje: 'Error interno del servidor al obtener conexión', error: err.message }); - } - connection.query(` +/** + * @function listarProductosEnsamblados + * @description Lista todos los productos ensamblados disponibles. + * @param {Object} req - Objeto de solicitud de Express. + * @param {Object} res - Objeto de respuesta de Express. + */ +controlador.listarProductosEnsamblados = (req, res) => { + req.getConnection((err, connection) => { + if (err) { + console.error('Error al obtener conexión para listar productos ensamblados:', err); + return res.status(500).json({ mensaje: 'Error interno del servidor al obtener conexión', error: err.message }); + } + connection.query(` SELECT PdctEnsb_ID, PdctEnsb_codigoEnsamble, @@ -338,13 +338,25 @@ controlador.app_itemsTab = (req, res) => { FROM productos_Ensamblados `, (err, rows) => { - if (err) { - console.error('Error al listar productos ensamblados:', err); - return res.status(500).json({ mensaje: 'Error interno del servidor al listar productos ensamblados', error: err.message }); - } - res.json(rows); - }); - }); - } + if (err) { + console.error('Error al listar productos ensamblados:', err); + return res.status(500).json({ mensaje: 'Error interno del servidor al listar productos ensamblados', error: err.message }); + } + res.json(rows); + }); + }); +} +controlador.pedido_detalle = (req, res) => { + const idPedido = req.params.id_pedido; + req.getConnection((err, conn) => { + conn.query('SELECT * FROM ver_detallePedidos WHERE idPedido = ?', [idPedido], (err, rows) => { + if (err) { + return res.status(500).json({ error: 'Error al obtener el detalle del pedido' }); + } + rows.forEach(dat => { dat.img = blob_a_b64(dat.img); }); + res.json(rows); + }); + }); +} module.exports = controlador; diff --git a/src/controladores/controlador_clientes_api.js b/src/controladores/controlador_clientes_api.js new file mode 100644 index 0000000..3df5d5b --- /dev/null +++ b/src/controladores/controlador_clientes_api.js @@ -0,0 +1,39 @@ +const controlador = {}; + +/** + * @description Consulta de clientes optimizada para la nueva app de pedidos (v2). + * Devuelve los campos necesarios para el modal de búsqueda de clientes. + * @param {Object} req - Objeto de solicitud de Express (req.query.consulta). + * @param {Object} res - Objeto de respuesta de Express. + */ +controlador.getClientesForV2 = (req, res) => { + const consulta = "%" + (req.query.consulta || '') + "%"; + + req.getConnection((err, conn) => { + if (err) { + console.error("Error al obtener conexión para getClientesForV2:", err); + return res.status(500).json({ message: "Error de conexión con la base de datos." }); + } + + // Usamos una consulta similar a las existentes, pero seleccionando solo los campos necesarios + const sql = ` + SELECT + client_id, + client_nombre, + client_rucCed + FROM clientes + WHERE client_nombre LIKE ? OR client_rucCed LIKE ? + LIMIT 50; + `; + + conn.query(sql, [consulta, consulta], (err, rows) => { + if (err) { + console.error("Error en la consulta getClientesForV2:", err); + return res.status(500).json({ message: "Error en la consulta de clientes." }); + } + res.json(rows); // Devolvemos el array directamente + }); + }); +}; + +module.exports = controlador; \ No newline at end of file diff --git a/src/controladores/controlador_cloud.js b/src/controladores/controlador_cloud.js index 95f78eb..20644d8 100644 --- a/src/controladores/controlador_cloud.js +++ b/src/controladores/controlador_cloud.js @@ -2,6 +2,7 @@ const controlador = {}; //const dirPath = "/home/pablinux/Projects/Node/APP-SIGMA-WEB/src/public/files/"; //const var_locals = ; //********* APP-panel control ********// +const { stringTo_md5 } = require('../scripts/helpers'); const path = require('path'); controlador.upload = (req, res) => { if (!req.files || Object.keys(req.files).length === 0) { @@ -92,16 +93,4 @@ controlador.cloud_panel = (req, res, next) => { }); }; -function stringTo_md5(data_string){ - var crypto = require('crypto'); - const md5 = crypto.createHash('md5').update(data_string).digest('hex'); - console.log("MD5: ", md5); - return md5; -} -function token(data_string){ - var crypto = require('crypto'); - const md5 = crypto.createHash('md5').update(data_string).digest('hex'); - //console.log("MD5: ", md5); - return md5; -} module.exports = controlador; diff --git a/src/controladores/controlador_items_api.js b/src/controladores/controlador_items_api.js new file mode 100644 index 0000000..455f0e3 --- /dev/null +++ b/src/controladores/controlador_items_api.js @@ -0,0 +1,50 @@ +const controlador = {}; + +function blob_a_b64(blob) { + if (blob != null) { + return blob.toString('base64'); + } + return ""; +} + +/** + * @description Consulta de items optimizada para la nueva app de pedidos (v2). + * Devuelve todos los campos necesarios para la UI, incluyendo idt_prdcto. + * @param {Object} req - Objeto de solicitud de Express (req.query.consulta, req.query.gp_precio). + * @param {Object} res - Objeto de respuesta de Express. + */ +controlador.getItemsForV2 = (req, res) => { + const item = "%" + (req.query.consulta || '') + "%"; + const grupo = req.query.gp_precio || 'PUBLICO'; + + req.getConnection((err, conn) => { + if (err) { + console.error("Error al obtener conexión para getItemsForV2:", err); + return res.status(500).json({ message: "Error de conexión con la base de datos." }); + } + + const sql = ` + SELECT + idt_prdcto, + codigo as codigo_prdcto, + nombre as detalle_prdcto, + descripcion as describe_prdcto, + precio as precio_vta, + img as url_img + FROM ver_inventario_precios_app + WHERE grupo_precio = ? AND (nombre LIKE ? OR codigo LIKE ?) + LIMIT 100; + `; + + conn.query(sql, [grupo, item, item], (err, rows) => { + if (err) { + console.error("Error en la consulta getItemsForV2:", err); + return res.status(500).json({ message: "Error en la consulta de ítems." }); + } + rows.forEach(dat => { dat.url_img = blob_a_b64(dat.url_img); }); + res.json(rows); + }); + }); +}; + +module.exports = controlador; \ No newline at end of file diff --git a/src/controladores/controlador_pedidos_api.js b/src/controladores/controlador_pedidos_api.js new file mode 100644 index 0000000..75bae82 --- /dev/null +++ b/src/controladores/controlador_pedidos_api.js @@ -0,0 +1,85 @@ +const controlador = {}; + +/** + * @description Obtiene la fecha y hora actual en formato para la BD (YYYY-MM-DD HH:MM:SS). + * @returns {string} La fecha y hora formateada. + */ +const reg_DB = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const hora = `${String(today.getHours()).padStart(2, '0')}:${String(today.getMinutes()).padStart(2, '0')}:${String(today.getSeconds()).padStart(2, '0')}`; + return `${year}-${month}-${day} ${hora}`; +}; + +/** + * @description Crea un nuevo pedido (cabecera y detalle) de forma transaccional. + * Espera un cuerpo JSON con la estructura del pedido. + * Ruta: POST /api/v2/pedidos + */ +controlador.crearPedido = async (req, res) => { + const pedidoData = req.body; + + // Validación básica del payload + if (!pedidoData || !pedidoData.clienteId || !Array.isArray(pedidoData.items) || pedidoData.items.length === 0) { + return res.status(400).json({ message: "Datos del pedido incompletos o inválidos." }); + } + + let conn; + try { + // Obtener una conexión del pool y usar su versión de promesas + req.getConnection(async (err, connection) => { + if (err) { + console.error("Error al obtener conexión de la BD:", err); + return res.status(500).json({ message: "Error de conexión con la base de datos." }); + } + conn = connection; + const promiseConn = conn.promise(); + + await promiseConn.beginTransaction(); + + // 1. Insertar la cabecera del pedido + const cabeceraPedido = { + PedUsoPrdct_Num: "1", // O un número de secuencia si lo tienes + PedUsoPrdct_idClient: pedidoData.clienteId, + PedUsoPrdct_reg: reg_DB(), + PedUsoPrdct_estado: pedidoData.estado || 'ACTIVO', + PedUsoPrdct_plataforma: pedidoData.plataforma || 'APP-SIGMA-WEB-V2', + PedUsoPrdct_usuario: pedidoData.user || 'WebAppUser', + PedUsoPrdct_valor: pedidoData.valor, + PedUsoPrdct_iva: pedidoData.iva, + PedUsoPrdct_origen: pedidoData.origen || 'WebApp' + }; + + const [resultCabecera] = await promiseConn.query('INSERT INTO PedidoUsoProduct SET ?', [cabeceraPedido]); + const idPedidoInsertado = resultCabecera.insertId; + + if (!idPedidoInsertado) { + throw new Error("No se pudo obtener el ID del pedido insertado."); + } + + // 2. Preparar y insertar el detalle del pedido + const itemsParaInsertar = pedidoData.items.map(item => [ + idPedidoInsertado, item.cod, item.cant, item.precio, + item.descuento || 0, 0, // IVA por item, 0 por ahora + item.gp_precio || 'PUBLICO', item.topings || '' + ]); + + const sqlDetalle = 'INSERT INTO PedidoUsoProduct_detalle (PedUsoPrdct_id, PedUsoPrdctDet_codigoProducto, PedUsoPrdct_cant, PedUsoPrdct_costo, PedUsoPrdct_desct, PedUsoPrdct_iva, PedUsoPrdct_gpPrecios, PedUsoPrdct_observacion) VALUES ?'; + await promiseConn.query(sqlDetalle, [itemsParaInsertar]); + + // 3. Si todo fue bien, confirmar la transacción + await promiseConn.commit(); + conn.release(); // Liberar la conexión + + res.status(201).json({ id: idPedidoInsertado, message: "Pedido Ingresado Correctamente" }); + }); + } catch (error) { + if (conn) await conn.promise().rollback(); // Revertir en caso de error + console.error("Error al crear el pedido:", error); + res.status(500).json({ message: "Error al procesar el pedido", error: error.message }); + } +}; + +module.exports = controlador; \ No newline at end of file diff --git a/src/public/css/app_pedidos_v2.css b/src/public/css/app_pedidos_v2.css new file mode 100644 index 0000000..1247c6c --- /dev/null +++ b/src/public/css/app_pedidos_v2.css @@ -0,0 +1,50 @@ +/* Custom Styles for SIGMA APP v2 */ + +:root { + --bs-primary-rgb: 23, 107, 135; /* Un azul corporativo */ + --bs-primary: #176B87; +} + +body { + background-color: #f8f9fa; +} + +.navbar-brand { + font-weight: 500; +} + +/* Estilo para las tarjetas de producto */ +.product-card { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + cursor: pointer; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.product-card .card-img-top { + width: 100%; + height: 150px; + object-fit: cover; /* Asegura que la imagen cubra el espacio sin deformarse */ +} + +.product-card .card-title { + font-size: 1rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.product-card .card-text { + font-size: 0.85rem; + color: #6c757d; +} + +/* Sidebar de Pedido Fijo */ +#order-sidebar { + position: sticky; + top: 80px; /* Altura del header + margen */ +} \ No newline at end of file diff --git a/src/public/js/app_pedidos_v2.js b/src/public/js/app_pedidos_v2.js new file mode 100644 index 0000000..de3ceb1 --- /dev/null +++ b/src/public/js/app_pedidos_v2.js @@ -0,0 +1,314 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- STATE MANAGEMENT --- + const state = { + orderItems: [], + selectedClient: null, + productCatalog: [], // Almacenará los productos cargados + }; + + // --- DOM ELEMENTS --- + const elements = { + searchInput: document.getElementById('search-input'), + productGrid: document.getElementById('product-grid'), + gridFeedback: document.getElementById('grid-feedback'), + loadingSpinner: document.getElementById('loading-spinner'), + noResultsMessage: document.getElementById('no-results-message'), + orderItemsList: document.getElementById('order-items-list'), + emptyCartMessage: document.getElementById('empty-cart-message'), + orderTotals: document.getElementById('order-totals'), + subtotalValue: document.getElementById('subtotal-value'), + ivaValue: document.getElementById('iva-value'), + totalValue: document.getElementById('total-value'), + clientNameInput: document.getElementById('client-name'), + clientSearchInput: document.getElementById('client-search-input'), + clientSearchResults: document.getElementById('client-search-results'), + submitOrderBtn: document.getElementById('submit-order-btn'), + clearOrderBtn: document.getElementById('clear-order-btn'), + toast: new bootstrap.Toast(document.getElementById('notification-toast')), + toastTitle: document.getElementById('toast-title'), + toastBody: document.getElementById('toast-body'), + }; + + // --- UTILITY FUNCTIONS --- + const debounce = (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + func.apply(this, args); + }, delay); + }; + }; + + const showToast = (title, message, isError = false) => { + elements.toastTitle.textContent = title; + elements.toastBody.textContent = message; + const toastEl = document.getElementById('notification-toast'); + toastEl.classList.toggle('bg-danger', isError); + toastEl.classList.toggle('text-white', isError); + elements.toast.show(); + }; + + // --- API CALLS --- + const fetchProducts = async (query = '') => { + elements.productGrid.innerHTML = ''; + elements.gridFeedback.style.display = 'block'; + elements.loadingSpinner.style.display = 'block'; + elements.noResultsMessage.style.display = 'none'; + + try { + // CORREGIDO: Se usa 'consulta' y se añade 'gp_precio' como en la app anterior. + const gp_precio = 'PUBLICO'; // O un valor dinámico si lo implementas + const response = await fetch(`/api/v2/items?consulta=${encodeURIComponent(query)}&gp_precio=${gp_precio}`); + if (!response.ok) throw new Error('Network response was not ok'); + const data = await response.json(); + // Guardamos los productos en el catálogo y luego renderizamos + state.productCatalog = data; // Asumiendo que la API devuelve un array directamente + renderProducts(state.productCatalog); + } catch (error) { + console.error('Error fetching products:', error); + elements.noResultsMessage.textContent = 'Error al cargar productos.'; + elements.noResultsMessage.style.display = 'block'; + } finally { + elements.loadingSpinner.style.display = 'none'; + } + }; + + const fetchClients = async (query) => { + try { + // CAMBIO: Usar el nuevo endpoint y el parámetro 'consulta' + const response = await fetch(`/api/v2/clientes?consulta=${encodeURIComponent(query)}`); + if (!response.ok) throw new Error('Network response was not ok'); + const data = await response.json(); + // CAMBIO: La nueva API devuelve un array directamente, no un objeto { Clientes: [...] } + renderClientSearchResults(data); + } catch (error) { + console.error('Error fetching clients:', error); + } + }; + + const submitOrder = async () => { + elements.submitOrderBtn.disabled = true; + elements.submitOrderBtn.innerHTML = ` + + Enviando... + `; + + const subtotal = state.orderItems.reduce((acc, item) => acc + parseFloat(item.price) * item.quantity, 0); + const iva = subtotal * 0.12; + const total = subtotal + iva; + + const orderPayload = { + clienteId: state.selectedClient.id, + user: 'WebAppUser', // O obtener del usuario logueado + estado: 'ACTIVO', + valor: total.toFixed(2), + iva: iva.toFixed(2), + plataforma: 'APP-SIGMA-WEB-V2', + origen: 'WebApp', + items: state.orderItems.map(item => ({ + cod: item.code, + cant: item.quantity, + precio: item.price, + descuento: 0, + gp_precio: 'PUBLICO', // O el grupo de precio correspondiente + topings: '' // No implementado aún + })) + }; + + try { + const response = await fetch('/api/v2/pedidos', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(orderPayload), + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.message || 'Error al enviar el pedido.'); + showToast('Pedido Enviado', `El pedido #${result.id} ha sido enviado correctamente.`); + clearOrder(); + } catch (error) { + console.error('Error submitting order:', error); + showToast('Error', error.message, true); + } finally { + elements.submitOrderBtn.innerHTML = ' Enviar Pedido'; + updateSubmitButtonState(); + } + }; + + // --- RENDERING --- + const productCardTemplate = (product) => ` +
+
+ ${product.detalle_prdcto} +
+
${product.detalle_prdcto}
+

${product.codigo_prdcto}

+
+ $${parseFloat(product.precio_vta).toFixed(2)} + +
+
+
+
+ `; + + const renderProducts = (products) => { + if (!products || products.length === 0) { + elements.noResultsMessage.style.display = 'block'; + elements.gridFeedback.style.display = 'block'; + elements.productGrid.innerHTML = ''; + } else { + elements.gridFeedback.style.display = 'none'; + elements.productGrid.innerHTML = products.map(productCardTemplate).join(''); + } + }; + + const renderOrder = () => { + if (state.orderItems.length === 0) { + elements.orderItemsList.innerHTML = ''; + elements.emptyCartMessage.style.display = 'block'; + elements.orderTotals.style.display = 'none'; + } else { + elements.emptyCartMessage.style.display = 'none'; + elements.orderTotals.style.display = 'block'; + elements.orderItemsList.innerHTML = state.orderItems.map((item, index) => ` +
+
+
${item.nombre}
+ $${(item.price * item.quantity).toFixed(2)} +
+
+ $${parseFloat(item.price).toFixed(2)} c/u +
+ + +
+
+
+ `).join(''); + } + calculateTotals(); + updateSubmitButtonState(); + }; + + const renderClientSearchResults = (clients) => { + if (!clients || clients.length === 0) { + elements.clientSearchResults.innerHTML = '

No se encontraron clientes.

'; + return; + } + // CAMBIO: Usar los nombres de campo correctos devueltos por la nueva API + elements.clientSearchResults.innerHTML = clients.map(client => ` + + ${client.client_nombre}
+ ${client.client_rucCed} +
+ `).join(''); + }; + + // --- LOGIC --- + const calculateTotals = () => { + const subtotal = state.orderItems.reduce((acc, item) => acc + parseFloat(item.price) * item.quantity, 0); + const iva = subtotal * 0.12; // Asumiendo 12% IVA + const total = subtotal + iva; + + elements.subtotalValue.textContent = `$${subtotal.toFixed(2)}`; + elements.ivaValue.textContent = `$${iva.toFixed(2)}`; + elements.totalValue.textContent = `$${total.toFixed(2)}`; + }; + + const updateSubmitButtonState = () => { + elements.submitOrderBtn.disabled = state.orderItems.length === 0 || !state.selectedClient; + }; + + const clearOrder = () => { + state.orderItems = []; + state.selectedClient = null; + elements.clientNameInput.value = 'No seleccionado'; + renderOrder(); + }; + + const addProductToOrder = (productId) => { + const product = state.productCatalog.find(p => p.idt_prdcto == productId); + if (!product) return; + + const existingItem = state.orderItems.find(item => item.id == productId); + + if (existingItem) { + existingItem.quantity++; + } else { + state.orderItems.push({ + id: product.idt_prdcto, + nombre: product.detalle_prdcto, + code: product.codigo_prdcto, + price: product.precio_vta, + quantity: 1, + }); + } + + renderOrder(); + showToast('Producto añadido', `${product.detalle_prdcto} fue añadido al pedido.`); + }; + + // --- EVENT HANDLERS --- + const handleSearch = debounce((event) => { + fetchProducts(event.target.value); + }, 300); + + const handleClientSearch = debounce((event) => { + fetchClients(event.target.value); + }, 300); + + // --- EVENT LISTENERS --- + elements.searchInput.addEventListener('keyup', handleSearch); + elements.clientSearchInput.addEventListener('keyup', handleClientSearch); + elements.clearOrderBtn.addEventListener('click', clearOrder); + elements.submitOrderBtn.addEventListener('click', submitOrder); + + // Event delegation para añadir productos al carrito + elements.productGrid.addEventListener('click', (e) => { + const addButton = e.target.closest('.add-to-cart-btn'); + if (addButton) { + const card = addButton.closest('.product-card'); + const productId = card.dataset.productId; + addProductToOrder(productId); + } + }); + + // Event delegation para actualizar cantidades y eliminar items del pedido + elements.orderItemsList.addEventListener('change', (e) => { + if (e.target.classList.contains('quantity-input')) { + const index = e.target.dataset.index; + state.orderItems[index].quantity = parseInt(e.target.value, 10); + renderOrder(); + } + }); + + elements.orderItemsList.addEventListener('click', (e) => { + if (e.target.closest('.remove-item-btn')) { + const index = e.target.closest('.remove-item-btn').dataset.index; + state.orderItems.splice(index, 1); + renderOrder(); + } + }); + + elements.clientSearchResults.addEventListener('click', (e) => { + e.preventDefault(); + const link = e.target.closest('.select-client-btn'); + if (link) { + state.selectedClient = { + id: link.dataset.clientId, + name: link.dataset.clientName, + }; + elements.clientNameInput.value = state.selectedClient.name; + bootstrap.Modal.getInstance(document.getElementById('clientSearchModal')).hide(); + updateSubmitButtonState(); + } + }); + + // --- INITIALIZATION --- + fetchProducts(); +}); \ No newline at end of file diff --git a/src/rutas/rt_Generales.js b/src/rutas/rt_Generales.js index 4005ade..e67b987 100644 --- a/src/rutas/rt_Generales.js +++ b/src/rutas/rt_Generales.js @@ -10,7 +10,7 @@ rutas.get('/consultaPedidos', controlador_init.app_PEDIDOS);//consulta PEDIDOS rutas.get('/gp_precios', controlador_init.app_GpPrecios);//consulta grupo precios rutas.get('/origen_pedidos', controlador_init.app_ORIGENES);//consulta grupo precios rutas.get('/panel_control', controlador_init.panel_control);//consulta grupo precios -rutas.get('/pedidos', controlador_init.app_sigma);//Una app para PEDIDOS +rutas.get('/pedidos', (req, res) => res.render('app_pedidos_v2'));//Una app para PEDIDOS v2 rutas.get('/recepcionPedidos', controlador_init.recibe_pedidos);//receptar pedidos rutas.post('/recepcionPedidos_post', controlador_init.recibe_pedidos_post);//receptar pedidos diff --git a/src/rutas/rt_api_v2.js b/src/rutas/rt_api_v2.js new file mode 100644 index 0000000..99c04f6 --- /dev/null +++ b/src/rutas/rt_api_v2.js @@ -0,0 +1,90 @@ +const express = require('express'); +const rutas = express.Router(); + +const controladorPedidosApi = require('../controladores/controlador_pedidos_api'); +const controladorItemsApi = require('../controladores/controlador_items_api'); +const controladorClientesApi = require('../controladores/controlador_clientes_api'); // Importar el nuevo controlador de clientes + +// --- Rutas para la nueva API de Pedidos V2 --- + +/** + * @swagger + * /api/v2/items: + * get: + * summary: Obtiene la lista de productos para la app de pedidos v2. + * tags: [Pedidos V2] + * parameters: + * - in: query + * name: consulta + * schema: + * type: string + * description: Término de búsqueda para filtrar productos. + * - in: query + * name: gp_precio + * schema: + * type: string + * description: Grupo de precios a aplicar (ej. PUBLICO). + * responses: + * 200: + * description: Una lista de productos. + * 500: + * description: Error del servidor. + */ +rutas.get('/api/v2/items', controladorItemsApi.getItemsForV2); + +/** + * @swagger + * /api/v2/clientes: + * get: + * summary: Obtiene la lista de clientes para la app de pedidos v2. + * tags: [Pedidos V2] + * parameters: + * - in: query + * name: consulta + * schema: + * type: string + * description: Término de búsqueda para filtrar clientes por nombre o RUC/CI. + * responses: + * 200: + * description: Una lista de clientes. + * 500: + * description: Error del servidor. + */ +rutas.get('/api/v2/clientes', controladorClientesApi.getClientesForV2); + +/** + * @swagger + * /api/v2/pedidos: + * post: + * summary: Crea un nuevo pedido de forma transaccional. + * tags: [Pedidos V2] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * clienteId: { type: integer, example: 1 } + * user: { type: string, example: 'WebAppUser' } + * estado: { type: string, example: 'ACTIVO' } + * valor: { type: number, example: 112.00 } + * iva: { type: number, example: 12.00 } + * plataforma: { type: string, example: 'APP-SIGMA-WEB-V2' } + * origen: { type: string, example: 'WebApp' } + * items: + * type: array + * items: + * type: object + * properties: + * cod: { type: string, example: 'PROD-001' } + * cant: { type: integer, example: 2 } + * precio: { type: number, example: 50.00 } + * responses: + * 201: { description: Pedido creado exitosamente. } + * 400: { description: Datos inválidos. } + * 500: { description: Error del servidor. } + */ +rutas.post('/api/v2/pedidos', controladorPedidosApi.crearPedido); + +module.exports = rutas; \ No newline at end of file diff --git a/src/rutas/rt_items.js b/src/rutas/rt_items.js index c618d17..0fdb3cf 100644 --- a/src/rutas/rt_items.js +++ b/src/rutas/rt_items.js @@ -25,6 +25,7 @@ rutas.post('/json', controladorItems.json);//ver menu en modo json //APP_SIGMA consultas categorias rutas.get('/categorias_json', controladorItems.cat_json);//ver categorias items en modo json rutas.get('/item_xCat/:cat&:gpp', controladorItems.item_xCat);//ver productos x categoria en modo json/post +rutas.get('/pedido_detalle/:id_pedido', controladorItems.pedido_detalle);//ver detalle de un pedido en modo json/post //APP_SIGMA consultas rutas.get('/consultaItemsPrecios/', controladorItems.app_consultaItemsPrecios);//ver productos en modo json/post diff --git a/src/scripts/helpers.js b/src/scripts/helpers.js new file mode 100644 index 0000000..2a8bcfa --- /dev/null +++ b/src/scripts/helpers.js @@ -0,0 +1,16 @@ +const crypto = require('crypto'); + +/** + * Convierte una cadena de texto a su hash MD5. + * @param {string} data_string La cadena a convertir. + * @returns {string} El hash MD5. + */ +function stringTo_md5(data_string) { + const md5 = crypto.createHash('md5').update(data_string, 'utf-8').digest('hex'); + // console.log("MD5: ", md5); // Descomentar para depuración + return md5; +} + +module.exports = { + stringTo_md5, +}; \ No newline at end of file diff --git a/src/swager_config.js b/src/swager_config.js index fbf7525..551b2f9 100644 --- a/src/swager_config.js +++ b/src/swager_config.js @@ -43,6 +43,7 @@ const swaggerOptions = { './rutas/rt_apps.js', './rutas/rt_arduino.js', './rutas/rt_proyectos.js', + './rutas/rt_api_v2.js', // AÑADIR NUEVA RUTA ] }; diff --git a/src/views/app_pedidos_v2.ejs b/src/views/app_pedidos_v2.ejs new file mode 100644 index 0000000..c0f91e1 --- /dev/null +++ b/src/views/app_pedidos_v2.ejs @@ -0,0 +1,170 @@ + + + + + + + + SIGMA APP v2 + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
Catálogo de Productos
+ +
+ + +
+ + +
+ +
+ + + +
+
+
+ + +
+
+
+
Resumen del Pedido
+ + +
+ +
+ + +
+
+ + +
+ +

El carrito está vacío.

+
+ + + + + +
+ + +
+
+
+
+
+
+ + + + + +
+ +
+ + + + + + + \ No newline at end of file