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.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cargando...
+
+
+
+
+ No se encontraron productos.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file