mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +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:
232
app.js
232
app.js
@@ -2360,8 +2360,8 @@ 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');
|
||||||
@@ -3248,12 +3466,18 @@ function addAnticipo() {
|
|||||||
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)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
65
index.html
65
index.html
@@ -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>
|
|
||||||
<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>
|
<span class="category-icon">💰</span>
|
||||||
<h4>Anticipos</h4>
|
<h4>Agregar Anticipo</h4>
|
||||||
<button class="category-toggle">▶</button>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
|||||||
159
styles.css
159
styles.css
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user