app.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. import { sendMessage as serviceSendMessage } from './service/chat.js';
  2. import { getProducts, sendOrder } from './service/product.js';
  3. import { login } from './service/auth.js'
  4. import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
  5. import { updateProgress, claimReward } from './utils/progressBar.js';
  6. import { showError } from './utils/error.js';
  7. import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
  8. import { hideGUI, showGUI } from './utils/gui.js';
  9. import { smartSearch } from './utils/searching.js';
  10. // --- Variables de Usuario ---
  11. let userId = -1;
  12. let userName = "Cliente";
  13. let userTable = null;
  14. let userToken = null;
  15. // --- Datos de Productos y Carrito ---
  16. let Allproducts = [];
  17. let cart = [];
  18. let itsEmpty = true;
  19. // --- Categorias Importantes ---
  20. const favoriteCategories = ["Shop", "Pizzas Familiares", "Pizza Medianas" ];
  21. // --- Historial de Chat ---
  22. let chatHistory = [
  23. { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" }
  24. ];
  25. // --- Elementos del DOM: Productos y Carrito ---
  26. const productListElement = document.getElementById("productList");
  27. const cartItemsElement = document.getElementById("cartItems");
  28. const cartTotalElement = document.getElementById("cartTotal");
  29. const emptyCartTextElement = document.getElementById("emptyCartText");
  30. const checkoutButton = document.getElementById("checkoutButton");
  31. const originalCheckoutButtonText = checkoutButton ? checkoutButton.textContent : "Finalizar Pedido";
  32. const cartCountElement = document.getElementById("cartCount");
  33. // --- Elementos del DOM: Chat ---
  34. const chatMessagesElement = document.getElementById("chatMessages");
  35. const chatInputElement = document.getElementById("chatInput");
  36. const chatForm = document.getElementById("chatForm");
  37. const aiLoadingIndicator = document.getElementById("aiLoadingIndicator");
  38. const chatSuggestionsElement = document.getElementById("chatSuggestions");
  39. //#region --- Inicialización y Configuracion ---
  40. async function initializeApp() {
  41. showGlobalLoader("Cargando productos...");
  42. await initializeProducts();
  43. await renderProducts(Allproducts);
  44. setupSearchListener();
  45. updateCartDisplay();
  46. setupShoppingCart(userId, userToken, userName);
  47. initializeChat();
  48. setupBasicListeners();
  49. showGUI();
  50. hideGlobalLoader();
  51. const chatSuggestions = Array.from(chatSuggestionsElement.children);
  52. chatSuggestions.forEach(suggestion => {
  53. suggestion.addEventListener("click", () => {
  54. sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
  55. });
  56. });
  57. }
  58. function initializeLoginModal() {
  59. const sessionModal = document.getElementById('sessionModal');
  60. const loginForm = document.getElementById('loginForm');
  61. sessionModal.classList.remove('hidden');
  62. loginForm.addEventListener('submit', async (event) => {
  63. event.preventDefault();
  64. event.stopPropagation();
  65. const fd = new FormData(loginForm);
  66. const email = fd.get('email').trim();
  67. const pin = fd.get('pin').trim();
  68. userTable = Number(fd.get('table').trim());
  69. if (!email || !pin || !userTable) {
  70. showError("Por favor, completa todos los campos.");
  71. return;
  72. }
  73. try {
  74. const { data } = await login(email, pin)
  75. userToken = data.token;
  76. userName = data.name;
  77. userId = data.id;
  78. updateProgress(data.reward_progress || 0);
  79. if (!userToken || data.id === undefined) {
  80. showError("Error al iniciar sesión.");
  81. return;
  82. }
  83. sessionModal.classList.add('hidden');
  84. initializeApp();
  85. } catch (error) {
  86. console.error(error)
  87. }
  88. })
  89. }
  90. function initializeChat() {
  91. if (!chatForm) return;
  92. chatForm.addEventListener("submit", (event) => {
  93. event.preventDefault();
  94. if (chatInputElement.value.trim() === "") return;
  95. sendMessageToAI();
  96. chatInputElement.addEventListener("input", () => {
  97. if (chatInputElement.value.trim() === "") {
  98. chatSuggestionsElement.classList.remove("hidden");
  99. } else {
  100. chatSuggestionsElement.classList.add("hidden");
  101. }
  102. });
  103. });
  104. }
  105. function setupBasicListeners() {
  106. if (!checkoutButton) return;
  107. checkoutButton.addEventListener("click", processOrder);
  108. initializeRewards();
  109. }
  110. function setupSearchListener() {
  111. const searchInput = document.getElementById("searchInput");
  112. if (!searchInput) return;
  113. let debounceTimer;
  114. searchInput.addEventListener("input", () => {
  115. // Limpiar el timer anterior si existe
  116. clearTimeout(debounceTimer);
  117. // Agregar una clase de "buscando" para el feedback visual
  118. productListElement.style.opacity = "0.7";
  119. productListElement.style.transform = "scale(0.98)";
  120. productListElement.style.transition = "opacity 0.2s ease, transform 0.2s ease";
  121. // Debounce de 200ms para evitar muchas llamadas
  122. debounceTimer = setTimeout(() => {
  123. const searchTerm = searchInput.value.toLowerCase();
  124. if (searchTerm.trim() === "") {
  125. renderProductsWithAnimation(Allproducts);
  126. return;
  127. }
  128. const finded = Allproducts.filter(product => {
  129. return smartSearch(product.name.split(" "), searchTerm).length > 0;
  130. });
  131. // Renderizar con animación
  132. renderProductsWithAnimation(finded);
  133. }, 200);
  134. });
  135. }
  136. //#endregion
  137. //#region ===== Utilidad =====
  138. function formatPrice(price) {
  139. return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
  140. }
  141. //#endregion
  142. //#region ===== Productos =====
  143. async function initializeProducts() {
  144. Allproducts = await getProducts(userToken);
  145. }
  146. async function createCategories(products) {
  147. let categories = new Set(products.map(product => product.type || "Sin categoría"));
  148. categories = Array.from(categories).sort((a, b) => a.localeCompare(b));
  149. for (const category of favoriteCategories.reverse() ) {
  150. if (categories.includes(category)) {
  151. categories = categories.filter(cat => cat !== category);
  152. categories.unshift(category); // Mover la categoría favorita al inicio
  153. }
  154. }
  155. if (!productListElement) return;
  156. const categoryContainers = categories.map(category => {
  157. const container = document.createElement("div");
  158. container.classList.add("category-container");
  159. container.classList.add("mb-8", "p-4");
  160. const title = document.createElement("h2");
  161. title.classList.add("category-title", "text-3xl", "font-bold", "border-b-2", "pb-3", "mb-4");
  162. let titleText = ["a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "p"].includes(category.charAt(category.length - 1).toLowerCase()) ? "s" :
  163. category.charAt(category.length - 1).toLowerCase() === "s" ? "" : "es";
  164. title.textContent = category + titleText;
  165. container.appendChild(title);
  166. const productList = document.createElement("div");
  167. productList.classList.add("product-list", "space-y-6");
  168. productList.id = `productList-${category}`;
  169. container.appendChild(productList);
  170. productListElement.appendChild(container);
  171. return { category, container: productList };
  172. });
  173. return categoryContainers;
  174. }
  175. async function renderProducts(products) {
  176. if (!productListElement) return;
  177. const template = document.getElementById("product-card-template");
  178. if (!template) return;
  179. productListElement.innerHTML = "";
  180. const categoryContainers = await createCategories(products);
  181. if (products.length === 0) {
  182. const noProductsMessage = document.createElement("p");
  183. noProductsMessage.textContent = "No hay productos disponibles.";
  184. productListElement.appendChild(noProductsMessage);
  185. return;
  186. }
  187. for (const { category, container } of categoryContainers) {
  188. let productsInCategory = products.filter(product => product.type === category);
  189. if (productsInCategory.length === 0) continue;
  190. productsInCategory.forEach(product => {
  191. const clone = template.content.cloneNode(true);
  192. clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
  193. clone.querySelector(".product-name").textContent = product.name;
  194. clone.querySelector(".product-description").textContent = product.description;
  195. clone.querySelector(".product-price").textContent = formatPrice(product.price);
  196. clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
  197. const addBtn = clone.querySelector(".add-to-cart-btn");
  198. addBtn.dataset.productId = product.id; // el listener usa esta info
  199. // Agregar event listener directamente al botón clonado
  200. addBtn.addEventListener('click', (event) => {
  201. const productId = parseInt(event.target.dataset.productId);
  202. addToCart(productId, event.target);
  203. });
  204. container.appendChild(clone);
  205. });
  206. }
  207. }
  208. async function renderProductsWithAnimation(products) {
  209. if (!productListElement) return;
  210. const template = document.getElementById("product-card-template");
  211. if (!template) return;
  212. // Fade out actual content
  213. productListElement.style.opacity = "0";
  214. productListElement.style.transform = "scale(0.95)";
  215. setTimeout(async () => {
  216. productListElement.innerHTML = "";
  217. const categoryContainers = await createCategories(products);
  218. console.log("Category containers created:", categoryContainers);
  219. if (products.length === 0) {
  220. const noProductsMessage = document.createElement("p");
  221. noProductsMessage.textContent = "No hay productos disponibles.";
  222. noProductsMessage.style.opacity = "0";
  223. noProductsMessage.style.transform = "translateY(20px)";
  224. noProductsMessage.style.transition = "opacity 0.3s ease, transform 0.3s ease";
  225. productListElement.appendChild(noProductsMessage);
  226. // Restore container
  227. productListElement.style.opacity = "1";
  228. productListElement.style.transform = "scale(1)";
  229. // Animate message
  230. setTimeout(() => {
  231. noProductsMessage.style.opacity = "1";
  232. noProductsMessage.style.transform = "translateY(0)";
  233. }, 50);
  234. return;
  235. }
  236. let animationDelay = 0;
  237. for (const { category, container } of categoryContainers) {
  238. let productsInCategory = products.filter(product => product.type === category);
  239. if (productsInCategory.length === 0) continue;
  240. productsInCategory.forEach((product, index) => {
  241. const clone = template.content.cloneNode(true);
  242. clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
  243. clone.querySelector(".product-name").textContent = product.name;
  244. clone.querySelector(".product-description").textContent = product.description;
  245. clone.querySelector(".product-price").textContent = formatPrice(product.price);
  246. clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
  247. const addBtn = clone.querySelector(".add-to-cart-btn");
  248. addBtn.dataset.productId = product.id;
  249. addBtn.addEventListener('click', (event) => {
  250. const productId = parseInt(event.target.dataset.productId);
  251. addToCart(productId, event.target);
  252. });
  253. // Get the first child (the product card element)
  254. const productCard = clone.children[0];
  255. // Set initial animation state
  256. productCard.style.opacity = "0";
  257. productCard.style.transform = "translateY(20px) scale(0.95)";
  258. productCard.style.transition = "opacity 0.4s ease, transform 0.4s ease";
  259. container.appendChild(clone);
  260. // Animate in with staggered delay
  261. setTimeout(() => {
  262. productCard.style.opacity = "1";
  263. productCard.style.transform = "translateY(0) scale(1)";
  264. }, animationDelay);
  265. animationDelay += 50; // 50ms delay between each product
  266. });
  267. }
  268. // Restore container with smooth transition
  269. productListElement.style.opacity = "1";
  270. productListElement.style.transform = "scale(1)";
  271. }, 150); // Wait for fade out to complete
  272. }
  273. //#endregion
  274. //#region ===== Carrito =====
  275. async function addToCart(productId, buttonElement = null) {
  276. const product = Allproducts.find(p => p.id === productId);
  277. if (!product) return;
  278. const cartItem = cart.find(item => item.id === productId);
  279. if (cartItem) {
  280. cartItem.quantity++;
  281. } else {
  282. cart.push({ ...product, quantity: 1 });
  283. }
  284. if (buttonElement) {
  285. const originalHTML = buttonElement.innerHTML;
  286. buttonElement.textContent = "✔ Agregado!";
  287. buttonElement.disabled = true;
  288. setTimeout(() => {
  289. buttonElement.innerHTML = originalHTML;
  290. buttonElement.disabled = false;
  291. }, 300);
  292. }
  293. updateCartDisplay();
  294. // Dentro de addToCart (después de updateCartDisplay())
  295. if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`);
  296. };
  297. async function removeFromCart(productId, removeAll = false) {
  298. const itemIndex = cart.findIndex(item => item.id === productId);
  299. if (itemIndex > -1) {
  300. if (removeAll || cart[itemIndex].quantity === 1) {
  301. cart.splice(itemIndex, 1);
  302. } else {
  303. cart[itemIndex].quantity--;
  304. }
  305. }
  306. updateCartDisplay();
  307. };
  308. function calculateTotal() {
  309. if (!cartTotalElement) return;
  310. const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  311. cartTotalElement.textContent = formatPrice(total);
  312. }
  313. function updateCartDisplay() {
  314. if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
  315. cartItemsElement.innerHTML = "";
  316. cartCountElement.textContent = cart.reduce((sum, item) => sum + item.quantity, 0);
  317. if (cart.length === 0) {
  318. cartCountElement.classList.add("hidden");
  319. emptyCartTextElement.classList.remove("hidden");
  320. checkoutButton.disabled = true;
  321. itsEmpty = true;
  322. } else {
  323. cartCountElement.classList.remove("hidden");
  324. if (cartCountElement && itsEmpty) {
  325. itsEmpty = false;
  326. cartCountElement.animate([
  327. { transform: 'scale(0)' },
  328. { transform: 'scale(1)' }
  329. ], {
  330. duration: 300,
  331. iterations: 1,
  332. easing: 'ease-in-out'
  333. })
  334. } else {
  335. cartCountElement.animate([
  336. { transform: 'scale(1) rotate(0deg)' },
  337. { transform: 'scale(1.2) rotate(180deg)' },
  338. { transform: 'scale(1) rotate(360deg)' }
  339. ], {
  340. duration: 300,
  341. iterations: 1,
  342. easing: 'ease-in-out'
  343. })
  344. }
  345. emptyCartTextElement.classList.add("hidden");
  346. checkoutButton.disabled = false;
  347. cart.forEach(item => {
  348. const cartItemHTML = `
  349. <div class="flex justify-between items-center border-b border-gray-700 pb-2 last:border-b-0 mb-2">
  350. <div>
  351. <h4 class="font-semibold text-base">${item.name} <span class="text-sm text-gray-400">(x${item.quantity})</span></h4>
  352. <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
  353. </div>
  354. <div class="flex items-center gap-1 sm:gap-2">
  355. <button class="plus-button text-green-500 hover:text-green-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">+</button>
  356. <button class="minus-button text-yellow-500 hover:text-yellow-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">-</button>
  357. <button class="remove-button text-red-500 hover:text-red-400 text-base sm:text-lg p-1 rounded-full hover:bg-gray-700 transition-colors">
  358. <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 sm:w-5 sm:h-5 pointer-events-none"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.032 3.22.096M15 5.79V4.5A2.25 2.25 0 0012.75 2.25h-1.5A2.25 2.25 0 009 4.5v1.29m0 0L9 19.5M15 5.79l-1.5-1.5M9 5.79l1.5-1.5" /></svg>
  359. </button>
  360. </div>
  361. </div>
  362. `;
  363. cartItemsElement.innerHTML += cartItemHTML;
  364. const plusButton = cartItemsElement.querySelectorAll(".plus-button");
  365. const minusButton = cartItemsElement.querySelectorAll(".minus-button");
  366. const removeButton = cartItemsElement.querySelectorAll(".remove-button");
  367. plusButton.forEach((btn, index) => {
  368. btn.addEventListener("click", () => {
  369. addToCart(item.id);
  370. btn.classList.add("animate-pulse");
  371. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  372. });
  373. });
  374. minusButton.forEach((btn, index) => {
  375. btn.addEventListener("click", () => {
  376. removeFromCart(item.id);
  377. btn.classList.add("animate-pulse");
  378. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  379. });
  380. });
  381. removeButton.forEach((btn, index) => {
  382. btn.addEventListener("click", () => {
  383. removeFromCart(item.id, true);
  384. btn.classList.add("animate-pulse");
  385. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  386. });
  387. });
  388. });
  389. cart
  390. }
  391. calculateTotal();
  392. }
  393. //#endregion
  394. //#region ===== Pedidos =====
  395. async function processOrder() {
  396. if (cart.length === 0) return;
  397. showGlobalLoader();
  398. if (checkoutButton) {
  399. checkoutButton.disabled = true;
  400. checkoutButton.textContent = "Procesando...";
  401. }
  402. try {
  403. const orderData = {
  404. customerId: userId,
  405. table: userTable,
  406. items: cart.map(item => ({ id: item.id, price: item.price, quantity: item.quantity })),
  407. totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
  408. orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
  409. };
  410. const data = await sendOrder(orderData, userToken);
  411. if (data && data.new_progress) {
  412. updateProgress(data.new_progress);
  413. }
  414. alert("Pedido enviado con éxito.");
  415. cart.forEach(item => {
  416. addHistoryRow({
  417. productName: item.name,
  418. quantity: item.quantity,
  419. price: item.price,
  420. });
  421. });
  422. cart = []
  423. updateCartDisplay();
  424. } catch (error) {
  425. console.error("Error al procesar la orden:", error);
  426. alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`);
  427. } finally {
  428. hideGlobalLoader();
  429. checkoutButton.disabled = cart.length === 0;
  430. checkoutButton.textContent = originalCheckoutButtonText
  431. }
  432. }
  433. //#endregion
  434. //#region ===== Chat =====
  435. function displayChatMessage(sender, message) {
  436. if (!chatMessagesElement) return;
  437. const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
  438. const messageDiv = document.createElement("div");
  439. messageDiv.classList.add("chat-bubble", bubbleClass);
  440. messageDiv.innerHTML = sender === "ai" && window.marked ? marked.parse(message) : message;
  441. chatMessagesElement.appendChild(messageDiv);
  442. chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
  443. }
  444. async function sendSuggestion(suggestion) {
  445. if (!chatInputElement || !aiLoadingIndicator) return;
  446. chatInputElement.value = suggestion;
  447. chatInputElement.focus();
  448. }
  449. async function sendMessageToAI() {
  450. if (!chatInputElement || !aiLoadingIndicator) return;
  451. const userInput = chatInputElement.value.trim();
  452. if (!userInput) return;
  453. displayChatMessage("user", userInput);
  454. chatSuggestionsElement.classList.add("hidden");
  455. chatInputElement.value = '';
  456. aiLoadingIndicator.classList.remove("hidden");
  457. try {
  458. const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
  459. if (!response) {
  460. displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
  461. } else if (response === "not_init") {
  462. if (await initializeService()) {
  463. const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
  464. if (response) {
  465. chatHistory = response.messageList;
  466. displayChatMessage("ai", response.assistantResponse);
  467. } else {
  468. displayChatMessage("ai", "Hubo un problema al enviar el mensaje.");
  469. }
  470. } else {
  471. displayChatMessage("ai", "Fallo la reconexión. Por favor, refresca la página.");
  472. }
  473. } else if (response.assistantResponse) {
  474. chatHistory = response.messageList;
  475. displayChatMessage("ai", response.assistantResponse);
  476. }
  477. } catch (error) {
  478. console.error("Error enviando mensaje a IA:", error);
  479. displayChatMessage("ai", `Error: ${error.message || "No se pudo conectar con el Chef IA."}`);
  480. } finally {
  481. aiLoadingIndicator.classList.add("hidden");
  482. if (chatInputElement) chatInputElement.focus();
  483. }
  484. }
  485. //#endregion
  486. //#region ===== Rewards =====
  487. // Referencias a elementos del DOM
  488. const rewardBtn = document.getElementById('rewardBtn');
  489. const rewardModal = document.getElementById('rewardModal');
  490. const closeRewardModal = document.getElementById('closeRewardModal');
  491. const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
  492. const cancelRewardBtn = document.getElementById('cancelRewardBtn');
  493. const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
  494. const claimRewardBtn = document.getElementById('claimRewardBtn');
  495. function initializeRewards() {
  496. // Abrir modal cuando se hace clic en el botón de recompensa
  497. closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
  498. rewardBtn.addEventListener('click', function () {
  499. if (!rewardBtn.disabled) {
  500. rewardModal.classList.remove('hidden');
  501. document.body.style.overflow = 'hidden'; // Evitar scroll del fondo
  502. }
  503. });
  504. // Cerrar modal - botón X
  505. closeRewardModal.addEventListener('click', function () {
  506. closeModal();
  507. });
  508. // Cerrar modal - botón Cancelar
  509. cancelRewardBtn.addEventListener('click', function () {
  510. closeModal();
  511. });
  512. // Cerrar modal haciendo clic fuera de él
  513. rewardModal.addEventListener('click', function (e) {
  514. if (e.target === rewardModal) {
  515. closeModal();
  516. }
  517. });
  518. // Cerrar modal con tecla Escape
  519. document.addEventListener('keydown', function (e) {
  520. if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) {
  521. closeModal();
  522. }
  523. });
  524. // Manejar el reclamo del premio
  525. claimRewardBtn.addEventListener('click', function () {
  526. if (!this.disabled && acceptTermsCheckbox.checked) {
  527. // Generar código de premio (puedes cambiar esta lógica)
  528. // Mostrar mensaje de éxito
  529. claimReward(userToken, userTable);
  530. // Cerrar modal
  531. closeModal();
  532. updateProgress(0);
  533. }
  534. });
  535. }
  536. function closeSuccessRewardModal() {
  537. const successRewardModal = document.getElementById("successRewardModal");
  538. successRewardModal.classList.add("hidden");
  539. document.body.style.overflow = ''; // Restaurar scroll
  540. }
  541. // Función para cerrar el modal
  542. function closeModal() {
  543. rewardModal.classList.add('hidden');
  544. document.body.style.overflow = ''; // Restaurar scroll
  545. // Reset del formulario al cerrar
  546. acceptTermsCheckbox.checked = false;
  547. claimRewardBtn.disabled = true;
  548. claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  549. }
  550. // Manejar el checkbox de términos y condiciones
  551. acceptTermsCheckbox.addEventListener('change', function () {
  552. if (this.checked) {
  553. // Habilitar botón de reclamar
  554. claimRewardBtn.disabled = false;
  555. claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
  556. } else {
  557. // Deshabilitar botón de reclamar
  558. claimRewardBtn.disabled = true;
  559. claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  560. }
  561. });
  562. // Función para mostrar mensaje de éxito con el código
  563. function showRewardSuccess(code) {
  564. // Crear elemento de notificación de éxito
  565. const successToast = document.createElement('div');
  566. successToast.className = `
  567. fixed top-4 left-1/2 transform -translate-x-1/2
  568. bg-green-500 text-white text-sm rounded-lg px-6 py-4
  569. shadow-lg z-50 max-w-sm text-center
  570. `;
  571. successToast.innerHTML = `
  572. <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
  573. <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
  574. <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
  575. `;
  576. document.body.appendChild(successToast);
  577. // Remover después de 5 segundos
  578. setTimeout(() => {
  579. if (successToast.parentNode) {
  580. successToast.remove();
  581. }
  582. }, 5000);
  583. }
  584. // Función para resetear el progreso de recompensa
  585. function resetRewardProgress() {
  586. const progressBar = document.getElementById('progressBar');
  587. const progressText = document.getElementById('progressText');
  588. const rewardBtn = document.getElementById('rewardBtn');
  589. // Resetear a 0%
  590. progressBar.style.width = '0%';
  591. progressText.textContent = '0%';
  592. // Deshabilitar botón de recompensa
  593. rewardBtn.disabled = true;
  594. rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  595. rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
  596. rewardBtn.textContent = '🎉 ¡Reclamar!';
  597. }
  598. //#endregion
  599. // --- APP initialization ---
  600. document.addEventListener("DOMContentLoaded", async () => {
  601. createGlobalLoader();
  602. initializeLoginModal();
  603. hideGUI();
  604. // initializeApp()
  605. });