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

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();
});