Browse Source

multi category support

latapp 9 months ago
parent
commit
939164bc4a
3 changed files with 275 additions and 167 deletions
  1. 3 49
      public/main/index.html
  2. 269 115
      public/main/js/app.js
  3. 3 3
      services/data_service.py

+ 3 - 49
public/main/index.html

@@ -45,11 +45,6 @@
     <!-- ===== MENÚ tab ===== -->
     <section id="menuTab" data-index="0" class="min-h-0 overflow-y-auto h-full" data-tab>
         <div class="pt-4 pb-3">
-            <h2 class="text-[19px] mx-4 font-bold text-[#101419]">
-                Pide tu shop express 🍺
-            </h2>
-            <p class="product-type mx-4 text-[#58728d] text-sm pb-4 mb-4 border-b border-gray-200">*solo lo más vendido</p>
-            
             <!-- Barra de progreso para cerveza gratis -->
             <div class="mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200">
                 <div class="flex items-center justify-between mb-2">
@@ -83,51 +78,10 @@
         </div>
         
         <div class="px-4 overflow-y-auto">
+            <form>
+              <input type="text" id="searchInput" placeholder="Buscar..." class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#101419]">
+            </form>
             <ul id="productList" class="space-y-6">
-                <!-- Productos de ejemplo para demostrar la funcionalidad -->
-                <li class="flex items-stretch justify-between gap-4 rounded-xl">
-                    <div class="flex flex-[2_2_0px] flex-col gap-4">
-                        <div class="flex flex-col gap-1">
-                            <p class="product-type text-[#58728d] text-sm">Cerveza</p>
-                            <p class="product-name text-[#101419] text-base font-bold leading-tight">Corona Extra</p>
-                            <p class="product-description text-[#58728d] text-sm">Cerveza mexicana refrescante</p>
-                        </div>
-                        <div class="flex items-center gap-3">
-                            <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3 rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium transition-colors">
-                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
-                                    <path d="M12 5v14m7-7H5" stroke-linecap="round" stroke-linejoin="round"/>
-                                </svg>
-                                Añadir
-                            </button>
-                            <span class="product-price text-sm font-semibold text-[#101419]">$2.500</span>
-                        </div>
-                    </div>
-                    <div class="product-image flex-1 aspect-video bg-gradient-to-br from-yellow-200 to-amber-400 rounded-xl flex items-center justify-center">
-                        <span class="text-2xl">🍺</span>
-                    </div>
-                </li>
-                
-                <li class="flex items-stretch justify-between gap-4 rounded-xl">
-                    <div class="flex flex-[2_2_0px] flex-col gap-4">
-                        <div class="flex flex-col gap-1">
-                            <p class="product-type text-[#58728d] text-sm">Snacks</p>
-                            <p class="product-name text-[#101419] text-base font-bold leading-tight">Papas Fritas</p>
-                            <p class="product-description text-[#58728d] text-sm">Crujientes y saladas</p>
-                        </div>
-                        <div class="flex items-center gap-3">
-                            <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3 rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium transition-colors">
-                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
-                                    <path d="M12 5v14m7-7H5" stroke-linecap="round" stroke-linejoin="round"/>
-                                </svg>
-                                Añadir
-                            </button>
-                            <span class="product-price text-sm font-semibold text-[#101419]">$1.200</span>
-                        </div>
-                    </div>
-                    <div class="product-image flex-1 aspect-video bg-gradient-to-br from-orange-200 to-red-300 rounded-xl flex items-center justify-center">
-                        <span class="text-2xl">🍟</span>
-                    </div>
-                </li>
             </ul>
         </div>
 

+ 269 - 115
public/main/js/app.js

@@ -12,7 +12,7 @@ let userName = "Cliente";
 let userTable = null;
 let userToken = null;
 // --- Datos de Productos y Carrito ---
-let products = [];
+let Allproducts = [];
 let cart = [];
 let itsEmpty = true;
 
@@ -41,12 +41,14 @@ const chatSuggestionsElement = document.getElementById("chatSuggestions");
 
 //#region --- Inicialización y Configuracion ---
 async function initializeApp() {
+    showGlobalLoader("Cargando productos...");
+    await initializeProducts();
+    await renderProducts(Allproducts);
+    setupSearchListener();
     updateCartDisplay();
+    setupShoppingCart(userId, userToken, userName);
     initializeChat();
     setupBasicListeners();
-    showGlobalLoader("Cargando productos...");
-    setupShoppingCart(userId, userToken,userName);
-    await renderProducts();
     showGUI();
     hideGlobalLoader();
 
@@ -58,7 +60,6 @@ async function initializeApp() {
         });
     });
 }
