app.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  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. // --- Variables de Usuario ---
  10. let userId = -1;
  11. let userName = "Cliente";
  12. let userTable = null;
  13. let userToken = null;
  14. // --- Datos de Productos y Carrito ---
  15. let products = [];
  16. let cart = [];
  17. let itsEmpty = true;
  18. // --- Historial de Chat ---
  19. let chatHistory = [
  20. { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" }
  21. ];
  22. // --- Elementos del DOM: Productos y Carrito ---
  23. const productListElement = document.getElementById("productList");
  24. const cartItemsElement = document.getElementById("cartItems");
  25. const cartTotalElement = document.getElementById("cartTotal");
  26. const emptyCartTextElement = document.getElementById("emptyCartText");
  27. const checkoutButton = document.getElementById("checkoutButton");
  28. const originalCheckoutButtonText = checkoutButton ? checkoutButton.textContent : "Finalizar Pedido";
  29. const cartCountElement = document.getElementById("cartCount");
  30. // --- Elementos del DOM: Chat ---
  31. const chatMessagesElement = document.getElementById("chatMessages");
  32. const chatInputElement = document.getElementById("chatInput");
  33. const chatForm = document.getElementById("chatForm");
  34. const aiLoadingIndicator = document.getElementById("aiLoadingIndicator");
  35. const chatSuggestionsElement = document.getElementById("chatSuggestions");
  36. //#region --- Inicialización y Configuracion ---
  37. async function initializeApp() {
  38. updateCartDisplay();
  39. initializeChat();
  40. setupBasicListeners();
  41. showGlobalLoader("Cargando productos...");
  42. setupShoppingCart(userId, userToken,userName);
  43. await renderProducts();
  44. showGUI();
  45. hideGlobalLoader();
  46. const chatSuggestions = Array.from(chatSuggestionsElement.children);
  47. chatSuggestions.forEach(suggestion => {
  48. suggestion.addEventListener("click", () => {
  49. sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
  50. });
  51. });
  52. }
  53. function initializeLoginModal() {
  54. const sessionModal = document.getElementById('sessionModal');
  55. const loginForm = document.getElementById('loginForm');
  56. sessionModal.classList.remove('hidden');
  57. loginForm.addEventListener('submit', async (event) => {
  58. event.preventDefault();
  59. event.stopPropagation();
  60. const fd = new FormData(loginForm);
  61. const email = fd.get('email').trim();
  62. const pin = fd.get('pin').trim();
  63. userTable = Number(fd.get('table').trim());
  64. if (!email || !pin || !userTable) {
  65. showError("Por favor, completa todos los campos.");
  66. return;
  67. }
  68. try{
  69. const {data} = await login(email, pin)
  70. userToken = data.token;
  71. userName = data.name;
  72. userId = data.id;
  73. updateProgress(data.reward_progress || 0);
  74. if (!userToken || data.id === undefined) {
  75. showError("Error al iniciar sesión.");
  76. return;
  77. }
  78. sessionModal.classList.add('hidden');
  79. initializeApp();
  80. }catch (error) {
  81. console.error(error)
  82. }
  83. })
  84. }
  85. function initializeChat() {
  86. if (!chatForm) return;
  87. chatForm.addEventListener("submit", (event) => {
  88. event.preventDefault();
  89. if (chatInputElement.value.trim() === "") return;
  90. sendMessageToAI();
  91. chatInputElement.addEventListener("input", () => {
  92. if (chatInputElement.value.trim() === "") {
  93. chatSuggestionsElement.classList.remove("hidden");
  94. } else {
  95. chatSuggestionsElement.classList.add("hidden");
  96. }
  97. });
  98. });
  99. }
  100. function setupBasicListeners() {
  101. if (!checkoutButton) return;
  102. checkoutButton.addEventListener("click", processOrder);
  103. initializeRewards();
  104. }
  105. //#endregion
  106. //#region ===== Utilidad =====
  107. function formatPrice(price) {
  108. return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
  109. }
  110. //#endregion
  111. //#region ===== Productos =====
  112. async function renderProducts() {
  113. if (!productListElement) return;
  114. const template = document.getElementById("product-card-template");
  115. if (!template) return;
  116. productListElement.innerHTML = "";
  117. console.log("Cargando productos...");
  118. products = await getProducts(userToken);
  119. products.forEach(product => {
  120. const clone = template.content.cloneNode(true);
  121. clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
  122. clone.querySelector(".product-name").textContent = product.name;
  123. clone.querySelector(".product-description").textContent = product.description;
  124. clone.querySelector(".product-price").textContent = formatPrice(product.price);
  125. clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
  126. const addBtn = clone.querySelector(".add-to-cart-btn");
  127. addBtn.dataset.productId = product.id; // el listener usa esta info
  128. productListElement.appendChild(clone);
  129. });
  130. document.querySelectorAll('.add-to-cart-btn').forEach(button => {
  131. button.addEventListener('click', (event) => {
  132. const productId = parseInt(event.target.dataset.productId);
  133. addToCart(productId, event.target);
  134. });
  135. });
  136. }
  137. //#endregion
  138. //#region ===== Carrito =====
  139. async function addToCart (productId, buttonElement = null) {
  140. const product = products.find(p => p.id === productId);
  141. if (!product) return;
  142. const cartItem = cart.find(item => item.id === productId);
  143. if (cartItem) {
  144. cartItem.quantity++;
  145. } else {
  146. cart.push({ ...product, quantity: 1 });
  147. }
  148. if (buttonElement) {
  149. const originalHTML = buttonElement.innerHTML;
  150. buttonElement.textContent = "✔ Agregado!";
  151. buttonElement.disabled = true;
  152. setTimeout(() => {
  153. buttonElement.innerHTML = originalHTML;
  154. buttonElement.disabled = false;
  155. }, 300);
  156. }
  157. updateCartDisplay();
  158. // Dentro de addToCart (después de updateCartDisplay())
  159. if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`);
  160. };
  161. async function removeFromCart (productId, removeAll = false) {
  162. const itemIndex = cart.findIndex(item => item.id === productId);
  163. if (itemIndex > -1) {
  164. if (removeAll || cart[itemIndex].quantity === 1) {
  165. cart.splice(itemIndex, 1);
  166. } else {
  167. cart[itemIndex].quantity--;
  168. }
  169. }
  170. updateCartDisplay();
  171. };
  172. function calculateTotal() {
  173. if (!cartTotalElement) return;
  174. const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  175. cartTotalElement.textContent = formatPrice(total);
  176. }
  177. function updateCartDisplay() {
  178. if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
  179. cartItemsElement.innerHTML = "";
  180. cartCountElement.textContent = cart.reduce((sum, item) => sum + item.quantity, 0);
  181. if (cart.length === 0) {
  182. cartCountElement.classList.add("hidden");
  183. emptyCartTextElement.classList.remove("hidden");
  184. checkoutButton.disabled = true;
  185. itsEmpty = true;
  186. } else {
  187. cartCountElement.classList.remove("hidden");
  188. if (cartCountElement && itsEmpty) {
  189. itsEmpty = false;
  190. cartCountElement.animate([
  191. { transform: 'scale(0)' },
  192. { transform: 'scale(1)' }
  193. ], {
  194. duration: 300,
  195. iterations: 1,
  196. easing: 'ease-in-out'
  197. })
  198. } else {
  199. cartCountElement.animate([
  200. { transform: 'scale(1) rotate(0deg)' },
  201. { transform: 'scale(1.2) rotate(180deg)' },
  202. { transform: 'scale(1) rotate(360deg)' }
  203. ], {
  204. duration: 300,
  205. iterations: 1,
  206. easing: 'ease-in-out'
  207. })
  208. }
  209. emptyCartTextElement.classList.add("hidden");
  210. checkoutButton.disabled = false;
  211. cart.forEach(item => {
  212. const cartItemHTML = `
  213. <div class="flex justify-between items-center border-b border-gray-700 pb-2 last:border-b-0 mb-2">
  214. <div>
  215. <h4 class="font-semibold text-base">${item.name} <span class="text-sm text-gray-400">(x${item.quantity})</span></h4>
  216. <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
  217. </div>
  218. <div class="flex items-center gap-1 sm:gap-2">
  219. <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>
  220. <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>
  221. <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">
  222. <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>
  223. </button>
  224. </div>
  225. </div>
  226. `;
  227. cartItemsElement.innerHTML += cartItemHTML;
  228. const plusButton = cartItemsElement.querySelectorAll(".plus-button");
  229. const minusButton = cartItemsElement.querySelectorAll(".minus-button");
  230. const removeButton = cartItemsElement.querySelectorAll(".remove-button");
  231. plusButton.forEach((btn, index) => {
  232. btn.addEventListener("click", () => {
  233. addToCart(item.id);
  234. btn.classList.add("animate-pulse");
  235. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  236. });
  237. });
  238. minusButton.forEach((btn, index) => {
  239. btn.addEventListener("click", () => {
  240. removeFromCart(item.id);
  241. btn.classList.add("animate-pulse");
  242. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  243. });
  244. });
  245. removeButton.forEach((btn, index) => {
  246. btn.addEventListener("click", () => {
  247. removeFromCart(item.id, true);
  248. btn.classList.add("animate-pulse");
  249. setTimeout(() => btn.classList.remove("animate-pulse"), 300);
  250. });
  251. });
  252. });
  253. cart
  254. }
  255. calculateTotal();
  256. }
  257. //#endregion
  258. //#region ===== Pedidos =====
  259. async function processOrder() {
  260. if (cart.length === 0) return;
  261. showGlobalLoader();
  262. if (checkoutButton) {
  263. checkoutButton.disabled = true;
  264. checkoutButton.textContent = "Procesando...";
  265. }
  266. try {
  267. const orderData = {
  268. customerId: userId,
  269. table: userTable,
  270. items: cart.map(item => ({ id: item.id, price: item.price, quantity: item.quantity})),
  271. totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
  272. orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
  273. };
  274. const data = await sendOrder(orderData,userToken);
  275. if (data && data.new_progress) {
  276. updateProgress(data.new_progress);
  277. }
  278. alert("Pedido enviado con éxito.");
  279. cart.forEach(item => {
  280. addHistoryRow({
  281. productName: item.name,
  282. quantity: item.quantity,
  283. price: item.price,
  284. });
  285. });
  286. cart = []
  287. updateCartDisplay();
  288. } catch (error) {
  289. console.error("Error al procesar la orden:", error);
  290. alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`);
  291. } finally {
  292. hideGlobalLoader();
  293. checkoutButton.disabled = cart.length === 0;
  294. checkoutButton.textContent = originalCheckoutButtonText
  295. }
  296. }
  297. //#endregion
  298. //#region ===== Chat =====
  299. function displayChatMessage(sender, message) {
  300. if (!chatMessagesElement) return;
  301. const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
  302. const messageDiv = document.createElement("div");
  303. messageDiv.classList.add("chat-bubble", bubbleClass);
  304. messageDiv.innerHTML = sender === "ai" && window.marked ? marked.parse(message) : message;
  305. chatMessagesElement.appendChild(messageDiv);
  306. chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
  307. }
  308. async function sendSuggestion(suggestion) {
  309. if (!chatInputElement || !aiLoadingIndicator) return;
  310. chatInputElement.value = suggestion;
  311. chatInputElement.focus();
  312. }
  313. async function sendMessageToAI() {
  314. if (!chatInputElement || !aiLoadingIndicator) return;
  315. const userInput = chatInputElement.value.trim();
  316. if (!userInput) return;
  317. displayChatMessage("user", userInput);
  318. chatSuggestionsElement.classList.add("hidden");
  319. chatInputElement.value = '';
  320. aiLoadingIndicator.classList.remove("hidden");
  321. try {
  322. const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
  323. if (!response) {
  324. displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
  325. } else if (response === "not_init") {
  326. if (await initializeService()) {
  327. const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
  328. if (response) {
  329. chatHistory = response.messageList;
  330. displayChatMessage("ai", response.assistantResponse);
  331. } else {
  332. displayChatMessage("ai", "Hubo un problema al enviar el mensaje.");
  333. }
  334. } else {
  335. displayChatMessage("ai", "Fallo la reconexión. Por favor, refresca la página.");
  336. }
  337. } else if (response.assistantResponse) {
  338. chatHistory = response.messageList;
  339. displayChatMessage("ai", response.assistantResponse);
  340. }
  341. } catch (error) {
  342. console.error("Error enviando mensaje a IA:", error);
  343. displayChatMessage("ai", `Error: ${error.message || "No se pudo conectar con el Chef IA."}`);
  344. } finally {
  345. aiLoadingIndicator.classList.add("hidden");
  346. if (chatInputElement) chatInputElement.focus();
  347. }
  348. }
  349. //#endregion
  350. //#region ===== Rewards =====
  351. // Referencias a elementos del DOM
  352. const rewardBtn = document.getElementById('rewardBtn');
  353. const rewardModal = document.getElementById('rewardModal');
  354. const closeRewardModal = document.getElementById('closeRewardModal');
  355. const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
  356. const cancelRewardBtn = document.getElementById('cancelRewardBtn');
  357. const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
  358. const claimRewardBtn = document.getElementById('claimRewardBtn');
  359. function initializeRewards() {
  360. // Abrir modal cuando se hace clic en el botón de recompensa
  361. closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
  362. rewardBtn.addEventListener('click', function() {
  363. if (!rewardBtn.disabled) {
  364. rewardModal.classList.remove('hidden');
  365. document.body.style.overflow = 'hidden'; // Evitar scroll del fondo
  366. }
  367. });
  368. // Cerrar modal - botón X
  369. closeRewardModal.addEventListener('click', function() {
  370. closeModal();
  371. });
  372. // Cerrar modal - botón Cancelar
  373. cancelRewardBtn.addEventListener('click', function() {
  374. closeModal();
  375. });
  376. // Cerrar modal haciendo clic fuera de él
  377. rewardModal.addEventListener('click', function(e) {
  378. if (e.target === rewardModal) {
  379. closeModal();
  380. }
  381. });
  382. // Cerrar modal con tecla Escape
  383. document.addEventListener('keydown', function(e) {
  384. if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) {
  385. closeModal();
  386. }
  387. });
  388. // Manejar el reclamo del premio
  389. claimRewardBtn.addEventListener('click', function() {
  390. if (!this.disabled && acceptTermsCheckbox.checked) {
  391. // Generar código de premio (puedes cambiar esta lógica)
  392. // Mostrar mensaje de éxito
  393. claimReward(userToken, userTable);
  394. // Cerrar modal
  395. closeModal();
  396. updateProgress(0);
  397. }
  398. });
  399. // Generar más confetti dinámicamente
  400. console.log("Rewards initialized");
  401. }
  402. function closeSuccessRewardModal() {
  403. const successRewardModal = document.getElementById("successRewardModal");
  404. successRewardModal.classList.add("hidden");
  405. document.body.style.overflow = ''; // Restaurar scroll
  406. }
  407. // Función para cerrar el modal
  408. function closeModal() {
  409. rewardModal.classList.add('hidden');
  410. document.body.style.overflow = ''; // Restaurar scroll
  411. // Reset del formulario al cerrar
  412. acceptTermsCheckbox.checked = false;
  413. claimRewardBtn.disabled = true;
  414. claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  415. }
  416. // Manejar el checkbox de términos y condiciones
  417. acceptTermsCheckbox.addEventListener('change', function() {
  418. if (this.checked) {
  419. // Habilitar botón de reclamar
  420. claimRewardBtn.disabled = false;
  421. claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
  422. } else {
  423. // Deshabilitar botón de reclamar
  424. claimRewardBtn.disabled = true;
  425. claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  426. }
  427. });
  428. // Función para mostrar mensaje de éxito con el código
  429. function showRewardSuccess(code) {
  430. // Crear elemento de notificación de éxito
  431. const successToast = document.createElement('div');
  432. successToast.className = `
  433. fixed top-4 left-1/2 transform -translate-x-1/2
  434. bg-green-500 text-white text-sm rounded-lg px-6 py-4
  435. shadow-lg z-50 max-w-sm text-center
  436. `;
  437. successToast.innerHTML = `
  438. <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
  439. <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
  440. <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
  441. `;
  442. document.body.appendChild(successToast);
  443. // Remover después de 5 segundos
  444. setTimeout(() => {
  445. if (successToast.parentNode) {
  446. successToast.remove();
  447. }
  448. }, 5000);
  449. }
  450. // Función para resetear el progreso de recompensa
  451. function resetRewardProgress() {
  452. const progressBar = document.getElementById('progressBar');
  453. const progressText = document.getElementById('progressText');
  454. const rewardBtn = document.getElementById('rewardBtn');
  455. // Resetear a 0%
  456. progressBar.style.width = '0%';
  457. progressText.textContent = '0%';
  458. // Deshabilitar botón de recompensa
  459. rewardBtn.disabled = true;
  460. rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
  461. rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
  462. rewardBtn.textContent = '🎉 ¡Reclamar!';
  463. }
  464. //#endregion
  465. // --- APP initialization ---
  466. document.addEventListener("DOMContentLoaded", async () => {
  467. createGlobalLoader();
  468. initializeLoginModal();
  469. hideGUI();
  470. // initializeApp()
  471. });