feat: Replace product cards with search bar interface

- Replace category cards with modern search bar for services/products
- Implement real-time search with debouncing (200ms delay)
- Add autocomplete functionality for better UX
- Maintain anticipo functionality with dedicated form
- Add visual feedback notifications instead of alerts
- Improve responsive design and accessibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Marco Gallegos
2025-09-16 12:07:31 -06:00
parent 5cb8dcdd9e
commit 9282bd5480
3 changed files with 416 additions and 52 deletions

238
app.js
View File

@@ -2360,9 +2360,9 @@ function initializeModernSalesInterface() {
header.addEventListener('click', toggleCategory); header.addEventListener('click', toggleCategory);
}); });
// Load products by categories // Initialize search functionality
loadProductsByCategories(); initializeProductSearch();
// Update cart display // Update cart display
updateCartDisplay(); updateCartDisplay();
} }
@@ -2396,6 +2396,224 @@ function collapseAllCategories() {
}); });
} }
// Initialize product search functionality
async function initializeProductSearch() {
try {
// Load all products
const response = await fetch('/api/products');
if (!response.ok) throw new Error('Failed to load products');
const allProducts = await response.json();
products = allProducts;
console.log('Products loaded for search:', products.length);
// Setup search event listeners
setupSearchEventListeners();
} catch (error) {
console.error('Error loading products for search:', error);
}
}
function setupSearchEventListeners() {
const searchInput = document.getElementById('service-search-input');
const searchResults = document.getElementById('search-results');
const anticipoSection = document.getElementById('anticipo-section');
if (!searchInput || !searchResults || !anticipoSection) return;
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim().toLowerCase();
if (query.length === 0) {
searchResults.style.display = 'none';
anticipoSection.style.display = 'none';
return;
}
// Always hide anticipo section when typing, it will show via search results or click
anticipoSection.style.display = 'none';
// Debounce search
searchTimeout = setTimeout(() => {
performProductSearch(query);
}, 200);
});
// Hide results when clicking outside
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
// Focus search input for easy access
searchInput.addEventListener('focus', function() {
if (this.value.trim() && !this.value.toLowerCase().includes('anticipo')) {
performProductSearch(this.value.trim().toLowerCase());
}
});
}
function performProductSearch(query) {
const searchResults = document.getElementById('search-results');
if (!searchResults) return;
// Filter products based on query
let filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(query) ||
(product.category && product.category.toLowerCase().includes(query))
);
// Add virtual "anticipo" product if searching for it
if (query.includes('anticipo') || query.includes('advance')) {
const anticipoProduct = {
id: 'virtual_anticipo',
name: 'Anticipo',
category: 'Anticipos',
price: 0,
custom_price: true,
virtual: true
};
filteredProducts.unshift(anticipoProduct); // Add at the beginning
}
// Render search results
renderSearchResults(filteredProducts);
searchResults.style.display = 'block';
}
function renderSearchResults(filteredProducts) {
const searchResults = document.getElementById('search-results');
if (!searchResults) return;
searchResults.innerHTML = '';
if (filteredProducts.length === 0) {
searchResults.innerHTML = '<div class="search-empty">No se encontraron servicios o productos</div>';
return;
}
filteredProducts.forEach(product => {
const resultItem = createSearchResultItem(product);
searchResults.appendChild(resultItem);
});
}
function createSearchResultItem(product) {
const item = document.createElement('div');
item.className = 'search-result-item';
item.dataset.productId = product.id;
const priceDisplay = product.custom_price
? '<div class="search-result-custom-price">Precio personalizado</div>'
: `<div class="search-result-price">$${parseFloat(product.price || 0).toFixed(2)}</div>`;
item.innerHTML = `
<div class="search-result-info">
<div class="search-result-name">${escapeHTML(product.name)}</div>
<div class="search-result-category">${escapeHTML(product.category || 'Sin categoría')}</div>
</div>
<div class="search-result-actions">
${priceDisplay}
</div>
`;
// Add click event to add product to cart
item.addEventListener('click', function() {
addProductToCartFromSearch(product.id);
// Clear search and hide results
const searchInput = document.getElementById('service-search-input');
const searchResults = document.getElementById('search-results');
if (searchInput) searchInput.value = '';
if (searchResults) searchResults.style.display = 'none';
});
return item;
}
function addProductToCartFromSearch(productId) {
// Handle virtual anticipo product
if (productId === 'virtual_anticipo') {
// Show anticipo section instead of adding directly
const anticipoSection = document.getElementById('anticipo-section');
const searchResults = document.getElementById('search-results');
if (anticipoSection) anticipoSection.style.display = 'block';
if (searchResults) searchResults.style.display = 'none';
// Focus on the amount input
const amountInput = document.getElementById('anticipo-amount');
if (amountInput) {
setTimeout(() => amountInput.focus(), 100);
}
return;
}
// Find the product in the products array
const product = products.find(p => p.id === productId);
if (!product) return;
// Handle custom price products
let price = product.price;
if (product.custom_price) {
const customPrice = prompt(`Ingresa el precio para "${product.name}":`, '0');
if (customPrice === null) return; // User cancelled
price = parseFloat(customPrice) || 0;
}
// Check if product is already in cart
const existingIndex = selectedProducts.findIndex(p => p.id === productId);
if (existingIndex >= 0) {
// Update quantity
selectedProducts[existingIndex].quantity += 1;
selectedProducts[existingIndex].price = price; // Update price in case it changed
} else {
// Add new product
selectedProducts.push({
id: product.id,
name: product.name,
price: price,
quantity: 1,
type: product.type,
custom_price: product.custom_price
});
}
updateCartDisplay();
calculateTotals();
// Show visual feedback
showAddToCartFeedback(product.name);
}
function showAddToCartFeedback(productName) {
// Create temporary notification
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 10px 15px;
border-radius: 5px;
z-index: 10000;
font-weight: bold;
animation: slideIn 0.3s ease;
`;
notification.textContent = `${productName} agregado al carrito`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 2000);
}
async function loadProductsByCategories() { async function loadProductsByCategories() {
try { try {
const response = await fetch('/api/products'); const response = await fetch('/api/products');
@@ -3247,13 +3465,19 @@ function addAnticipo() {
// Clear inputs // Clear inputs
amountInput.value = ''; amountInput.value = '';
commentInput.value = ''; commentInput.value = '';
// Hide anticipo section and clear search
const anticipoSection = document.getElementById('anticipo-section');
const searchInput = document.getElementById('service-search-input');
if (anticipoSection) anticipoSection.style.display = 'none';
if (searchInput) searchInput.value = '';
// Update cart display // Update cart display
updateCartDisplay(); updateCartDisplay();
calculateTotals(); calculateTotals();
// Show confirmation // Show visual feedback instead of alert
alert(`✅ ANTICIPO AGREGADO\n\n${anticipoName}: $${amount.toFixed(2)}`); showAddToCartFeedback(`${anticipoName}: $${amount.toFixed(2)}`);
} }

View File

@@ -115,63 +115,44 @@
</div> </div>
</div> </div>
<!-- Categorías de Productos --> <!-- Barra de Búsqueda de Servicios -->
<div class="categories-container"> <div class="services-search-container">
<h3>Selecciona tus servicios</h3> <h3>Buscar servicios y productos</h3>
<!-- Vanity Lashes --> <!-- Barra de búsqueda principal -->
<div class="category-section" data-category="Vanity Lashes"> <div class="search-bar-container">
<div class="category-header"> <div class="search-input-wrapper">
<span class="category-icon">👁️</span> <input type="text" id="service-search-input" placeholder="Buscar servicios, productos o escribir 'anticipo'..." autocomplete="off" />
<h4>Vanity Lashes</h4> <span class="search-icon">🔍</span>
<button class="category-toggle"></button>
</div> </div>
<div class="products-grid" id="pestanas-products">
<!-- Los productos se cargarán dinámicamente aquí --> <!-- Resultados de búsqueda -->
<div id="search-results" class="search-results" style="display: none;">
<!-- Los resultados aparecerán aquí -->
</div> </div>
</div> </div>
<!-- PMU Services --> <!-- Sección de Anticipos -->
<div class="category-section" data-category="PMU Services"> <div id="anticipo-section" class="anticipo-section" style="display: none;">
<div class="category-header"> <div class="anticipo-form-card">
<span class="category-icon">✏️</span> <div class="anticipo-header">
<h4>PMU Services</h4> <span class="category-icon">💰</span>
<button class="category-toggle"></button> <h4>Agregar Anticipo</h4>
</div> </div>
<div class="products-grid" id="microblading-products">
<!-- Los productos se cargarán dinámicamente aquí -->
</div>
</div>
<!-- Uñas -->
<div class="category-section" data-category="Uñas">
<div class="category-header">
<span class="category-icon">💅</span>
<h4>Nail Art</h4>
<button class="category-toggle"></button>
</div>
<div class="products-grid" id="unas-products">
<!-- Los productos se cargarán dinámicamente aquí -->
</div>
</div>
<!-- Anticipos -->
<div class="category-section" data-category="Anticipos">
<div class="category-header">
<span class="category-icon">💰</span>
<h4>Anticipos</h4>
<button class="category-toggle"></button>
</div>
<div class="anticipos-grid" id="anticipos-products">
<div class="anticipo-form"> <div class="anticipo-form">
<div class="anticipo-input-group"> <div class="anticipo-input-group">
<input type="number" id="anticipo-amount" min="1" placeholder="Cantidad" /> <input type="number" id="anticipo-amount" min="1" placeholder="Cantidad" />
<input type="text" id="anticipo-comment" placeholder="Comentario (opcional)" /> <input type="text" id="anticipo-comment" placeholder="Comentario (opcional)" />
<button class="btn-add-anticipo" onclick="addAnticipo()">Agregar</button> <button class="btn-add-anticipo" onclick="addAnticipo()">Agregar Anticipo</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Mensaje de ayuda -->
<div class="search-help">
<p>💡 Escribe el nombre del servicio o producto, o escribe "anticipo" para agregar un anticipo</p>
</div>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,165 @@
- Secundario: #6c757d (gris medio) - Secundario: #6c757d (gris medio)
*/ */
/* --- Estilos para barra de búsqueda de servicios --- */
.services-search-container {
margin-bottom: 20px;
}
.search-bar-container {
position: relative;
margin-bottom: 15px;
}
.search-input-wrapper {
position: relative;
}
#service-search-input {
width: 100%;
box-sizing: border-box;
padding: 12px 40px 12px 15px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 16px;
background-color: #fff;
transition: border-color 0.2s, box-shadow 0.2s;
}
#service-search-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.search-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
font-size: 18px;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
max-height: 300px;
overflow-y: auto;
z-index: 1000;
}
.search-result-item {
padding: 12px 15px;
border-bottom: 1px solid #f1f3f4;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-info {
display: flex;
flex-direction: column;
}
.search-result-name {
font-weight: 600;
color: #212529;
margin-bottom: 2px;
}
.search-result-category {
font-size: 12px;
color: #6c757d;
}
.search-result-price {
font-weight: 600;
color: #28a745;
}
.search-result-custom-price {
font-size: 12px;
color: #ffc107;
font-weight: 500;
}
.anticipo-section {
margin-top: 15px;
}
.anticipo-form-card {
background: #fff;
border: 2px solid #28a745;
border-radius: 8px;
padding: 15px;
}
.anticipo-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.anticipo-header .category-icon {
margin-right: 8px;
font-size: 20px;
}
.anticipo-header h4 {
margin: 0;
color: #28a745;
}
.search-help {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 6px;
padding: 10px 15px;
margin-top: 15px;
}
.search-help p {
margin: 0;
font-size: 14px;
color: #0066cc;
}
.search-empty {
padding: 20px;
text-align: center;
color: #6c757d;
font-style: italic;
}
/* Animación para notificaciones */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Estilos generales y tipografías */ /* Estilos generales y tipografías */
body { body {
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;