import { sendMessage as serviceSendMessage } from './service/chat.js'; import { getProducts, sendOrder } from './service/product.js'; import { login } from './service/auth.js' import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js'; import { updateProgress, claimReward } from './utils/progressBar.js'; import { showError } from './utils/error.js'; import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js'; import { hideGUI, showGUI } from './utils/gui.js'; import { smartSearch } from './utils/searching.js'; import { getUserData } from './service/user.js'; // --- Variables de Usuario --- let userId = -1; let userName = "Cliente"; let userTable = null; let userToken = null; // --- Datos de Productos y Carrito --- let Allproducts = []; let cart = []; let itsEmpty = true; let cacheMode = false // --- Categorias Importantes --- const favoriteCategories = ["Shop", "Pizzas Familiares", "Pizza Medianas" ]; // --- Historial de Chat --- let chatHistory = [ { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" } ]; // --- Elementos del DOM: Productos y Carrito --- const productListElement = document.getElementById("productList"); const cartItemsElement = document.getElementById("cartItems"); const cartTotalElement = document.getElementById("cartTotal"); const emptyCartTextElement = document.getElementById("emptyCartText"); const checkoutButton = document.getElementById("checkoutButton"); const originalCheckoutButtonText = checkoutButton ? checkoutButton.textContent : "Finalizar Pedido"; const cartCountElement = document.getElementById("cartCount"); // --- Elementos del DOM: Chat --- const chatMessagesElement = document.getElementById("chatMessages"); const chatInputElement = document.getElementById("chatInput"); const chatForm = document.getElementById("chatForm"); const aiLoadingIndicator = document.getElementById("aiLoadingIndicator"); 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(); showGUI(); hideGlobalLoader(); const chatSuggestions = Array.from(chatSuggestionsElement.children); chatSuggestions.forEach(suggestion => { suggestion.addEventListener("click", () => { sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent); }); }); } function initializeLoginModal() { const sessionModal = document.getElementById('sessionModal'); const loginForm = document.getElementById('loginForm'); const logoutBtn = document.getElementById('logoutBtn'); sessionModal.classList.remove('hidden'); loginForm.addEventListener('submit', async (event) => { event.preventDefault(); event.stopPropagation(); if (cacheMode) { sessionModal.classList.add('hidden'); initializeApp(); return; } const fd = new FormData(loginForm); const email = fd.get('email').trim(); const pin = fd.get('pin').trim(); userTable = Number(fd.get('table').trim()); if (!email || !pin || !userTable) { showError("Por favor, completa todos los campos."); return; } try { const { data } = await login(email, pin) userToken = data.token; userName = data.name; userId = data.id; setCookie("userToken", userToken, 7); updateProgress(data.reward_progress || 0); if (!userToken || data.id === undefined) { showError("Error al iniciar sesión."); return; } sessionModal.classList.add('hidden'); initializeApp(); } catch (error) { console.error(error) } }) if (logoutBtn) { logoutBtn.addEventListener('click', () => { setCookie("userToken", "", -1); // Eliminar cookie userId = -1; userName = "Cliente"; userTable = null; userToken = null; cacheMode = false; hideGUI(); sessionModal.classList.remove('hidden'); document.querySelector("#emailInputContainer").classList.remove("hidden"); document.querySelector("#pinInputContainer").classList.remove("hidden"); const loginTitle = document.getElementById("loginTitle"); loginTitle.textContent = "¡Bienvenido!"; // Restaurar atributos required a los inputs document.querySelector("#emailInput").setAttribute("required", ""); document.querySelector("#pinInput").setAttribute("required", ""); logoutBtn.classList.add("hidden"); }); } } function initializeChat() { if (!chatForm) return; chatForm.addEventListener("submit", (event) => { event.preventDefault(); if (chatInputElement.value.trim() === "") return; sendMessageToAI(); chatInputElement.addEventListener("input", () => { if (chatInputElement.value.trim() === "") { chatSuggestionsElement.classList.remove("hidden"); } else { chatSuggestionsElement.classList.add("hidden"); } }); }); } 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); const specialCases = { "shop": ["cerveza"] } // 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(); if (searchTerm.trim() === "") { renderProductsWithAnimation(Allproducts); return; } const finded = smartSearch(Allproducts, searchTerm); console.log(finded) // Renderizar con animación renderProductsWithAnimation(finded, false, searchTerm); }, 200); }); } //#endregion //#region ===== Utilidad ===== /** * Ordena resultados por relevancia */ function sortByRelevance(items) { console.log("Sorting results by relevance...", items); const sortedResults = items .sort((a, b) => { // Primero por score (descendente) if (a.score !== b.score) { return b.score - a.score; } // Luego por distancia (ascendente) if (a.distance !== b.distance) { return a.distance - b.distance; } // Finalmente por longitud del texto (ascendente) const aText = typeof a.item === 'string' ? a.item : JSON.stringify(a.item); const bText = typeof b.item === 'string' ? b.item : JSON.stringify(b.item); return aText.length - bText.length; }) .map(result => result.item); console.log("Sorted results: ",sortedResults); return sortedResults; } function setCookie(name, value, days) { const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString(); document.cookie = `${name}=${value}; expires=${expires}; path=/`; } function getCookie(name) { const cookies = document.cookie.split('; '); for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split('='); if (cookieName === name) { return decodeURIComponent(cookieValue); } } return null; } async function checkCache() { const tokenCache = getCookie("userToken"); let user = null if (tokenCache) { userToken = tokenCache; try{ const data = await getUserData(userToken); console.log(data) user = data.data }catch (error) { console.error("Error fetching user data:", error); console.error("Invalid user token, clearing cache."); setCookie("userToken", "", -1); // Eliminar cookie si es inválida return false; } if (user) { console.log("User data loaded from cache:", user); userId = user.id; userName = user.name; userTable = user.table; updateProgress(user.reward_progress || 0); const [emailInputContainer, pinInputContainer] = [document.getElementById("emailInputContainer"), document.getElementById("pinInputContainer")]; const [emailInput, pinInput] = [document.getElementById("emailInput"), document.getElementById("pinInput")]; const loginTitle = document.getElementById("loginTitle"); loginTitle.textContent = `¡Bienvenido ${user.name.split(" ")[0]}!`; emailInputContainer.classList.add("hidden"); emailInput.removeAttribute("required"); pinInputContainer.classList.add("hidden"); pinInput.removeAttribute("required"); document.querySelector("#logoutBtn").classList.remove("hidden"); cacheMode = true; return true } } return cacheMode } function formatPrice(price) { return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" }); } //#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)); for (const category of favoriteCategories.reverse() ) { if (categories.includes(category)) { categories = categories.filter(cat => cat !== category); categories.unshift(category); // Mover la categoría favorita al inicio } } 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", "á", "é", "í", "ó", "ú", "p"].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(products, groupInCategories = true, searchTerm = "") { if (!productListElement) return; const template = document.getElementById("product-card-template"); if (!template) return; productListElement.innerHTML = ""; let categoryContainers = []; if (groupInCategories) { categoryContainers = await createCategories(products); }else{ categoryContainers = [ { category: searchTerm, container: productListElement } ]; } if (products.length === 0) { const noProductsMessage = document.createElement("p"); noProductsMessage.textContent = "No hay productos disponibles."; productListElement.appendChild(noProductsMessage); return; } for (const { category, container } of categoryContainers) { let productsInCategory if (groupInCategories){ productsInCategory = products.filter(product => product.type === category); }else{ productsInCategory = products; } 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); }); container.appendChild(clone); }); } } async function renderProductsWithAnimation(products, groupInCategories = true, searchTerm = "") { 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 = ""; let categoryContainers = []; if (groupInCategories){ categoryContainers = await createCategories(products); }else { categoryContainers = [ { category: searchTerm, container: productListElement } ]; } console.log("Category containers created:", categoryContainers); 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 = []; if (groupInCategories){ productsInCategory = products.filter(product => product.type === category); }else { productsInCategory = products } 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 = Allproducts.find(p => p.id === productId); if (!product) return; const cartItem = cart.find(item => item.id === productId); if (cartItem) { cartItem.quantity++; } else { cart.push({ ...product, quantity: 1 }); } if (buttonElement) { const originalHTML = buttonElement.innerHTML; buttonElement.textContent = "✔ Agregado!"; buttonElement.disabled = true; setTimeout(() => { buttonElement.innerHTML = originalHTML; buttonElement.disabled = false; }, 300); } updateCartDisplay(); // Dentro de addToCart (después de updateCartDisplay()) if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`); }; async function removeFromCart(productId, removeAll = false) { const itemIndex = cart.findIndex(item => item.id === productId); if (itemIndex > -1) { if (removeAll || cart[itemIndex].quantity === 1) { cart.splice(itemIndex, 1); } else { cart[itemIndex].quantity--; } } updateCartDisplay(); }; function calculateTotal() { if (!cartTotalElement) return; const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); cartTotalElement.textContent = formatPrice(total); } function updateCartDisplay() { if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return; cartItemsElement.innerHTML = ""; cartCountElement.textContent = cart.reduce((sum, item) => sum + item.quantity, 0); if (cart.length === 0) { cartCountElement.classList.add("hidden"); emptyCartTextElement.classList.remove("hidden"); checkoutButton.disabled = true; itsEmpty = true; } else { cartCountElement.classList.remove("hidden"); if (cartCountElement && itsEmpty) { itsEmpty = false; cartCountElement.animate([ { transform: 'scale(0)' }, { transform: 'scale(1)' } ], { duration: 300, iterations: 1, easing: 'ease-in-out' }) } else { cartCountElement.animate([ { transform: 'scale(1) rotate(0deg)' }, { transform: 'scale(1.2) rotate(180deg)' }, { transform: 'scale(1) rotate(360deg)' } ], { duration: 300, iterations: 1, easing: 'ease-in-out' }) } emptyCartTextElement.classList.add("hidden"); checkoutButton.disabled = false; cart.forEach(item => { const cartItemHTML = `

