Consulta Detalle de pedido
This commit is contained in:
13
.env
Normal file
13
.env
Normal 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
13
.env.example
Normal 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
12
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
39
src/controladores/controlador_clientes_api.js
Normal file
39
src/controladores/controlador_clientes_api.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
50
src/controladores/controlador_items_api.js
Normal file
50
src/controladores/controlador_items_api.js
Normal 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;
|
||||
85
src/controladores/controlador_pedidos_api.js
Normal file
85
src/controladores/controlador_pedidos_api.js
Normal 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;
|
||||
50
src/public/css/app_pedidos_v2.css
Normal file
50
src/public/css/app_pedidos_v2.css
Normal 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 */
|
||||
}
|
||||
314
src/public/js/app_pedidos_v2.js
Normal file
314
src/public/js/app_pedidos_v2.js
Normal 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();
|
||||
});
|
||||
@@ -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
90
src/rutas/rt_api_v2.js
Normal 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;
|
||||
@@ -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
16
src/scripts/helpers.js
Normal 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,
|
||||
};
|
||||
@@ -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
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
170
src/views/app_pedidos_v2.ejs
Normal file
170
src/views/app_pedidos_v2.ejs
Normal 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>
|
||||
Reference in New Issue
Block a user