-
 function initializeLoginModal() {
     const sessionModal = document.getElementById('sessionModal');
     const loginForm = document.getElementById('loginForm');
@@ -77,8 +78,8 @@ function initializeLoginModal() {
             showError("Por favor, completa todos los campos.");
             return;
         }
-        try{
-            const {data} = await login(email, pin)
+        try {
+            const { data } = await login(email, pin)
             userToken = data.token;
             userName = data.name;
             userId = data.id;
@@ -91,11 +92,11 @@ function initializeLoginModal() {
             sessionModal.classList.add('hidden');
 
             initializeApp();
-        }catch (error) {
+        } catch (error) {
             console.error(error)
-    }
-})
-}
+        }
+    })
+}   
 function initializeChat() {
     if (!chatForm) return;
     chatForm.addEventListener("submit", (event) => {
@@ -112,12 +113,41 @@ function initializeChat() {
         });
     });
 }
-
 function setupBasicListeners() {
     if (!checkoutButton) return;
     checkoutButton.addEventListener("click", processOrder);
     initializeRewards();
 }
+function setupSearchListener() {
+    const searchInput = document.getElementById("searchInput");
+    if (!searchInput) return;
+    
+    let debounceTimer;
+    
+    searchInput.addEventListener("input", () => {
+        // Limpiar el timer anterior si existe
+        clearTimeout(debounceTimer);
+        
+        // Agregar una clase de "buscando" para el feedback visual
+        productListElement.style.opacity = "0.7";
+        productListElement.style.transform = "scale(0.98)";
+        productListElement.style.transition = "opacity 0.2s ease, transform 0.2s ease";
+        
+        // Debounce de 200ms para evitar muchas llamadas
+        debounceTimer = setTimeout(() => {
+            const searchTerm = searchInput.value.toLowerCase();
+            const finded = Allproducts.filter(product => {
+                return product.name.toLowerCase().includes(searchTerm) ||
+                    product.description.toLowerCase().includes(searchTerm) ||
+                    (product.type && product.type.toLowerCase().includes(searchTerm)) ||
+                    (product.category && product.category.toLowerCase().includes(searchTerm));
+            });
+            
+            // Renderizar con animación
+            renderProductsWithAnimation(finded);
+        }, 200);
+    });
+}
 //#endregion
 //#region ===== Utilidad =====
 
