Consulta Detalle de pedido
This commit is contained in:
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();
|
||||
});
|
||||
Reference in New Issue
Block a user