mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 05:15:14 +00:00
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:
238
app.js
238
app.js
@@ -2360,9 +2360,9 @@ function initializeModernSalesInterface() {
|
||||
header.addEventListener('click', toggleCategory);
|
||||
});
|
||||
|
||||
// Load products by categories
|
||||
loadProductsByCategories();
|
||||
|
||||
// Initialize search functionality
|
||||
initializeProductSearch();
|
||||
|
||||
// Update cart display
|
||||
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() {
|
||||
try {
|
||||
const response = await fetch('/api/products');
|
||||
@@ -3247,13 +3465,19 @@ function addAnticipo() {
|
||||
// Clear inputs
|
||||
amountInput.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
|
||||
updateCartDisplay();
|
||||
calculateTotals();
|
||||
|
||||
// Show confirmation
|
||||
alert(`✅ ANTICIPO AGREGADO\n\n${anticipoName}: $${amount.toFixed(2)}`);
|
||||
|
||||
// Show visual feedback instead of alert
|
||||
showAddToCartFeedback(`${anticipoName}: $${amount.toFixed(2)}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
71
index.html
71
index.html
@@ -115,63 +115,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categorías de Productos -->
|
||||
<div class="categories-container">
|
||||
<h3>Selecciona tus servicios</h3>
|
||||
|
||||
<!-- Vanity Lashes -->
|
||||
<div class="category-section" data-category="Vanity Lashes">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">👁️</span>
|
||||
<h4>Vanity Lashes</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
<!-- Barra de Búsqueda de Servicios -->
|
||||
<div class="services-search-container">
|
||||
<h3>Buscar servicios y productos</h3>
|
||||
|
||||
<!-- Barra de búsqueda principal -->
|
||||
<div class="search-bar-container">
|
||||
<div class="search-input-wrapper">
|
||||
<input type="text" id="service-search-input" placeholder="Buscar servicios, productos o escribir 'anticipo'..." autocomplete="off" />
|
||||
<span class="search-icon">🔍</span>
|
||||
</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>
|
||||
|
||||
<!-- PMU Services -->
|
||||
<div class="category-section" data-category="PMU Services">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">✏️</span>
|
||||
<h4>PMU Services</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
</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">
|
||||
<!-- Sección de Anticipos -->
|
||||
<div id="anticipo-section" class="anticipo-section" style="display: none;">
|
||||
<div class="anticipo-form-card">
|
||||
<div class="anticipo-header">
|
||||
<span class="category-icon">💰</span>
|
||||
<h4>Agregar Anticipo</h4>
|
||||
</div>
|
||||
<div class="anticipo-form">
|
||||
<div class="anticipo-input-group">
|
||||
<input type="number" id="anticipo-amount" min="1" placeholder="Cantidad" />
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
159
styles.css
159
styles.css
@@ -8,6 +8,165 @@
|
||||
- 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 */
|
||||
body {
|
||||
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
Reference in New Issue
Block a user