Consulta Detalle de pedido

This commit is contained in:
Pablinux
2025-08-05 02:08:31 -05:00
parent ef90a045e6
commit 7ad00e45bb
20 changed files with 913 additions and 56 deletions

13
.env Normal file
View File

@@ -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&'

13
.env.example Normal file
View File

@@ -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=

12
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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){

View File

@@ -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
}
}

View File

@@ -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) => {

View File

@@ -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");
@@ -346,5 +346,17 @@ controlador.app_itemsTab = (req, res) => {
});
});
}
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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 */
}

View File

@@ -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 = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
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 = '<i class="bi bi-cloud-upload"></i> Enviar Pedido';
updateSubmitButtonState();
}
};
// --- RENDERING ---
const productCardTemplate = (product) => `
<div class="col-xl-3 col-lg-4 col-md-6 product-card-wrapper">
<div class="card h-100 product-card" data-product-id="${product.idt_prdcto}">
<img src="${product.url_img || '/img/placeholder.png'}" class="card-img-top" alt="${product.detalle_prdcto}">
<div class="card-body d-flex flex-column">
<h5 class="card-title">${product.detalle_prdcto}</h5>
<p class="card-text text-muted">${product.codigo_prdcto}</p>
<div class="mt-auto d-flex justify-content-between align-items-center">
<span class="fs-5 fw-bold text-primary">$${parseFloat(product.precio_vta).toFixed(2)}</span>
<button class="btn btn-sm btn-outline-primary add-to-cart-btn">
<i class="bi bi-cart-plus"></i> Añadir
</button>
</div>
</div>
</div>
</div>
`;
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) => `
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${item.nombre}</h6>
<small>$${(item.price * item.quantity).toFixed(2)}</small>
</div>
<div class="d-flex align-items-center justify-content-between">
<small class="text-muted">$${parseFloat(item.price).toFixed(2)} c/u</small>
<div class="d-flex align-items-center">
<input type="number" value="${item.quantity}" min="1" class="form-control form-control-sm quantity-input me-2" data-index="${index}" style="width: 70px;">
<button class="btn btn-sm btn-outline-danger remove-item-btn" data-index="${index}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
}
calculateTotals();
updateSubmitButtonState();
};
const renderClientSearchResults = (clients) => {
if (!clients || clients.length === 0) {
elements.clientSearchResults.innerHTML = '<p class="text-center text-muted">No se encontraron clientes.</p>';
return;
}
// CAMBIO: Usar los nombres de campo correctos devueltos por la nueva API
elements.clientSearchResults.innerHTML = clients.map(client => `
<a href="#" class="list-group-item list-group-item-action select-client-btn" data-client-id="${client.client_id}" data-client-name="${client.client_nombre}">
<strong>${client.client_nombre}</strong><br>
<small class="text-muted">${client.client_rucCed}</small>
</a>
`).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();
});

View File

@@ -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

90
src/rutas/rt_api_v2.js Normal file
View File

@@ -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;

View File

@@ -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

16
src/scripts/helpers.js Normal file
View File

@@ -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,
};

View File

@@ -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
]
};

View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="google" content="notranslate" />
<title>SIGMA APP v2</title>
<!-- Favicons -->
<link rel="icon" href="/img/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/img/favicon-64x64.png" sizes="64x64" type="image/png">
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Custom Styles -->
<link href="/css/app_pedidos_v2.css" rel="stylesheet" type="text/css" />
</head>
<body>
<!-- Header -->
<header class="navbar navbar-dark bg-primary sticky-top shadow">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="./img/favicon_sigma/LOGO_sigma64.png" alt="Logo" width="30" height="30"
class="d-inline-block align-text-top">
SIGMA Pedidos
</a>
<div class="d-flex text-white">
<i class="bi bi-person-circle fs-4 me-2"></i>
<span class="navbar-text">Pablinux</span>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container-fluid mt-4">
<div class="row">
<!-- Columna de Productos -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">Catálogo de Productos</h5>
<!-- Barra de Búsqueda -->
<div class="input-group mb-3">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" id="search-input" class="form-control" placeholder="Buscar producto por nombre o código...">
</div>
<!-- Grid de Productos -->
<div id="product-grid" class="row g-3">
<!-- Las tarjetas de productos se insertarán aquí -->
</div>
<!-- Indicador de Carga y Mensajes -->
<div id="grid-feedback" class="text-center p-5" style="display: none;">
<!-- Spinner -->
<div class="spinner-border text-primary" role="status" id="loading-spinner">
<span class="visually-hidden">Cargando...</span>
</div>
<!-- Mensaje de no resultados -->
<p id="no-results-message" class="text-muted fs-5" style="display: none;">
<i class="bi bi-emoji-frown fs-2"></i><br>
No se encontraron productos.
</p>
</div>
</div>
</div>
</div>
<!-- Columna del Pedido (Sidebar) -->
<div class="col-lg-4">
<div id="order-sidebar" class="card shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">Resumen del Pedido</h5>
<!-- Selección de Cliente -->
<div class="mb-3">
<label class="form-label">Cliente</label>
<div class="input-group">
<input type="text" id="client-name" class="form-control" placeholder="No seleccionado" readonly>
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#clientSearchModal">
<i class="bi bi-person-plus"></i>
</button>
</div>
</div>
<!-- Lista de Items del Pedido -->
<div id="order-items-list" class="list-group mb-3">
<!-- Los items del pedido se insertarán aquí -->
<p id="empty-cart-message" class="text-center text-muted p-3">El carrito está vacío.</p>
</div>
<!-- Totales -->
<div id="order-totals" style="display: none;">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">
Subtotal
<span id="subtotal-value">$0.00</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
IVA (12%)
<span id="iva-value">$0.00</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center fw-bold fs-5">
Total
<span id="total-value">$0.00</span>
</li>
</ul>
</div>
<!-- Acciones -->
<div class="d-grid gap-2 mt-3">
<button id="submit-order-btn" class="btn btn-success" disabled>
<i class="bi bi-cloud-upload"></i> Enviar Pedido
</button>
<button id="clear-order-btn" class="btn btn-outline-danger">
<i class="bi bi-trash"></i> Limpiar Pedido
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Modal de Búsqueda de Clientes -->
<div class="modal fade" id="clientSearchModal" tabindex="-1" aria-labelledby="clientSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="clientSearchModalLabel">Buscar Cliente</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" id="client-search-input" class="form-control" placeholder="Buscar por nombre, RUC o CI...">
<div id="client-search-results" class="list-group mt-3">
<!-- Resultados de la búsqueda de clientes -->
</div>
</div>
</div>
</div>
</div>
<!-- Toast Container para notificaciones -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="notification-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong id="toast-title" class="me-auto">Notificación</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div id="toast-body" class="toast-body">
<!-- Mensaje de la notificación -->
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script src="/js/app_pedidos_v2.js"></script>
</body>
</html>