@@ -129,45 +159,172 @@ function formatPrice(price) {
 //#endregion
 //#region ===== Productos =====
 
+async function initializeProducts() {
+    Allproducts = await getProducts(userToken);
+}
+
+async function createCategories(products) {
+    let categories = new Set(products.map(product => product.type || "Sin categoría"));
+    categories = Array.from(categories).sort((a, b) => a.localeCompare(b));
+
+    if (!productListElement) return;
+
+    const categoryContainers = categories.map(category => {
+        const container = document.createElement("div");
+        container.classList.add("category-container");
+        container.classList.add("mb-8", "p-4");
+        const title = document.createElement("h2");
+        title.classList.add("category-title", "text-3xl", "font-bold", "border-b-2", "pb-3", "mb-4");
+        let titleText = ["a", "e", "i", "o", "u"].includes(category.charAt(category.length - 1).toLowerCase()) ? "s" : 
+            ["á", "é", "í", "ó", "ú"].includes(category.charAt(category.length - 1).toLowerCase()) ? "s" : 
+            category.charAt(category.length - 1).toLowerCase() === "s" ? "" : "es";
+        title.textContent = category + titleText;
+        container.appendChild(title);
+        const productList = document.createElement("div");
+        productList.classList.add("product-list", "space-y-6");
+        productList.id = `productList-${category}`;
+        container.appendChild(productList);
+        productListElement.appendChild(container);
+
+        return { category, container: productList };
+    });
+
+    return categoryContainers;
+}
 
-async function renderProducts() {
+async function renderProducts(products) {
     if (!productListElement) return;
 
     const template = document.getElementById("product-card-template");
     if (!template) return;
 
     productListElement.innerHTML = "";
-    console.log("Cargando productos...");
-    products = await getProducts(userToken);
 
-    products.forEach(product => {
-        const clone = template.content.cloneNode(true);
+    const categoryContainers = await createCategories(products);
 
-        clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
-        clone.querySelector(".product-name").textContent = product.name;
-        clone.querySelector(".product-description").textContent = product.description;
-        clone.querySelector(".product-price").textContent = formatPrice(product.price);
-        clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
+    if (products.length === 0) {
+        const noProductsMessage = document.createElement("p");
+        noProductsMessage.textContent = "No hay productos disponibles.";
+        productListElement.appendChild(noProductsMessage);
+        return;
+    }
 
-        const addBtn = clone.querySelector(".add-to-cart-btn");
-        addBtn.dataset.productId = product.id;   // el listener usa esta info
+    for (const { category, container } of categoryContainers) {
+        let productsInCategory = products.filter(product => product.type === category);
 
-        productListElement.appendChild(clone);
-    });
+        if (productsInCategory.length === 0) continue;
+
+        productsInCategory.forEach(product => {
+            const clone = template.content.cloneNode(true);
+
+            clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
+            clone.querySelector(".product-name").textContent = product.name;
+            clone.querySelector(".product-description").textContent = product.description;
+            clone.querySelector(".product-price").textContent = formatPrice(product.price);
+            clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
+
+            const addBtn = clone.querySelector(".add-to-cart-btn");
+            addBtn.dataset.productId = product.id;   // el listener usa esta info
+            // Agregar event listener directamente al botón clonado
+            addBtn.addEventListener('click', (event) => {
+                const productId = parseInt(event.target.dataset.productId);
+                addToCart(productId, event.target);
+            });
 
-    document.querySelectorAll('.add-to-cart-btn').forEach(button => {
-        button.addEventListener('click', (event) => {
-            const productId = parseInt(event.target.dataset.productId);
-            addToCart(productId, event.target);
+            container.appendChild(clone);
         });
-    });
+    }
+}
+
+async function renderProductsWithAnimation(products) {
+    if (!productListElement) return;
+
+    const template = document.getElementById("product-card-template");
+    if (!template) return;
+
+    // Fade out actual content
+    productListElement.style.opacity = "0";
+    productListElement.style.transform = "scale(0.95)";
+    
+    setTimeout(async () => {
+        productListElement.innerHTML = "";
+
+        const categoryContainers = await createCategories(products);
+
+        if (products.length === 0) {
+            const noProductsMessage = document.createElement("p");
+            noProductsMessage.textContent = "No hay productos disponibles.";
+            noProductsMessage.style.opacity = "0";
+            noProductsMessage.style.transform = "translateY(20px)";
+            noProductsMessage.style.transition = "opacity 0.3s ease, transform 0.3s ease";
+            productListElement.appendChild(noProductsMessage);
+            
+            // Restore container
+            productListElement.style.opacity = "1";
+            productListElement.style.transform = "scale(1)";
+            
+            // Animate message
+            setTimeout(() => {
+                noProductsMessage.style.opacity = "1";
+                noProductsMessage.style.transform = "translateY(0)";
+            }, 50);
+            return;
+        }
+
+        let animationDelay = 0;
+        for (const { category, container } of categoryContainers) {
+            let productsInCategory = products.filter(product => product.type === category);
+
+            if (productsInCategory.length === 0) continue;
+
+            productsInCategory.forEach((product, index) => {
+                const clone = template.content.cloneNode(true);
+
+                clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
+                clone.querySelector(".product-name").textContent = product.name;
+                clone.querySelector(".product-description").textContent = product.description;
+                clone.querySelector(".product-price").textContent = formatPrice(product.price);
+                clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
+
+                const addBtn = clone.querySelector(".add-to-cart-btn");
+                addBtn.dataset.productId = product.id;
+                addBtn.addEventListener('click', (event) => {
+                    const productId = parseInt(event.target.dataset.productId);
+                    addToCart(productId, event.target);
+                });
+
+                // Get the first child (the product card element)
+                const productCard = clone.children[0];
+                
+                // Set initial animation state
+                productCard.style.opacity = "0";
+                productCard.style.transform = "translateY(20px) scale(0.95)";
+                productCard.style.transition = "opacity 0.4s ease, transform 0.4s ease";
+
+                container.appendChild(clone);
+
+                // Animate in with staggered delay
+                setTimeout(() => {
+                    productCard.style.opacity = "1";
+                    productCard.style.transform = "translateY(0) scale(1)";
+                }, animationDelay);
+                
+                animationDelay += 50; // 50ms delay between each product
+            });
+        }
+
+        // Restore container with smooth transition
+        productListElement.style.opacity = "1";
+        productListElement.style.transform = "scale(1)";
+        
+    }, 150); // Wait for fade out to complete
 }
 //#endregion
 //#region ===== Carrito =====
 
 
-async function addToCart (productId, buttonElement = null) {
-    const product = products.find(p => p.id === productId);
+async function addToCart(productId, buttonElement = null) {
+    const product = Allproducts.find(p => p.id === productId);
     if (!product) return;
     const cartItem = cart.find(item => item.id === productId);
     if (cartItem) {
@@ -191,7 +348,7 @@ async function addToCart (productId, buttonElement = null) {
 
 };
 
-async function removeFromCart (productId, removeAll = false) {
+async function removeFromCart(productId, removeAll = false) {
     const itemIndex = cart.findIndex(item => item.id === productId);
     if (itemIndex > -1) {
         if (removeAll || cart[itemIndex].quantity === 1) {
@@ -306,11 +463,11 @@ async function processOrder() {
         const orderData = {
             customerId: userId,
             table: userTable,
-            items: cart.map(item => ({ id: item.id, price: item.price, quantity: item.quantity})),
+            items: cart.map(item => ({ id: item.id, price: item.price, quantity: item.quantity })),
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
         };
-        const data = await sendOrder(orderData,userToken);
+        const data = await sendOrder(orderData, userToken);
         if (data && data.new_progress) {
             updateProgress(data.new_progress);
         }
@@ -396,22 +553,22 @@ async function sendMessageToAI() {
 
 //#endregion
 //#region ===== Rewards =====
- // Referencias a elementos del DOM
-    const rewardBtn = document.getElementById('rewardBtn');
-    const rewardModal = document.getElementById('rewardModal');
-    const closeRewardModal = document.getElementById('closeRewardModal');
-    const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
-    const cancelRewardBtn = document.getElementById('cancelRewardBtn');
-    const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
-    const claimRewardBtn = document.getElementById('claimRewardBtn');
+// Referencias a elementos del DOM
+const rewardBtn = document.getElementById('rewardBtn');
+const rewardModal = document.getElementById('rewardModal');
+const closeRewardModal = document.getElementById('closeRewardModal');
+const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
+const cancelRewardBtn = document.getElementById('cancelRewardBtn');
+const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
+const claimRewardBtn = document.getElementById('claimRewardBtn');
 
 
-    function initializeRewards() {
-     // Abrir modal cuando se hace clic en el botón de recompensa
+function initializeRewards() {
+    // Abrir modal cuando se hace clic en el botón de recompensa
 
-        closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
+    closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
 
-    rewardBtn.addEventListener('click', function() {
+    rewardBtn.addEventListener('click', function () {
         if (!rewardBtn.disabled) {
             rewardModal.classList.remove('hidden');
             document.body.style.overflow = 'hidden'; // Evitar scroll del fondo
@@ -419,31 +576,31 @@ async function sendMessageToAI() {
     });
 
     // Cerrar modal - botón X
-    closeRewardModal.addEventListener('click', function() {
+    closeRewardModal.addEventListener('click', function () {
         closeModal();
     });
 
     // Cerrar modal - botón Cancelar
-    cancelRewardBtn.addEventListener('click', function() {
+    cancelRewardBtn.addEventListener('click', function () {
         closeModal();
     });
 
     // Cerrar modal haciendo clic fuera de él
-    rewardModal.addEventListener('click', function(e) {
+    rewardModal.addEventListener('click', function (e) {
         if (e.target === rewardModal) {
             closeModal();
         }
     });
 
     // Cerrar modal con tecla Escape
-    document.addEventListener('keydown', function(e) {
+    document.addEventListener('keydown', function (e) {
         if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) {
             closeModal();
         }
     });
 
-        // Manejar el reclamo del premio
-    claimRewardBtn.addEventListener('click', function() {
+    // Manejar el reclamo del premio
+    claimRewardBtn.addEventListener('click', function () {
         if (!this.disabled && acceptTermsCheckbox.checked) {
             // Generar código de premio (puedes cambiar esta lógica)
 
@@ -455,84 +612,81 @@ async function sendMessageToAI() {
         }
     });
 
+}
+
+function closeSuccessRewardModal() {
+    const successRewardModal = document.getElementById("successRewardModal");
+    successRewardModal.classList.add("hidden");
+    document.body.style.overflow = ''; // Restaurar scroll
+}
 
-    // Generar más confetti dinámicamente
-        console.log("Rewards initialized");
-    }
-   
-    function closeSuccessRewardModal() {
-        const successRewardModal = document.getElementById("successRewardModal");
-        successRewardModal.classList.add("hidden");
-        document.body.style.overflow = ''; // Restaurar scroll
-    }
+// Función para cerrar el modal
+function closeModal() {
+    rewardModal.classList.add('hidden');
+    document.body.style.overflow = ''; // Restaurar scroll
 
-    // Función para cerrar el modal
-    function closeModal() {
-        rewardModal.classList.add('hidden');
-        document.body.style.overflow = ''; // Restaurar scroll
-        
-        // Reset del formulario al cerrar
-        acceptTermsCheckbox.checked = false;
+    // Reset del formulario al cerrar
+    acceptTermsCheckbox.checked = false;
+    claimRewardBtn.disabled = true;
+    claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+}
+
+// Manejar el checkbox de términos y condiciones
+acceptTermsCheckbox.addEventListener('change', function () {
+    if (this.checked) {
+        // Habilitar botón de reclamar
+        claimRewardBtn.disabled = false;
+        claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
+    } else {
+        // Deshabilitar botón de reclamar
         claimRewardBtn.disabled = true;
         claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
     }
-
-    // Manejar el checkbox de términos y condiciones
-    acceptTermsCheckbox.addEventListener('change', function() {
-        if (this.checked) {
-            // Habilitar botón de reclamar
-            claimRewardBtn.disabled = false;
-            claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
-        } else {
-            // Deshabilitar botón de reclamar
-            claimRewardBtn.disabled = true;
-            claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
-        }
-    });
+});
 
 
 
-    // Función para mostrar mensaje de éxito con el código
-    function showRewardSuccess(code) {
-        // Crear elemento de notificación de éxito
-        const successToast = document.createElement('div');
-        successToast.className = `
+// Función para mostrar mensaje de éxito con el código
+function showRewardSuccess(code) {
+    // Crear elemento de notificación de éxito
+    const successToast = document.createElement('div');
+    successToast.className = `
             fixed top-4 left-1/2 transform -translate-x-1/2 
             bg-green-500 text-white text-sm rounded-lg px-6 py-4 
             shadow-lg z-50 max-w-sm text-center
         `;
-        successToast.innerHTML = `
+    successToast.innerHTML = `
             <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
             <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
             <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
         `;
-        
-        document.body.appendChild(successToast);
-        
-        // Remover después de 5 segundos
-        setTimeout(() => {
-            if (successToast.parentNode) {
-                successToast.remove();
-            }
-        }, 5000);
-    }
 
-    // Función para resetear el progreso de recompensa
-    function resetRewardProgress() {
-        const progressBar = document.getElementById('progressBar');
-        const progressText = document.getElementById('progressText');
-        const rewardBtn = document.getElementById('rewardBtn');
-        
-        // Resetear a 0%
-        progressBar.style.width = '0%';
-        progressText.textContent = '0%';
-        
-        // Deshabilitar botón de recompensa
-        rewardBtn.disabled = true;
-        rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
-        rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
-        rewardBtn.textContent = '🎉 ¡Reclamar!';
-    }
+    document.body.appendChild(successToast);
+
+    // Remover después de 5 segundos
+    setTimeout(() => {
+        if (successToast.parentNode) {
+            successToast.remove();
+        }
+    }, 5000);
+}
+
+// Función para resetear el progreso de recompensa
+function resetRewardProgress() {
+    const progressBar = document.getElementById('progressBar');
+    const progressText = document.getElementById('progressText');
+    const rewardBtn = document.getElementById('rewardBtn');
+
+    // Resetear a 0%
+    progressBar.style.width = '0%';
+    progressText.textContent = '0%';
+
+    // Deshabilitar botón de recompensa
+    rewardBtn.disabled = true;
+    rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+    rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
+    rewardBtn.textContent = '🎉 ¡Reclamar!';
+}
 
 //#endregion
 // --- APP initialization ---

+ 3 - 3
services/data_service.py

@@ -46,9 +46,9 @@ ESQUEMA DE BASE DE DATOS SQLITE (data.db)
 - price        INTEGER NOT NULL
 - image        TEXT (URL de la imagen)
 - status       INTEGER DEFAULT 1 NOT NULL CHECK (status IN (0, 1)) -- 0: Inactivo, 1: Activo
-- promo_id     INTEGER (ID de la promoción asociada, si existe)
-- promo_price  INTEGER (Precio promocional, si existe)
-- promo_day    INTEGER (Día de la semana para la promoción, 1-7)
+- promo_id     INTEGER (ID de la promoción asociada, si existe)(puede ser null)
+- promo_price  INTEGER (Precio promocional, si existe)(puede ser null)
+- promo_day    INTEGER (Día de la semana para la promoción, 1-7)(puede ser null)
 (Guarda los productos disponibles para venta con su estado activo/inactivo)
 
 3. Tabla: sales