${item.name} (x${item.quantity})

${formatPrice(item.price * item.quantity)}

`; cartItemsElement.innerHTML += cartItemHTML; const plusButton = cartItemsElement.querySelectorAll(".plus-button"); const minusButton = cartItemsElement.querySelectorAll(".minus-button"); const removeButton = cartItemsElement.querySelectorAll(".remove-button"); plusButton.forEach((btn, index) => { btn.addEventListener("click", () => { addToCart(item.id); btn.classList.add("animate-pulse"); setTimeout(() => btn.classList.remove("animate-pulse"), 300); }); }); minusButton.forEach((btn, index) => { btn.addEventListener("click", () => { removeFromCart(item.id); btn.classList.add("animate-pulse"); setTimeout(() => btn.classList.remove("animate-pulse"), 300); }); }); removeButton.forEach((btn, index) => { btn.addEventListener("click", () => { removeFromCart(item.id, true); btn.classList.add("animate-pulse"); setTimeout(() => btn.classList.remove("animate-pulse"), 300); }); }); }); cart } calculateTotal(); } //#endregion //#region ===== Pedidos ===== async function processOrder() { if (cart.length === 0) return; showGlobalLoader(); if (checkoutButton) { checkoutButton.disabled = true; checkoutButton.textContent = "Procesando..."; } try { const orderData = { customerId: userId, table: userTable, 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); if (data && data.new_progress) { updateProgress(data.new_progress); } alert("Pedido enviado con éxito."); cart.forEach(item => { addHistoryRow({ productName: item.name, quantity: item.quantity, price: item.price, }); }); cart = [] updateCartDisplay(); } catch (error) { console.error("Error al procesar la orden:", error); alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`); } finally { hideGlobalLoader(); checkoutButton.disabled = cart.length === 0; checkoutButton.textContent = originalCheckoutButtonText } } //#endregion //#region ===== Chat ===== function displayChatMessage(sender, message) { if (!chatMessagesElement) return; const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai"; const messageDiv = document.createElement("div"); messageDiv.classList.add("chat-bubble", bubbleClass); messageDiv.innerHTML = sender === "ai" && window.marked ? marked.parse(message) : message; chatMessagesElement.appendChild(messageDiv); chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight; } async function sendSuggestion(suggestion) { if (!chatInputElement || !aiLoadingIndicator) return; chatInputElement.value = suggestion; chatInputElement.focus(); } async function sendMessageToAI() { if (!chatInputElement || !aiLoadingIndicator) return; const userInput = chatInputElement.value.trim(); if (!userInput) return; displayChatMessage("user", userInput); chatSuggestionsElement.classList.add("hidden"); chatInputElement.value = ''; aiLoadingIndicator.classList.remove("hidden"); try { const response = await serviceSendMessage(userInput, chatHistory, userName, userToken); if (!response) { displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA."); } else if (response === "not_init") { if (await initializeService()) { const response = await serviceSendMessage(userInput, chatHistory, userName, userToken); if (response) { chatHistory = response.messageList; displayChatMessage("ai", response.assistantResponse); } else { displayChatMessage("ai", "Hubo un problema al enviar el mensaje."); } } else { displayChatMessage("ai", "Fallo la reconexión. Por favor, refresca la página."); } } else if (response.assistantResponse) { chatHistory = response.messageList; displayChatMessage("ai", response.assistantResponse); } } catch (error) { console.error("Error enviando mensaje a IA:", error); displayChatMessage("ai", `Error: ${error.message || "No se pudo conectar con el Chef IA."}`); } finally { aiLoadingIndicator.classList.add("hidden"); if (chatInputElement) chatInputElement.focus(); } } //#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'); function initializeRewards() { // Abrir modal cuando se hace clic en el botón de recompensa closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal); rewardBtn.addEventListener('click', function () { if (!rewardBtn.disabled) { rewardModal.classList.remove('hidden'); document.body.style.overflow = 'hidden'; // Evitar scroll del fondo } }); // Cerrar modal - botón X closeRewardModal.addEventListener('click', function () { closeModal(); }); // Cerrar modal - botón Cancelar cancelRewardBtn.addEventListener('click', function () { closeModal(); }); // Cerrar modal haciendo clic fuera de él rewardModal.addEventListener('click', function (e) { if (e.target === rewardModal) { closeModal(); } }); // Cerrar modal con tecla Escape document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) { closeModal(); } }); // Manejar el reclamo del premio claimRewardBtn.addEventListener('click', function () { if (!this.disabled && acceptTermsCheckbox.checked) { // Generar código de premio (puedes cambiar esta lógica) // Mostrar mensaje de éxito claimReward(userToken, userTable); // Cerrar modal closeModal(); updateProgress(0); } }); } 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 // 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'); } }); // 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 = `
🎉 ¡Premio Reclamado!
Tu código: ${code}
Muéstralo al mesero
`; 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 --- document.addEventListener("DOMContentLoaded", async () => { const isCacheValid = await checkCache(); if (!isCacheValid) { // Si la caché no es válida, redirigir al usuario o mostrar un mensaje console.warn("Cache is invalid"); } createGlobalLoader(); initializeLoginModal(); hideGUI(); // initializeApp() });