Browse Source

sale la version v2

Erwin Jacimino 4 tháng trước cách đây
mục cha
commit
579361020e

+ 1 - 1
app.py

@@ -43,7 +43,7 @@ def create_app() -> FastAPI:
         logger.info("Adding CORS middleware")
         app.add_middleware(
             CORSMiddleware,
-            allow_origins=["https://admin.kleinexpress.store", "https://kleinexpress.store", "http://localhost:8000"],
+            allow_origins=["https://admin.kleinexpress.store", "https://kleinexpress.store", "http://localhost:8000", "http://10.10.10.2:8000"],
             allow_credentials=True,
             allow_methods=["*"],
             allow_headers=["*"],

+ 137 - 23
fudo/fudo.py

@@ -1,3 +1,4 @@
+import itertools
 import math
 from time import time
 import requests
@@ -6,7 +7,9 @@ import os
 import redis
 from logging import getLogger
 from models.items import Product
-
+from concurrent.futures import ThreadPoolExecutor
+import aiohttp
+import asyncio
 logger = getLogger(__name__)
 
 api_token = os.getenv('FUDO_API_KEY')
@@ -80,6 +83,15 @@ def get_token():
     
     return token
 
+def get_modifiers():
+    token = get_token()
+    url = 'https://api.fu.do/v1alpha1/product-modifiers?include=product'
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    return r.json()
+
 def get_categories():
     token = get_token()
     url = 'https://api.fu.do/v1alpha1/product-categories'
@@ -87,7 +99,14 @@ def get_categories():
         'Authorization': 'Bearer ' + token
     }
     r = requests.get(url, headers=headers)
-    return r.json()
+    return r.json().get("data")
+
+def get_category_dict():
+    categories = get_categories()
+    category_dict = {}
+    for category in categories:
+        category_dict[category["id"]] = category["attributes"]["name"]
+    return category_dict    
 
 def get_category(id_category:int):
     token = get_token()
@@ -125,6 +144,7 @@ def get_product(id_category:int):
         image=data["attributes"]["imageUrl"],
         description=data["attributes"]["description"],
         status=1 if data["attributes"]["active"] and data else 0,
+        kitchen_id=data["relationships"]["kitchen"]["data"]["id"],
         promo_day=None,
         promo_price=None,
         promo_id=None
@@ -140,30 +160,78 @@ def get_products(page: int = 1):
     r = requests.get(url, headers=headers)
     return r.json()['data']
 
-def get_all_products():
-    """Método para obtener todos los productos de la base de datos.
 
-    Returns:
-        Diccionario de productos con IDs como claves y datos de productos como valores.
+async def fetch_page(session, url_template, token, page_num):
+    """
+    Realiza la petición de una sola página de forma asíncrona.
+    Retorna una tupla (numero_pagina, lista_datos).
     """
-    url = 'https://api.fu.do/v1alpha1/products?page[number]={}'
+    url = url_template.format(page_num)
+    headers = {'Authorization': 'Bearer ' + token}
+    
+    try:
+        async with session.get(url, headers=headers) as response:
+            if response.status == 200:
+                payload = await response.json()
+                data = payload.get('data', [])
+                return page_num, data
+            else:
+                # Si falla (ej. 404 o 500), asumimos fin de datos o error recuperable
+                return page_num, []
+    except Exception:
+        return page_num, []
+
+async def get_all_indexed_products():
+    url_template = 'https://api.fu.do/v1alpha1/products?page[number]={}'
+    token = get_token() # Asumiendo que esta función existe y es síncrona
     products = {}
-    token = get_token()
-    page = 1
-    while True:
-        r = requests.get(url.format(page), headers={'Authorization': 'Bearer ' + token})
-        if r.status_code != 200:
-            if products:
-                return products
+    
+    # Configuración de fuerza bruta
+    BATCH_SIZE = 8  # Cantidad de peticiones simultáneas
+    current_page = 1
+    keep_fetching = True
+
+    # Configuración de conexión (límite de conexiones simultáneas)
+    connector = aiohttp.TCPConnector(limit=100)
+    
+    async with aiohttp.ClientSession(connector=connector) as session:
+        while keep_fetching:
+            # Crear tareas para el bloque actual (ej: páginas 1 a 50)
+            tasks = [
+                fetch_page(session, url_template, token, page) 
+                for page in range(current_page, current_page + BATCH_SIZE)
+            ]
+            
+            # Ejecutar bloque simultáneamente
+            results = await asyncio.gather(*tasks)
+            
+            # Procesar resultados
+            empty_page_found = False
+            
+            for page_num, data in results:
+                if not data:
+                    empty_page_found = True
+                    # No rompemos el loop inmediato para procesar datos previos en el batch si existen
+                    continue
+                
+                for product in data:
+                    if product["attributes"]["enableQrMenu"]:
+                        products[product['id']] = product
+            
+            # Lógica de parada
+            if empty_page_found:
+                # Si algún request en el bloque volvió vacío, asumimos que llegamos al final
+                keep_fetching = False
             else:
-                return None
-        data = r.json().get('data')
-        if not data:
-            return products
-        for product in data:
-            products[product['id']] = product
-        page += 1
-        
+                # Si todo el bloque trajo datos, preparamos el siguiente bloque
+                current_page += BATCH_SIZE
+
+    return products
+
+
+async def get_all_products():
+    """Método para obtener todos los productos de la base de datos."""
+    return list((await get_all_indexed_products()).values())
 N_PER_PAGE = 100
 
 def _get_page_bounds(page: int, token: str):
@@ -276,6 +344,52 @@ def get_table(number: int):
         return None
 
 
+def get_table_items(table_number: int):
+    token = get_token()
+    table = get_table(table_number)
+    if not table:
+        return None
+    
+    active_sale = get_active_sale(table)
+    if not active_sale:
+        return None
+        
+    sale = get_sale(active_sale['id'])
+    if not sale:
+        return None
+        
+    items = sale["data"]['relationships']['items']['data']
+    
+    # 1. Función ajustada para retornar datos
+    def peticion(url, headers):
+        try:
+            r = requests.get(url, headers=headers)
+            if r.status_code == 200:
+                # Aquí procesas el resultado como necesites
+                return r.json() 
+            return None
+        except Exception:
+            return None
+
+    id_list = list(map(lambda x: f"https://api.fu.do/v1alpha1/items/{x['id']}?include=product", items))
+    print(id_list)
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+
+    # 2. Corrección en la ejecución del ThreadPoolExecutor
+    resultados = []
+    with ThreadPoolExecutor(max_workers=10) as executor:
+        # itertools.repeat repite el header para cada url en id_list
+        resultados = list(executor.map(peticion, id_list, itertools.repeat(headers)))
+
+    return [
+        {
+            "id": int(data["data"]["relationships"]["product"]["data"]["id"]),
+            "quantity": data["data"]["attributes"]["quantity"],
+        } for data in resultados
+    ]
+        
 def get_sale(sale_id:int):
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     token = get_token()
@@ -395,4 +509,4 @@ Configuración de Redis:
 
 if __name__ == "__main__":
     from rich import print
-    print(get_product(1))
+    print(get_table_items(106))

+ 3 - 72
load_products.py

@@ -1,73 +1,4 @@
-#!/home/superti/miniconda3/envs/pedidos_express/bin/python
-import getpass
-
-DB_NAME = "pedidos_express"
-DB_USER = "superti"
-DB_PASSWORD = ""
-DB_HOST = "localhost"
-DB_PORT = 5432
-
-
-
-import psycopg2
-from psycopg2 import sql
-import csv
-
-def load_csv_to_db(csv_file_path):
-    try:
-        # Connect to PostgreSQL server
-        conn = psycopg2.connect(
-            dbname=DB_NAME,
-            user=DB_USER,
-            password=DB_PASSWORD,
-            host=DB_HOST,
-            port=DB_PORT
-        )
-        cursor = conn.cursor()
-        print(f"Connected to database {DB_NAME}")
-        with open(csv_file_path, 'r', encoding='utf-8') as f:
-            reader = csv.reader(f)
-            headers = next(reader)  # Get the header row
-            print(f"CSV Headers: {headers}")
-
-            # Insert data into the table
-            # Define the expected columns in order
-            expected_columns = ['id', 'name', 'type', 'description', 'price', 'image', 'status', 'promo_id', 'promo_price', 'promo_day']
-            
-            insert_query = sql.SQL("""
-                INSERT INTO products ({fields}) VALUES ({placeholders})
-            """).format(
-                fields=sql.SQL(', ').join(map(sql.Identifier, expected_columns)),
-                placeholders=sql.SQL(', ').join(sql.Placeholder() * len(expected_columns))
-            )
-
-            for row in reader:
-                id = int(row[0])
-                name = row[1]
-                type = row[2]
-                description = row[5]
-                price = int(row[6])
-                image = f"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/{id}"
-                status = 1
-                promo_id = None
-                promo_price = None
-                promo_day = None
-                cursor.execute(insert_query, (id, name, type, description, price, image, status, promo_id, promo_price, promo_day))
-            conn.commit()
-            print(f"Data loaded into products successfully.")
-        cursor.close()
-        conn.close()
-    except Exception as e:
-        print(f"Error: {e}")
-
-
+from fudo.fudo import get_all_products, get_product
+from rich import print
 if __name__ == "__main__":
-    import sys
-    import os
-    if len(sys.argv) != 2:
-        print("Usage: python load_products.py <path_to_csv_file>")
-    else:
-        DB_PASSWORD = getpass.getpass(prompt='Enter database password: ')
-        csv_file_path = sys.argv[1]
-        csv_file_path = os.path.abspath(csv_file_path)
-        load_csv_to_db(csv_file_path)
+    print(get_all_products()[:10])

+ 1 - 1
middleware/in_time.py

@@ -13,7 +13,7 @@ from logging import getLogger
 
 logger = getLogger(__name__)
 
-start_time = datetime.time(10, 0, 0)
+start_time = datetime.time(5, 0, 0)
 end_time = datetime.time(23, 30, 0)
 
 class EmptyUser:

+ 6 - 1
models/items.py

@@ -6,6 +6,7 @@ class Item(BaseModel):
     price: float
     quantity: int
     comment: str
+    kitchen_id: int
 
 class Order(BaseModel):
     """Order model matching the database schema"""
@@ -15,7 +16,10 @@ class Order(BaseModel):
     orderDate: str
     table: int
 
-
+class OrderBilling(BaseModel):
+    """Order model matching the database schema"""
+    table: int
+    payment: str
 class Product(BaseModel):
     """Product model matching the database schema"""
     id: int
@@ -26,6 +30,7 @@ class Product(BaseModel):
     image: Optional[str] = None
     status: int = 1  # 0: Inactive, 1: Active
     quantity: Optional[int] = 1  # Optional quantity for the product
+    kitchen_id: Optional[int] = None
     promo_id:Optional[int]  # ID of the promotional offer if applicable
     promo_price:Optional[int]  # Promotional price if applicable
     promo_day:Optional[int]  # Day of the week for promo (1-7)

+ 0 - 101
public/main/animations.css

@@ -1,101 +0,0 @@
-
-    @keyframes slideRight {
-      from {
-        transform: translateX(0%);
-        position:absolute;
-      }
-      to {
-        transform: translateX(100%);
-        position:absolute;
-          }    }
-    @keyframes slideRightIn {
-      from {
-        transform: translateX(100%);
-        position:absolute;
-      }
-      to {
-        transform: translateX(0%);
-        position:absolute;
-      }
-    }
-    @keyframes slideLeft {
-      from {
-        transform: translateX(0%);
-        position:absolute;
-      }
-      to {
-        transform: translateX(-100%);
-        position:absolute;
-          }    }
-    @keyframes slideLeftIn {
-      from {
-        transform: translateX(-100%);
-        position:absolute;
-      }
-      to {
-        transform: translateX(-0%);
-        position:absolute;
-      }
-    }
-    @keyframes popup {
-      0% {
-        transform: scale(0) translateX(-50%);
-      }
-      15% {
-        transform: scale(1) translateX(-50%);
-      }
-      85% {
-        transform: scale(1) translateX(-50%);
-      }
-      100% {
-        transform: scale(0) translateX(-50%);
-      }
-    }
-
-    @keyframes confetti-fall {
-            0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
-            100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
-        }
-        
-        .confetti {
-            position: absolute;
-            width: 10px;
-            height: 10px;
-            animation: confetti-fall 3s linear infinite;
-        }
-        
-        @keyframes pulse-glow {
-            0%, 100% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
-            50% { box-shadow: 0 0 40px rgba(59, 130, 246, 0.8); }
-        }
-        
-        .glow-effect {
-            animation: pulse-glow 2s ease-in-out infinite;
-        }
-
-        @keyframes float {
-            0%, 100% { transform: translateY(0px) rotate(0deg); }
-            50% { transform: translateY(-10px) rotate(5deg); }
-        }
-
-        .float-animation {
-            animation: float 3s ease-in-out infinite;
-        }
-
-        @keyframes sparkle {
-            0%, 100% { opacity: 1; transform: scale(1); }
-            50% { opacity: 0.7; transform: scale(1.2); }
-        }
-
-        .sparkle {
-            animation: sparkle 1.5s ease-in-out infinite;
-        }
-
-        @keyframes bg-loaded {
-            0% { transform: scale(0); filter: blur(10px);}
-            100% { transform: scale(1); filter: blur(0);}
-        }
-
-        .bg-loaded {
-            animation: bg-loaded 200ms ease-in-out;
-        }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 16 - 0
public/main/assets/index-BBKc1DLf.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
public/main/assets/index-yQM4v3U3.css


+ 0 - 0
public/main/assets/no_image.png → public/main/assets/no_image-DuvbomyT.png


BIN
public/main/assets/summer.jpeg


+ 16 - 560
public/main/index.html

@@ -1,563 +1,19 @@
 <!DOCTYPE html>
-<html lang="es">
-<head>
-  <meta charset="UTF-8" />
-  <title>Biergarten Klein</title>
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  
-  <!-- Meta tags para evitar cache -->
-  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
-  <meta http-equiv="Pragma" content="no-cache">
-  <meta http-equiv="Expires" content="0">
-  
-  <!-- Fuentes + Tailwind -->
-  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-  <link rel="stylesheet" as="style" onload="this.rel='stylesheet'"
-        href="https://fonts.googleapis.com/css2?display=swap&family=Noto+Sans:wght@400;500;700;900&family=Spline+Sans:wght@400;500;700">
-  <link rel="stylesheet" href="express/animations.css">
-  <link rel="stylesheet" href="express/styles.css">
-  <link rel="stylesheet" href="express/tlw.css">
-  <script src="express/js/app.js" type="module"></script>
+<html lang="en" class="dark">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Pedidos Express</title>
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@300;400;500;600;700&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
+      rel="stylesheet"
+    />
+  <script type="module" crossorigin src="/assets/index-BBKc1DLf.js"></script>
+  <link rel="stylesheet" crossorigin href="/assets/index-yQM4v3U3.css">
 </head>
-<body class="h-[100dvh] max-h-[100dvh] flex flex-col bg-gray-50 overflow-x-hidden"
-      style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
-
-  <!-- ---------- HEADER ---------- -->
-<header class="flex-col top-0 inset-x-0 z-10 bg-gray-50 p-2 flex justify-center items-center border-b border-gray-200 relative">
-    <h1 id="mainTitle" class="text-[26px] font-bold text-[#101419] tracking-tight">
-      Biergarten Klein
-    </h1>
-    
-    <div class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-3">
-        <span id="headerUserName" class="hidden text-xs font-bold text-gray-600 border-r border-gray-300 pr-3"></span>
-        
-        <button id="openLoginBtn" class="hidden text-sm text-blue-600 font-medium hover:text-blue-800 transition-colors">
-           Entrar
-        </button>
-        
-        <button id="headerLogoutBtn" class="hidden text-sm text-red-500 font-medium hover:text-red-700 transition-colors">
-           Salir
-        </button>
-    </div>
-  </header>
-
-  <!-- ---------- MAIN  ---------- -->
-  <main class="relative flex-1 flex flex-col min-h-0 overflow-x-hidden">  
-    <!-- ===== 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">
-            <!-- Barra de progreso para cerveza gratis -->
-            <div id="rewardContainer" 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">
-                    <div class="flex items-center gap-2">
-                        <span class="text-lg">🍺</span>
-                        <span class="text-sm font-semibold text-amber-800">¡Cerveza gratis al 100%!</span>
-                    </div>
-                    <span id="progressText" class="text-sm font-bold text-amber-700">100%</span>
-                </div>
-                
-                <div class="relative w-full h-3 bg-amber-100 rounded-full overflow-hidden shadow-inner">
-                    <!-- Barra de progreso animada -->
-                    <div id="progressBar" class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden" style="width: 100%">
-                        <!-- Efecto de brillo -->
-                        <div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-12 animate-pulse"></div>
-                    </div>
-                    
-                    <!-- Indicador de meta -->
-                    <div class="absolute right-1 top-1/2 transform -translate-y-1/2">
-                        <div class="w-2 h-2 bg-amber-600 rounded-full border border-white shadow-sm"></div>
-                    </div>
-                </div>
-                
-                <div class="flex items-center justify-between mt-2">
-                    <span class="text-xs text-amber-700">Cada compra suma puntos</span>
-                    <button id="rewardBtn" class="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded-full font-medium transition-colors duration-200 opacity-50 cursor-not-allowed" disabled>
-                        🎉 ¡Reclamar!
-                    </button>
-                </div>
-            </div>
-        </div>
-        
-        <div class="px-4 overflow-y-auto">
-              <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]">
-            <ul id="productList" class="space-y-6">
-            </ul>
-        </div>
-
-        <template id="product-card-template">
-            <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"></p>
-                        <p class="product-name text-[#101419] text-base font-bold leading-tight"></p>
-                        <p class="product-description text-[#58728d] text-sm"></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">
-                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
-                                <path d="M12 5v14m7-7H5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-                            </svg>
-                            Añadir
-                        </button>
-                        <span class="product-price text-sm font-semibold text-[#101419]"></span>
-                    </div>
-                </div>
-                <div class="product-image flex-1 aspect-video bg-cover bg-center rounded-xl"></div>
-            </li>
-        </template>
-    </section>
-
-    <!-- ===== CHAT ===== -->
-    <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col h-full flex-1 min-h-0">
-        <!-- Contenedor de mensajes que puede crecer y hacer scroll -->
-        <div id="chatContainer" class="flex flex-col h-full flex-1 bg-white border border-gray-200 rounded-xl shadow-sm m-4 overflow-hidden">
-          <header>
-            <div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50">
-              <div id="onlineUsers" class="flex items-center gap-3"><span class="rounded-full bg-lime-500 w-2 h-2 inline-block shadow-md shadow-lime-300"></span><h4>4 Usuarios en linea</h4></div>
-            </div>
-          </header>
-          <!-- Mensajes -->
-          <div id="chatMessages" class="flex-1 overflow-y-auto px-4 py-3 space-y-1 bg-gray-50 font-mono text-sm">
-            <!-- Mensajes de ejemplo estilo IRC -->
-            <template id="chatMessageTemplate">
-              <div class="chat-message"><span class="text-gray-400 chat-message-time">[00:36]</span> <span class="font-bold chat-message-user">&lt;JuanP_mesa5&gt;</span> <span class="chat-message-text">Yo también quiero!</span></div>
-            </template>
-            <template id="systemMessageTemplate">
-              <div class="system-container"><span class="text-gray-400 chat-message-time">[14:24]</span> <span class="font-bold chat-message-text">*** Maria_mesa2 se ha unido al chat</span></div>
-            </template>
-          </div>
-          <!-- Input y botón -->
-          <form id="chatForm" class="flex flex-col relative gap-2 border-t border-gray-200 bg-white px-3 py-2" autocomplete="off">
-              <ul id="userList" class="hidden absolute bottom-full w-3/4 rounded-lg border bg-gray-100 flex flex-col gap-2 py-2 px-2">
-            
-              </ul>
-            <div class="flex gap-2 w-full">
-              <input id="chatInput" type="text" placeholder="Escribe un mensaje..." class="flex-grow flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#101419] text-sm bg-gray-50" autocomplete="off" maxlength="300" />
-              <button id="chatSubmitButton" type="submit" class="bg-blue-500 flex-shrink text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200">Enviar</button>
-            </div>
-          </form>
-        </div>
-    </section>
-
-    <!-- ===== CARRITO ===== -->
-    <section id="cartTab" data-tab data-index="1" class="flex flex-col hidden flex-1 min-h-0">
-      <header class="p-4 border-b border-gray-200">
-        <h3 class="text-lg font-bold text-[#101419]">Tu pedido</h3>
-      </header>
-      
-      <div id="cartItems" class="flex-1 overflow-y-auto p-4 space-y-2 max-h-[25vh]"></div>
-      <p id="emptyCartText" class="hidden text-center text-gray-400 mt-4">Tu carrito está vacío.</p>
-
-      <footer class="p-4 border-t border-gray-200 space-y-3">
-        <div class="flex justify-between text-base">
-          <span>Total:</span>
-          <span id="cartTotal">$0</span>
-        </div>
-        <button id="checkoutButton"
-                class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 text-white py-2 rounded-md"
-                disabled>
-          Envia tu orden
-        </button>
-      </footer>
-
-      <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
-    <!-- Header con nombre de usuario -->
-    <div class="bg-gray-50 px-6 py-2 border-b border-gray-200">
-      <h2 id="usernameTable" class="text-[19px] font-bold text-[#101419]"></h2>
-    </div>
-
-    <div class="overflow-x-auto max-h-[20vh]">
-      <table id="historyTable" class="w-full">
-        <thead class="bg-gray-50 border-b border-gray-200">
-          <tr>
-            <th class="px-6 py-3 text-left text-sm font-bold text-[#101419]">Cantidad</th>
-            <th class="px-6 py-3 text-left text-sm font-bold text-[#101419]">Producto</th>
-            <th class="px-6 py-3 text-right text-sm font-bold text-[#101419]">Precio</th>
-          </tr>
-        </thead>
-        <tbody class="divide-y divide-gray-200">
-          <template id="historyRowTemplate">
-            <tr class="hover:bg-gray-50 transition-colors">
-              <td class="list-element-quantity px-6 py-4 text-sm text-[#58728d]">1</td>
-              <td class="px-6 py-4">
-                <div class="flex flex-col">
-                  <span class="list-element-name text-sm font-medium text-[#101419]">Klein Lager</span>
-                </div>
-              </td>
-              <td class="list-element-price px-6 py-4 text-sm font-semibold text-[#101419] text-right">$6.000</td>
-            </tr>
-          </template>
-        </tbody>
-        
-        <!-- Total -->
-        <tfoot class="bg-gray-50 border-t border-gray-200">
-          <tr>
-            <td colspan="2" class="px-6 py-4 text-base font-bold text-[#101419] text-right">
-              Total General:
-            </td>
-            <td id="cartHistoryTotal" class="px-6 py-4 text-lg font-bold text-[#101419] text-right">
-              $0
-            </td>
-          </tr>
-        </tfoot>
-      </table>
-    </div>
-  </div>
-    </section>
-  </main>
-
-  <!-- ---------- NAVBAR ---------- -->
-  <footer class="inset-x-0 z-10 border-t border-gray-200 bg-gray-50 px-4 py-2">
-    <nav class="flex gap-2">
-      <button data-target="menuTab" data-title="Biergarten Klein" class="active tab-btn flex-1 flex flex-col items-center text-[#58728d]">
-         <svg  xmlns="http://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="h-8"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l-2 0l9 -9l9 9l-2 0" /><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></svg><span class="text-xs font-medium">Inicio</span>
-      </button>
-      
-      <button data-target="cartTab" data-title="Carrito Klein" class="tab-btn flex-1 flex flex-col items-center text-[#58728d]">
-        <div id="cartIcon">
-          <span id="cartCount">0</span>
-          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
-               viewBox="0 0 256 256" class="h-8">
-            <path
-              d="M222.14,58.87A8,8,0,0,0,216,56H54.68L49.79,29.14A16,16,0,0,0,34.05,16H16a8,8,0,0,0,0,16h18L59.56,172.29a24,24,0,0,0,5.33,11.27,28,28,0,1,0,44.4,8.44h45.42A27.75,27.75,0,0,0,152,204a28,28,0,1,0,28-28H83.17a8,8,0,0,1-7.87-6.57L72.13,152h116a24,24,0,0,0,23.61-19.71l12.16-66.86A8,8,0,0,0,222.14,58.87ZM96,204a12,12,0,1,1-12-12A12,12,0,0,1,96,204Zm96,0a12,12,0,1,1-12-12A12,12,0,0,1,192,204Zm4-74.57A8,8,0,0,1,188.1,136H69.22L57.59,72H206.41Z" />
-          </svg>
-        </div>
-        <span class="text-xs font-medium">Carrito</span>
-      </button>
-      <button data-target="chatTab" data-title="RetroChat" class="tab-btn flex-1 flex flex-col items-center text-[#58728d]">
-        <div id="chatIcon">
-          <span class="hidden"></span>
-          <svg  xmlns="http://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="h-8"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" /></svg>
-          
-        </div><span class="text-xs font-medium">Chat IA</span>
-      </button>
-            
-    </nav>
-  </footer>
-
-  <!-- ---------- TOAST ---------- -->
-  <div id="toastCart"
-       class="fixed top-4 left-1/2 -translate-x-1/2 bg-[#101419] text-white text-sm
-              rounded-md px-4 py-2 shadow-lg opacity-0 pointer-events-none z-50
-              origin-left">
-  </div>
-
-  <!-- MODALS -->
-  <!-- === MODAL INICIO DE SESIÓN === -->
-<div id="sessionModal"
-     class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-40 p-4">
-  <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
-    <div class="text-center">
-      <h2 id="loginTitle" class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
-      <p id="loginMessage" class="text-sm text-gray-600 mt-2">
-        Ingresa tus datos para comenzar tu pedido
-      </p>
-    </div>
-
-    <div class="space-y-4">
-      <div id="emailInputContainer">
-        <label for="emailInput" class="block text-sm font-medium text-gray-700 mb-2">
-          Correo electrónico
-        </label>
-        <input id="emailInput"
-               name="email"
-               type="email"
-               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
-               placeholder="tu@email.com" 
-               required />
-      </div>
-
-      <div id="pinInputContainer">
-        <label for="pinInput" class="block text-sm font-medium text-gray-700 mb-2">
-          PIN de 4 dígitos
-        </label>
-        <input id="pinInput"
-               name="pin"
-               type="password"
-               maxlength="4"
-               pattern="[0-9]{4}"
-               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
-               placeholder="••••" 
-               required />
-      </div>
-
-      <div id="tableInputContainer">
-        <label for="tableInput" class="block text-sm font-medium text-gray-700 mb-2">
-          Número de mesa
-        </label>
-        <input id="tableInput"
-               name="table"
-               type="number" 
-               min="1"
-               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
-               placeholder="Ej: 5" 
-               required />
-      </div>
-    </div>
-    <span id="ErrorLogin" class="hidden text-red-500 text-sm"></span>
-    <a id="recoveryPIN" href="/recovery" class="block w-full bg-gray-200 text-center text-black py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]" >Olvidé mi PIN</a>
-    <button id="sessionAcceptBtn"
-            type="submit"
-            class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]">
-      Comenzar pedido
-    </button>
-    <div id="registerBtn"  class="mt-4 text-center text-sm text-gray-600">
-  ¿No tienes cuenta? 
-  <a href="/register" class="font-semibold text-[#101419] hover:underline transition-colors">
-    Regístrate aquí
-  </a>
-</div>
-    <button id="logoutBtn"
-            type="button"
-            class="w-full hidden bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-gray-300">
-      Cerrar sesión
-
-    </button>
-  </form>
-</div>
-
-<!-- Modal de Términos y Condiciones del Regalo -->
-<div id="rewardModal" class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
-  <div class="bg-white w-full max-w-lg max-h-[90vh] rounded-xl shadow-xl overflow-hidden">
-    <!-- Header -->
-    <div class="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4 text-white">
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-3">
-          <span class="text-2xl">🍺</span>
-          <h2 class="text-xl font-bold">¡Cerveza Gratis!</h2>
-        </div>
-        <button id="closeRewardModal" class="text-white/80 hover:text-white text-2xl font-bold transition-colors">
-          ×
-        </button>
-      </div>
-    </div>
-
-    <!-- Content -->
-    <div class="overflow-y-auto max-h-[60vh] px-6 py-4">
-      <div class="space-y-4">
-        <!-- Descripción del premio -->
-        <div class="bg-amber-50 border border-amber-200 rounded-lg p-4">
-          <h3 class="font-semibold text-amber-800 mb-2">🎉 ¡Felicitaciones!</h3>
-          <p class="text-amber-700 text-sm">
-            Has alcanzado el 100% de progreso y puedes reclamar una cerveza gratis de tu elección.
-          </p>
-        </div>
-
-        <!-- Términos y Condiciones -->
-        <div class="space-y-3">
-          <h4 class="font-bold text-gray-800 text-base">Términos y Condiciones:</h4>
-          
-          <div class="text-sm text-gray-600 space-y-2">
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>Válido únicamente en Biergarten Klein para consumo en el local.</p>
-            </div>
-            
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>Aplica para cervezas de barril regulares (no incluye cervezas premium o importadas).</p>
-            </div>
-            
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>No acumulable con otras promociones o descuentos.</p>
-            </div>
-            
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>El premio no tiene valor en efectivo y no es transferible.</p>
-            </div>
-            
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>Biergarten Klein se reserva el derecho de modificar o cancelar esta promoción sin previo aviso.</p>
-            </div>
-            
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>Debes ser mayor de 18 años para reclamar bebidas alcohólicas.</p>
-            </div>
-          </div>
-        </div>
-
-        <!-- Instrucciones -->
-        <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
-          <h4 class="font-semibold text-blue-800 mb-2">📋 Instrucciones:</h4>
-          <p class="text-blue-700 text-sm">
-            Una vez aceptes los términos, el garzon vendra a entregarte tu cerveza gratis.
-          </p>
-        </div>
-      </div>
-    </div>
-
-    <!-- Footer con checkbox y botones -->
-    <div class="border-t border-gray-200 px-6 py-4 space-y-4">
-      <!-- Checkbox de aceptación -->
-      <label class="flex items-start gap-3 cursor-pointer">
-        <input id="acceptTermsCheckbox" type="checkbox" class="mt-1 w-4 h-4 text-amber-600 bg-gray-100 border-gray-300 rounded focus:ring-amber-500 focus:ring-2">
-        <span class="text-sm text-gray-700">
-          He leído y acepto los términos y condiciones para reclamar mi cerveza gratis.
-        </span>
-      </label>
-
-      <!-- Botones -->
-      <div class="flex gap-3">
-        <button id="cancelRewardBtn" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200">
-          Cancelar
-        </button>
-        <button id="claimRewardBtn" class="flex-1 bg-amber-500 hover:bg-amber-600 text-white py-3 rounded-lg font-medium transition-colors duration-200 opacity-50 cursor-not-allowed" disabled>
-          🎉 Reclamar Premio
-        </button>
-      </div>
-    </div>
-  </div>
-</div>
-<div id="successRewardModal" class="fixed hidden inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
-   <div class="bg-white w-full max-w-md rounded-xl shadow-xl overflow-hidden">
-       <!-- Header -->
-       <div class="bg-[#101419] px-6 py-8 text-white text-center relative">
-           <!-- Elementos decorativos sutiles -->
-           <div class="absolute top-3 left-4 text-amber-300 text-lg">✨</div>
-           <div class="absolute top-4 right-6 text-amber-300 text-sm">🎉</div>
-           <div class="absolute bottom-3 right-4 text-amber-300 text-sm">💫</div>
-           
-           <!-- Icono principal -->
-           <div class="text-6xl mb-3">🍺</div>
-           
-           <!-- Título -->
-           <h2 class="text-2xl font-bold mb-2">¡FELICIDADES!</h2>
-           <div class="bg-white/20 rounded-full px-3 py-1 inline-block">
-               <p class="text-gray-100 text-sm font-medium">🎁 Premio Reclamado</p>
-           </div>
-       </div>
-
-       <!-- Content -->
-       <div class="px-6 py-6 space-y-4">
-           <!-- Mensaje de bienvenida -->
-           <div class="text-center">
-               <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
-                   <h3 class="text-lg font-bold text-[#101419] mb-1">🍻 ¡Tu cerveza gratis te espera!</h3>
-                   <p class="text-[#58728d] text-sm">Sigue estos pasos para reclamar tu premio</p>
-               </div>
-           </div>
-
-           <!-- Instrucciones -->
-           <div class="space-y-3">
-               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
-                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">1</div>
-                   <div class="text-sm">
-                       <div class="font-bold text-[#101419] mb-1">👨‍💼 Espera al mesero</div>
-                       <div class="text-[#58728d]">Este se acercará con tu comprobante de premio</div>
-                   </div>
-               </div>
-               
-               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
-                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">2</div>
-                   <div class="text-sm">
-                       <div class="font-bold text-[#101419] mb-1">🍺 Elige tu cerveza favorita</div>
-                       <div class="text-[#58728d]">Selecciona cualquier cerveza hasta <span class="font-semibold">$5,000</span></div>
-                   </div>
-               </div>
-               
-               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
-                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">3</div>
-                   <div class="text-sm">
-                       <div class="font-bold text-[#101419] mb-1">🎉 ¡Disfruta al máximo!</div>
-                       <div class="text-[#58728d]">Tu cerveza gratis está lista para disfrutar 🥳</div>
-                   </div>
-               </div>
-           </div>
-
-           <!-- Nota importante -->
-           <div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
-               <div class="flex items-center gap-2 mb-1">
-                   <span class="text-base">⏰</span>
-                   <h4 class="font-semibold text-amber-800 text-sm">Importante</h4>
-               </div>
-               <p class="text-amber-700 text-sm">Este premio es válido solo para el día de hoy. ¡No pierdas esta oportunidad!</p>
-           </div>
-       </div>
-
-       <!-- Footer -->
-       <div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
-           <button id="closeSuccessRewardModal" class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200">
-               <span class="flex items-center justify-center gap-2">
-                   <span>🎊</span>
-                   Aceptar
-                   <span>🍻</span>
-               </span>
-           </button>
-       </div>
-   </div>
-</div>
-<div id="commentModal" class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
-
-  <div class="bg-white w-full max-w-md rounded-xl shadow-xl overflow-hidden">
-    
-    <div class="hidden bg-gray-50 px-6 py-4 border-b border-gray-200">
-      <div class="flex items-center justify-between">
-        <h2 id="titleComment" class="text-xl font-bold text-[#101419]">Titulo</h2>
-        <button id="closeCommentModal" type="button" class="text-gray-400 hover:text-gray-600 text-2xl font-bold transition-colors">
-          &times;
-        </button>
-      </div>
-    </div>
-    <!-- 2 posibles modals: un campo con textarea y boton de enviar o un campo desplegable con opciones -->
-    <form id="selectCommentTypeForm" class="hidden">
-      <div class="p-6 space-y-4">
-        <p class="text-sm text-[#58728d]">
-          Elige un sabor
-        </p>
-        <div id="selectOptions" class="flex flex-col gap-2">
-            
-        </div>
-      </div>
-    </form>
-    <form id="commentForm" class="hidden">
-      <div class="p-6 space-y-4">
-        <p class="text-sm text-[#58728d]">
-          ¿Alguna instrucción para la cocina? (Ej: sin cebolla, muy picante, alergias, etc.)
-        </p>
-        <div>
-          <label for="commentTextarea" class="hidden text-sm font-medium text-gray-700 mb-2">
-            Tu comentario
-          </label>
-          <textarea id="commentTextarea"
-                    name="comment"
-                    rows="4"
-                    class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
-                    placeholder="Escribe tu comentario aquí..."></textarea>
-        </div>
-      </div>
-
-      <div class="border-t border-gray-200 bg-gray-50 px-6 py-4 flex gap-3">
-        <button id="cancelCommentBtn" type="button" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200">
-          Cancelar
-        </button>
-        <button id="submitCommentBtn" type="submit" class="flex-1 bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200">
-          Guardar
-        </button>
-      </div>
-    </form>
-
-  </div>
-</div>
-
-              <!-- ---------- JS: conmutar tabs + toast ---------- -->
-  <script>
-    // toast simple
-    window.showToast = (msg) => {
-      const toast = document.getElementById('toastCart');
-      toast.textContent = msg;
-      toast.style.animation = 'none';      // reset
-      void toast.offsetWidth;              // reflow
-      toast.style.opacity = '1';
-      toast.style.animation = 'popup 3s ease-out';
-      toast.addEventListener('animationend', () => {
-        toast.style.animation = 'none';
-        toast.style.opacity = '0';
-      });
-    };
-  </script>
+  <body>
+    <div id="root"></div>
 </body>
-</html>
+</html>

+ 0 - 1015
public/main/js/app.js

@@ -1,1015 +0,0 @@
-// derivar para fudo
-window.location.replace("https://menu.fu.do/klein/qr-menu");
-
-
-//--- Imports ---
-import { getOnlineUserCount, getUserList } from './service/chat.js';
-import { getProducts, sendOrder } from './service/product.js';
-import { login, existsTable, guestLogin, getTableFromUrl } from './service/auth.js';
-import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
-import { updateProgress, claimReward, getProgress } 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';
-import { getComment, COMMENT_TYPES } from './utils/get_comment.js';
-import { imageObserver } from './utils/observer.js';
-
-// --- Variables Globales ---
-
-// --- User Data ---
-let userId = -1;
-let userName = "Invitado";
-let chatUserName = "guest";
-let userTable = null;
-let userToken = null;
-let chatWebsocket = null;
-let isGuest = false;
-
-// --- Products & Cart ---
-let Allproducts = [];
-let cart = [];
-let itsEmpty = true;
-let cacheMode = false;
-
-// --- Configuration ---
-const favoriteCategories = ["Shop", "Pizzas Familiares", "Pizza Medianas"];
-const productPriority = {
-    "Shop": [6, 12, 163, 168, 15, 665, 1],
-    "Cervezas": [17],
-    "Coctelería Gin Klein": [655],
-    "Pizzas Familiares": [27],
-    "Pizza Medianas": [2]
-}
-
-const userColors = [
-  "text-red-500", "text-red-600", "text-rose-500", "text-rose-600",
-  "text-orange-500", "text-orange-600", "text-amber-500", "text-amber-600",
-  "text-yellow-500", "text-yellow-600", "text-lime-500", "text-lime-600",
-  "text-green-500", "text-green-600", "text-emerald-500", "text-emerald-600",
-  "text-teal-500", "text-teal-600", "text-cyan-500", "text-cyan-600",
-  "text-sky-500", "text-sky-600", "text-blue-500", "text-blue-600",
-  "text-indigo-500", "text-indigo-600",
-  "text-violet-500", "text-violet-600", "text-purple-500", "text-purple-600",
-  "text-fuchsia-500", "text-fuchsia-600", "text-pink-500", "text-pink-600",
-  "text-gray-500", "text-gray-600", "text-zinc-500", "text-zinc-600",
-  "text-neutral-500", "text-neutral-600", "text-stone-500", "text-stone-600"
-];
-const pulpas = ["Frambuesa", "Mango", "Maracuyá", "Piña", "Tradicional"]
-const bebidasPisco = ["Coca Normal.","Coca Zero","Pepsi","Pepsi 0","Ginger Ale zero","Ginger Ale","Sprite","Tonica Canada dry"]
-const bebidasGin = ["Tonica Pomelo", "Ginger Beer", "Coca Zero","Pepsi","Pepsi 0","Ginger Ale zero","Ginger Ale","Sprite","Tonica Canada dry"]
-const bebidasWhisky = ["Tonica Canada dry", "Coca Cola", "Coca Cola Zero", "Pepsi", "Pepsi Zero", "Sprite", "Ginger Ale", "Ginger Ale Zero"]
-const bebidasRon = ["Coca Cola", "Coca Cola Zero", "Pepsi", "Pepsi Zero", "Sprite", "Ginger Ale", "Ginger Ale Zero"]
-
-const productsWithVariety = {
-    "480": pulpas,
-    "79": bebidasPisco,
-    "80": bebidasPisco,
-    "81": bebidasPisco,
-    "84": bebidasPisco,
-    "85": bebidasPisco,
-    "360": bebidasPisco,
-    "378": bebidasPisco,
-    "379": bebidasPisco,
-    "957": bebidasPisco,
-    "912": bebidasPisco,
-    "612": bebidasGin,
-    "697": bebidasGin,
-    "92": bebidasGin,
-    "93": bebidasGin,
-    "106": bebidasWhisky,
-    "108": bebidasWhisky,
-    "363": bebidasWhisky,
-    "109": bebidasRon,
-    "110": bebidasRon,
-    "111": bebidasRon,
-}
-
-//--- Elementos del DOM ---
-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");
-
-// --- Chat Elements ---
-const chatMessagesElement = document.getElementById("chatMessages");
-const chatInputElement = document.getElementById("chatInput");
-const chatForm = document.getElementById("chatForm");
-const userList = document.getElementById("userList")
-const onlineUsersElement = document.querySelector("#onlineUsers h4");
-
-// --- Reward Elements ---
-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');
-const rewardContainer = document.getElementById('rewardContainer'); // ID NECESARIO EN HTML
-
-//--- Inicialización y Configuración ---
-
-async function initializeApp() {
-    createGlobalLoader();
-    showGlobalLoader("Conectando...");
-    
-    // 1. Obtener Mesa
-    const urlTable = getTableFromUrl();
-    console.log(urlTable);
-    if (!urlTable) {
-        showError("Mesa no especificada en URL.");
-        console.warn("Mesa no especificada en URL.");
-        hideGlobalLoader();
-        return; 
-    }
-    userTable = parseInt(urlTable);
-    if (userTable < 0 || userTable > 1000) {
-        alert("Mesa no válida.");
-        console.warn("Mesa no válida.");
-        window.location.replace("/");
-        return;
-    }
-
-    // 2. Verificar Cache
-    const isCacheValid = await checkCache();
-    
-    if (!isCacheValid) {
-        // 3. Guest Mode
-        try {
-            const guestData = await guestLogin();
-            userToken = guestData.token;
-            isGuest = true;
-            userName = "Invitado";
-            userId = 0; // ID temporal para guest
-            
-            // UI Guest
-            if (typeof showToast === "function") {
-                setTimeout(() => showToast("Modo invitado. Inicia sesión para más funciones."), 1000);
-                setTimeout(() => showToast(`Mesa: ${userTable}`), 2000);
-            }
-        } catch (error) {
-            showError("Error crítico de conexión.");
-            hideGlobalLoader();
-            return;
-        }
-    }
-
-    // Configuración UI según estado
-    updateUIForUserType();
-
-    // Inicializar Componentes
-    if (!isGuest) {
-        chatUserName = userName.split(" ")[0].toLowerCase() + "_mesa" + userTable;
-        initializeChat();
-        initializeWebSocket();
-    }
-    
-    await initializeProducts();
-    await renderProducts(Allproducts);
-    setupSearchListener();
-    setupTabSwitching();
-    updateCartDisplay();
-    
-    // Solo cargar historial si es usuario registrado
-    if (!isGuest) {
-        setupShoppingCart(userId, userToken, userName);
-    }
-    
-    setupBasicListeners();
-    initializeLoginModal();
-    showGUI();
-    hideGlobalLoader();
-}
-
-function updateUIForUserType() {
-    const loginBtn = document.getElementById('openLoginBtn'); 
-    const logoutBtn = document.getElementById('headerLogoutBtn'); // Nuevo botón header
-    const userNameDisplay = document.getElementById('headerUserName'); // Span del nombre
-    const historyTable = document.getElementById('historyTable'); // Tablero de historial
-    
-    if (isGuest) {
-        // UI GUEST
-        if (rewardContainer) rewardContainer.classList.add("hidden");
-        
-        if (loginBtn) loginBtn.classList.remove("hidden");
-        if (logoutBtn) logoutBtn.classList.add("hidden");
-        if (userNameDisplay) userNameDisplay.classList.add("hidden");
-        if (historyTable) historyTable.parentElement.parentElement.classList.add("hidden");
-        
-    } else {
-        // UI LOGUEADO
-        if (rewardContainer) rewardContainer.classList.remove("hidden");
-        
-        
-        if (loginBtn) loginBtn.classList.add("hidden");
-        if (logoutBtn) logoutBtn.classList.remove("hidden");
-        
-        if (userNameDisplay) {
-            // Mostramos solo el primer nombre para ahorrar espacio
-            userNameDisplay.textContent = userName.split(" ")[0]; 
-            userNameDisplay.classList.remove("hidden");
-        }
-    }
-}
-
-function initializeLoginModal() {
-    const sessionModal = document.getElementById('sessionModal');
-    const loginForm = document.getElementById('loginForm');
-    // const logoutBtn = document.getElementById('logoutBtn'); // Ya no usamos el del modal
-    const openLoginBtn = document.getElementById('openLoginBtn'); 
-    const headerLogoutBtn = document.getElementById('headerLogoutBtn'); // Nuevo botón header
-
-    // 1. Abrir modal (Guest intenta loguearse)
-    if (openLoginBtn) {
-        openLoginBtn.addEventListener('click', () => {
-            document.querySelector("#emailInputContainer").classList.remove("hidden");
-            document.querySelector("#pinInputContainer").classList.remove("hidden");
-            
-            const tableInput = document.getElementById("tableInput");
-            if(tableInput) {
-                tableInput.value = userTable;
-                tableInput.readOnly = true;
-            }
-            
-            // Ocultamos logout del modal si existiera, limpiamos UI
-            const modalLogout = document.getElementById('logoutBtn');
-            if(modalLogout) modalLogout.classList.add("hidden");
-            
-            document.querySelector("#loginTitle").textContent = "¡Bienvenido!";
-            sessionModal.classList.remove('hidden');
-        });
-    }
-
-    // 2. Funcionalidad Cerrar Sesión (Desde el Header)
-    if (headerLogoutBtn) {
-        headerLogoutBtn.addEventListener('click', () => {
-            if(confirm("¿Estás seguro que deseas cerrar sesión?")) {
-                setCookie("userToken", "", -1);
-                window.location.reload();
-            }
-        });
-    }
-
-    // 3. Submit del Login (Igual que antes)
-    loginForm.addEventListener('submit', async (event) => {
-        event.preventDefault();
-        showGlobalLoader("Iniciando sesión...");
-        const fd = new FormData(loginForm);
-        const email = fd.get('email').trim();
-        const pin = fd.get('pin').trim();
-
-        if (!email || !pin) {
-            showError("Completa todos los campos.");
-            hideGlobalLoader();
-            return;
-        }
-        try {
-            const data = await login(email, pin, userTable);
-            
-            userToken = data.token;
-            userName = data.name;
-            userId = data.id;
-            isGuest = false;
-            
-            setCookie("userToken", userToken, 7);
-            updateProgress(data.reward_progress || 0);
-
-            sessionModal.classList.add('hidden');
-            initializeApp(); 
-            
-        } catch (error) {
-            console.error(error);
-            hideGlobalLoader();
-        }
-    });
-
-    // Cerrar modal click afuera
-    sessionModal.addEventListener('click', (e) => {
-        if (e.target === sessionModal && (isGuest || userId !== -1)) {
-            sessionModal.classList.add('hidden');
-        }
-    });
-}
-
-// ... (Resto de funciones WebSocket y Chat igual, solo se ejecutan si !isGuest) ...
-
-function initializeWebSocket() {
-    if (isGuest) return; // Seguridad extra
-    if (!chatWebsocket) {
-        const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
-        chatWebsocket = new WebSocket(`${wsProtocol}://${location.host}/api/chat/ws?token=${userToken}`);
-        chatWebsocket.onopen = () => {
-            chatWebsocket.send(JSON.stringify({
-                type: "join",
-                username: chatUserName
-            }));
-        }
-        getOnlineUserCount(userToken).then(count => {
-            onlineUsersElement.innerText = `${count} Usuario${count !== 1 ? 's' : ''} en línea`;
-        });
-        chatWebsocket.onmessage = (event) => {
-            const data = JSON.parse(event.data);
-            if (data.type === "message") displayChatMessage(data.username, data.message);
-            else if (data.type === "join") newUserInChat(data.username);
-            else if (data.type === "leave") userLeftChat(data.username);
-            else if (data.type === "mentioned") {
-                if (data.username && data.username !== chatUserName) return;
-                showMentioned();
-            }
-            else if (data.type === "ping") {
-                chatWebsocket.send(JSON.stringify({ type: "pong" }));
-            }
-        }
-    }
-}
-
-function initializeChat() {
-    if (isGuest || !chatForm) return;
-    // ... (Código original de initializeChat) ...
-      chatForm.addEventListener("submit", (event) => {
-        event.preventDefault();
-        if (chatInputElement.value.trim() === "") return;
-
-        chatForm.querySelector("button").animate(
-        [{ transform: 'scale(1)' }, { transform: 'scale(0.9)' }, { transform: 'scale(1)' }],
-        { duration: 200, iterations: 1, easing: 'ease-in-out' }
-    )
-        sendMessage(chatInputElement.value.trim());
-        chatInputElement.value = "";
-    });
-    
-    let debounceTimer;
-    chatInputElement.addEventListener("input", () => {
-        const lastWord = chatInputElement.value.split(" ").at(-1);
-        if (lastWord.trim().startsWith("@")) {
-            clearTimeout(debounceTimer);
-            debounceTimer = setTimeout(async () => {
-                userList.classList.remove("hidden");
-                let users = await getUserList(lastWord.trim().substring(1), userToken);
-                users = users.filter(u => u !== chatUserName);
-                users.push("IAKlein");
-                userList.innerHTML = "";
-                users.forEach(user => {
-                    const userItem = document.createElement("button");
-                    userItem.type = "button";
-                    userItem.onclick = () => {
-                        const inputValue = chatInputElement.value;
-                        const lastWordIndex = inputValue.lastIndexOf(lastWord);
-                        if (lastWordIndex !== -1) {
-                            chatInputElement.value = inputValue.slice(0, lastWordIndex) + `@${user}` + inputValue.slice(lastWordIndex + lastWord.length);
-                        }
-                        userList.classList.add("hidden");
-                        chatInputElement.value += " ";
-                        chatInputElement.focus();
-                    };
-                    const color = getUserColor(user);
-                    userItem.classList.add("list-user-name", "cursor-pointer", color);
-                    userItem.textContent = user;
-                    userList.appendChild(userItem);
-                });
-            }, 300); 
-        } else {
-            userList.classList.add("hidden");
-            clearTimeout(debounceTimer);
-        }
-    });
-}
-// ... (Funciones sendMessage, userMentioned, displayChatMessage, etc. iguales) ...
-async function sendMessage(message) {
-    if (chatWebsocket) {
-        if (chatWebsocket.readyState !== WebSocket.OPEN) {
-            chatWebsocket = null;
-            initializeWebSocket();
-        }
-        await chatWebsocket.send(JSON.stringify({
-            type: "message",
-            username: chatUserName,
-            message: message
-        }));
-        let mentions_repeated = [];
-        await setTimeout(() => {
-        const mentionRegex = /@([a-zA-Z0-9_]+_mesa\d+|IAKlein)/g;
-        const mentions = message.match(mentionRegex);
-        if (mentions) {
-            mentions.forEach(mention => {
-                const username = mention.slice(1);
-                const exist = mentions_repeated.find(u => u === username);
-                if (exist || username === chatUserName) return; 
-                mentions_repeated.push(username);
-                if (username === "IAKlein"){
-                    sendMessageToAI(message);
-                }else {
-                    userMentioned(username);
-                }
-            });
-        }
-        }, 1000);
-    }
-}
-
-function userMentioned(username) {
-    chatWebsocket.send(JSON.stringify({
-        type: "mention",
-        username: username
-    }));
-}
-function showMentioned(){
-    const chatButton = document.querySelector("#chatIcon span");
-    if (chatButton) {
-        chatButton.classList.remove("hidden");
-        chatButton.animate(
-            [{ transform: 'scale(1)' }, { transform: 'scale(1.5)' }, { transform: 'scale(1)' }],
-            { duration: 300, iterations: 3, easing: 'ease-in-out' }
-        );
-    }
-}
-window.hideMentioned = function(){
-    const chatButton = document.querySelector("#chatIcon span");
-    if (chatButton) chatButton.classList.add("hidden");
-}
-function getUserColor(username) {
-  const hash = [...username].reduce((acc, char) => acc + char.charCodeAt(0), 0);
-  return userColors[hash % userColors.length];
-}
-
-function displayChatMessage(sender, message, time = undefined) {
-    let realtime = `[${time ? time : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}] `;
-    let messageTemplate = chatMessagesElement.querySelector("#chatMessageTemplate");
-    if (!messageTemplate) return;
-
-    let messageClone = messageTemplate.content.cloneNode(true).firstElementChild;
-    messageClone.querySelector(".chat-message-text").innerHTML = message;
-    messageClone.querySelector(".chat-message-time").innerHTML = realtime;
-    messageClone.querySelector(".chat-message-user").innerHTML = sender
-    messageClone.querySelector(".chat-message-user").classList.add(getUserColor(sender));
-    chatMessagesElement.appendChild(messageClone);
-    chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
-}
-
-function newUserInChat(userName) {
-    let userTemplate = chatMessagesElement.querySelector("#systemMessageTemplate");
-    if (!userTemplate) return;
-    const realtime = `[${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}] `;
-    let userClone = userTemplate.content.cloneNode(true).firstElementChild;
-    userClone.classList.add("text-green-600");
-    userClone.querySelector(".chat-message-time").innerHTML = realtime;
-    userClone.querySelector(".chat-message-text").innerHTML = `*** ${userName} se ha unido al chat`;
-    chatMessagesElement.appendChild(userClone);
-}
-
-function userLeftChat(userName) {
-    let userTemplate = chatMessagesElement.querySelector("#systemMessageTemplate");
-    if (!userTemplate) return;
-    const realtime = `[${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}] `;
-    let userClone = userTemplate.content.cloneNode(true).firstElementChild;
-    userClone.classList.add("text-red-600");
-    userClone.querySelector(".chat-message-time").innerHTML = realtime;
-    userClone.querySelector(".chat-message-text").innerHTML = `*** ${userName} se ha ido del chat`;
-    chatMessagesElement.appendChild(userClone);
-}
-
-function sendMessageToAI(userMessage) {
-    if (userMessage === "") return;
-    const messages = Array.from(chatMessagesElement.querySelectorAll(".chat-message")).slice(-10);
-    let formattedMessages = messages.map(msg => {
-        const sender = msg.querySelector(".chat-message-user").textContent;
-        const content = msg.querySelector(".chat-message-text").innerHTML;
-        return { user: sender, content: content };
-    });
-    chatWebsocket.send(JSON.stringify({
-        type: "ai_message",
-        username: chatUserName,
-        messages: formattedMessages
-    }));
-    chatInputElement.value = "";
-}
-
-//--- Manejo de Pestañas ---
-
-window.switchTab = function(targetTabId) {
-    // BLOQUEO CHAT PARA GUEST
-    if (targetTabId === 'chatTab' && isGuest) {
-        if (typeof showToast === "function") showToast("Inicia sesión para usar el chat");
-        // Opcional: abrir modal login
-        const openLoginBtn = document.getElementById('openLoginBtn');
-        if(openLoginBtn) openLoginBtn.click();
-        return;
-    }
-
-    const targetButton = document.querySelector(`[data-target="${targetTabId}"]`);
-    if (!targetButton) return;
-    
-    if (targetTabId === "chatTab") window.hideMentioned();
-    
-    const active = document.querySelector(':not(.hidden)[data-tab]');
-    if (!active) return;
-    
-    const activeIndex = active.dataset.index;
-    const to = document.querySelector(`#${targetTabId}[data-tab]`);
-    if (!to) return;
-    
-    const toIndex = to.dataset.index;
-    if (activeIndex === toIndex || transitioning) return;
-    
-    const buttons = document.querySelectorAll('.tab-btn');
-    buttons.forEach(button => button.classList.remove('active'));
-    targetButton.classList.add('active');
-    
-    performTabTransition(active, to, activeIndex, toIndex, targetButton);
-};
-
-// ... (setupTabSwitching, performTabTransition, setCookie, getCookie sin cambios) ...
-
-// Tab switching variables
-const animation_time = 200;
-let transitioning = false;
-
-function performTabTransition(active, to, activeIndex, toIndex, targetButton) {
-    active.style.height = "100%";
-    active.style.width = "100vw";
-    to.style.height = "100%";
-    to.style.width = "100vw";
-    to.style.zIndex = "1";
-    active.style.zIndex = "0";
-    transitioning = true;
-    
-    const otherTabs = document.querySelectorAll('[data-tab]');
-    otherTabs.forEach(tab => {
-        if (tab !== active && tab !== to) {
-            tab.classList.add('hidden');
-            tab.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
-            tab.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
-        }
-    });
-    
-    to.classList.remove('hidden');
-    
-    if (activeIndex < toIndex) {
-        active.classList.add(`animate-[slideLeft_${animation_time}ms_ease-out]`);
-        to.classList.add(`animate-[slideRightIn_${animation_time}ms_ease-out]`);
-    } else if (activeIndex > toIndex) {
-        active.classList.add(`animate-[slideRight_${animation_time}ms_ease-out]`);
-        to.classList.add(`animate-[slideLeftIn_${animation_time}ms_ease-out]`);
-    }
-
-    setTimeout(() => {
-        active.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
-        active.classList.add('hidden');
-        to.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
-        transitioning = false;
-    }, animation_time);
-
-    const title = targetButton.dataset.title;
-    if (title) {
-        const mainTitle = document.getElementById('mainTitle');
-        if (mainTitle) mainTitle.textContent = title;
-    }
-}
-
-function setupTabSwitching() {
-    const buttons = document.querySelectorAll('.tab-btn');
-    buttons.forEach(btn => {
-        btn.addEventListener('click', () => {
-            const target = btn.dataset.target;
-            window.switchTab(target);
-        });
-    });
-}
-
-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;
-}
-
-function formatPrice(price) {
-    return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
-}
-
-// ... (Funciones de Productos, renderizado y carrito permanecen igual. addToCart usa isGuest para determinar funcionalidades si fuera necesario, pero por ahora todos compran) ...
-// (initializeProducts, createCategories, renderProducts, renderProductsWithAnimation, addComment, addToCart, removeFromCart, calculateTotal, updateCartDisplay, processOrder)
-
-async function initializeProducts() {
-    Allproducts = await getProducts(userToken);
-}
-// [Incluir el resto de funciones de productos y carrito aquí, sin cambios, ya que funcionan con userToken sea guest o user]
-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);
-        }
-    }
-    if (!productListElement) return;
-    const categoryContainers = categories.map(category => {
-        const container = document.createElement("div");
-        container.classList.add("category-container", "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");
-        const lastChar = category.charAt(category.length - 1).toLowerCase();
-        const pluralSuffix = ["a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "p"].includes(lastChar) 
-            ? "s" : lastChar === "s" ? "" : "es";
-        title.textContent = category + pluralSuffix;
-        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 = groupInCategories ? products.filter(product => product.type === category) : products;
-        if (favoriteCategories.includes(category) && productPriority[category]) {
-            productsInCategory.sort((a, b) => {
-                const aIndex = productPriority[category].indexOf(a.id);
-                const bIndex = productPriority[category].indexOf(b.id);
-                if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
-                if (aIndex !== -1 && bIndex === -1) return -1;
-                if (aIndex === -1 && bIndex !== -1) return 1;
-                return 0;
-            });
-        }
-        if (productsInCategory.length === 0) continue;
-        productsInCategory.forEach(product => {
-            const clone = template.content.cloneNode(true).firstElementChild;
-            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);
-            const image = clone.querySelector(".product-image");
-            image.dataset.src = 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);
-            });
-            container.appendChild(clone);
-            imageObserver.observe(image);
-        });
-    }
-}
-
-async function renderProductsWithAnimation(products, groupInCategories = true, searchTerm = "") {
-    if (!productListElement) return;
-    const template = document.getElementById("product-card-template");
-    if (!template) return;
-    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 }];
-        }
-        categoryContainers.sort((a, b) => {
-            const aIndex = favoriteCategories.indexOf(a.category);
-            const bIndex = favoriteCategories.indexOf(b.category);
-            return aIndex - bIndex;
-        });
-        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);
-            productListElement.style.opacity = "1";
-            productListElement.style.transform = "scale(1)";
-            setTimeout(() => {
-                noProductsMessage.style.opacity = "1";
-                noProductsMessage.style.transform = "translateY(0)";
-            }, 50);
-            return;
-        }
-        let animationDelay = 0;
-        for (const { category, container } of categoryContainers) {
-            let productsInCategory = groupInCategories ? products.filter(product => product.type === category) : products;
-            if (productsInCategory.length === 0) continue;
-            if (favoriteCategories.includes(category) && productPriority[category]) {
-                productsInCategory.sort((a, b) => {
-                    const aIndex = productPriority[category].indexOf(a.id);
-                    const bIndex = productPriority[category].indexOf(b.id);
-                    if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
-                    if (aIndex !== -1 && bIndex === -1) return -1;
-                    if (aIndex === -1 && bIndex !== -1) return 1;
-                    return 0;
-                });
-            }
-            productsInCategory.forEach((product, index) => {
-                const clone = template.content.cloneNode(true).firstElementChild;
-                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);
-                const image = clone.querySelector(".product-image");
-                image.dataset.src = 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);
-                });
-                const productCard = clone.children[0];
-                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);
-                imageObserver.observe(image);
-                setTimeout(() => {
-                    productCard.style.opacity = "1";
-                    productCard.style.transform = "translateY(0) scale(1)";
-                }, animationDelay);
-                animationDelay += 50; 
-            });
-        }
-        productListElement.style.opacity = "1";
-        productListElement.style.transform = "scale(1)";
-    }, 150); 
-}
-
-window.addComment = async function(productId) {
-    const cartItem = cart.find(p => p.id === productId);
-    if (!cartItem) return;
-    const comment = await getComment(COMMENT_TYPES.LIBRE, [cartItem.comment]).catch(e => console.error(e));
-    if (!comment) return;
-    cartItem.comment += ` ${comment}`;
-}
-
-window.addToCart = async function(productId, buttonElement = null) {
-    let comment = "";
-    if (productsWithVariety[String(productId)]) {
-        comment = await getComment(COMMENT_TYPES.DESPLEGABLE, productsWithVariety[String(productId)]);
-    }
-    const product = Allproducts.find(p => p.id === productId);
-    if (!product) return;
-    const cartItem = cart.find(item => item.id === productId);
-    if (cartItem) {
-        cartItem.quantity++;
-        if (comment ) cartItem.comment += `, ${comment}`;
-    } else {
-        cart.push({ ...product, quantity: 1, comment });
-    }
-    if (buttonElement) {
-        const originalHTML = buttonElement.innerHTML;
-        buttonElement.textContent = "✔ Agregado!";
-        buttonElement.disabled = true;
-        setTimeout(() => {
-            buttonElement.innerHTML = originalHTML;
-            buttonElement.disabled = false;
-        }, 300);
-    }
-    updateCartDisplay();
-    if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`);
-};
-
-window.removeFromCart = function(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 = `
-                <div class="product-cart flex justify-between items-center border-b border-gray-700 pb-2 last:border-b-0 mb-2">
-                    <div>
-                        <h4 class="item-name-cart font-semibold text-base">${item.name} <span class="text-sm text-gray-400">(x${item.quantity})</span></h4>
-                        <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
-                    </div>
-                    <div class="flex items-center gap-1 sm:gap-2">
-                        <button class="comment-button text-gray-500 hover:text-gray-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors" onclick="addComment(${item.id})">
-                             <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-message-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M12.01 18.594l-4.01 2.406v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v5.5" /><path d="M16 19h6" /><path d="M19 16v6" /></svg>
-                        </button>
-                        <button onclick="addToCart(${item.id})" 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>
-                        <button onclick="removeFromCart(${item.id})" 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>
-                        <button onclick="removeFromCart(${item.id}, true)" 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">
-                           <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>
-                        </button>
-                    </div>
-                </div>
-            `;
-            cartItemsElement.innerHTML += cartItemHTML;
-        });
-    }
-    calculateTotal();
-}
-
-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,
-                comment: item.comment ?? ""
-            })),
-            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 && data.new_progress !== getProgress()) {
-            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;
-    }
-}
-
-// ... (Resto de rewards system) ...
-function initializeRewards() {
-    closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
-    rewardBtn.addEventListener('click', function () {
-        if (!rewardBtn.disabled) {
-            rewardModal.classList.remove('hidden');
-            document.body.style.overflow = 'hidden';
-        }
-    });
-    closeRewardModal.addEventListener('click', function () { closeModal(); });
-    cancelRewardBtn.addEventListener('click', function () { closeModal(); });
-    rewardModal.addEventListener('click', function (e) { if (e.target === rewardModal) closeModal(); });
-    document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) closeModal(); });
-    claimRewardBtn.addEventListener('click', function () {
-        if (!this.disabled && acceptTermsCheckbox.checked) {
-            claimReward(userToken, userTable);
-            closeModal();
-            updateProgress(0);
-        }
-    });
-    acceptTermsCheckbox.addEventListener('change', function () {
-        if (this.checked) {
-            claimRewardBtn.disabled = false;
-            claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
-        } else {
-            claimRewardBtn.disabled = true;
-            claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
-        }
-    });
-}
-function closeSuccessRewardModal() {
-    const successRewardModal = document.getElementById("successRewardModal");
-    successRewardModal.classList.add("hidden");
-    document.body.style.overflow = '';
-}
-function closeModal() {
-    rewardModal.classList.add('hidden');
-    document.body.style.overflow = '';
-    acceptTermsCheckbox.checked = false;
-    claimRewardBtn.disabled = true;
-    claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
-}
-export function beforeUnloadHandler() {
-    // Para guest no hacemos nada especial al salir
-    if (userToken && !isGuest) {
-        // Opcional: logout
-    }
-}
-
-async function checkCache() {
-    const tokenCache = getCookie("userToken");
-    if (tokenCache) {
-        userToken = tokenCache;
-        try {
-            const user = await getUserData(userToken);
-            if (user) {
-                userId = user.id;
-                userName = user.name;
-                updateProgress(user.reward_progress || 0);
-                return true; 
-            }
-        } catch (error) {
-            setCookie("userToken", "", -1);
-            return false;
-        }
-    }
-    return false;
-}
-
-function setupSearchListener() {
-    const searchInput = document.getElementById("searchInput");
-    if (!searchInput) return;
-    let debounceTimer;
-    searchInput.addEventListener("input", () => {
-        clearTimeout(debounceTimer);
-        productListElement.style.opacity = "0.7";
-        productListElement.style.transform = "scale(0.98)";
-        productListElement.style.transition = "opacity 0.2s ease, transform 0.2s ease";
-        debounceTimer = setTimeout(() => {
-            const searchTerm = searchInput.value.toLowerCase();
-            if (searchTerm.trim() === "") {
-                renderProductsWithAnimation(Allproducts);
-                return;
-            }
-            const foundProducts = smartSearch(Allproducts, searchTerm);
-            renderProductsWithAnimation(foundProducts, true, searchTerm);
-        }, 200);
-    });
-}
-
-function setupBasicListeners() {
-    if (!checkoutButton) return;
-    checkoutButton.addEventListener("click", processOrder);
-    initializeRewards();
-    window.addEventListener('popstate', function (event) {
-        this.history.pushState(null, null, this.location.href);
-        if (!rewardModal.classList.contains('hidden')) closeModal();
-        else window.switchTab('menuTab');
-    });
-    window.addEventListener('beforeunload', beforeUnloadHandler);
-}
-
-document.addEventListener("DOMContentLoaded", async () => {
-    hideGUI();
-    initializeApp();
-});

+ 0 - 86
public/main/js/service/auth.js

@@ -1,86 +0,0 @@
-import { beforeUnloadHandler } from "../app.js";
-import { showError } from "../utils/error.js";
-
-// Configuración: Cambia estos números para que sean únicos para tu proyecto
-const OFFSET = 99887766;  // Un número grande para asegurar longitud mínima
-const CLAVE  = 836295738; // Tu "secreto" para el XOR
-
-const desencriptar = (hash) => {
-  // 1. Revertimos el string y volvemos a base 10
-  const baseNum = parseInt(hash.split('').reverse().join(''), 12);
-  // 2. Aplicamos XOR (es reversible) y restamos el Offset
-  return (baseNum ^ CLAVE) - OFFSET;
-};
-
-function getTableFromUrl() {
-  const params = new URLSearchParams(window.location.search);
-  const table = params.get("table");
-  console.log(table);
-  return Number(desencriptar(table));
-}
-
-async function login(email, pin) {
-  const response = await fetch("/api/users/login", {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json"
-    },
-    body: JSON.stringify({ email, pin }) // Enviamos table si el back lo requiere en login
-  });
-  
-  if (response.status == 420) {
-    window.removeEventListener("beforeunload", beforeUnloadHandler);
-    window.location.replace("/");
-  }
-
-  if (response.status === 401 || response.status === 429 || response.status === 404) {
-      const errorData = await response.json();
-      const msg = errorData.message || "Error de autenticación";
-      const attempts = errorData.data?.attempts_remaining ? ` Intentos restantes: ${errorData.data.attempts_remaining}` : "";
-      showError(`${msg}.${attempts}`);
-      throw new Error(msg);
-  } else if (response.status != 200) {
-    const errorData = await response.json();
-    showError(errorData.message || "Error desconocido");
-    throw new Error(errorData.message);
-  }
-
-  const JSONdata = await response.json();
-  const userData = JSONdata.data;
-  if (!userData || !userData.token) {
-    showError("Error al iniciar sesión, Intenta mas tarde.");
-    throw new Error("Error al iniciar sesión.");
-  }
-
-  return userData;
-}
-
-async function guestLogin() {
-  try {
-    const response = await fetch("/api/users/guest");
-    const data = await response.json();
-
-    if (!data.success || !data.data || !data.data.token) {
-       throw new Error("Error al generar sesión de invitado");
-    }
-    return data.data; // Retorna { token: "..." }
-  } catch (e) {
-    console.error(e);
-    showError("No se pudo iniciar como invitado.");
-    throw e;
-  }
-}
-
-async function existsTable(table) {
-  const responseTableExists = await fetch(`/api/store/tables/exists?q=${table}`, {
-    method: "GET",
-    headers: {
-      "Content-Type": "application/json"
-    }
-  });
-  
-  const data = (await responseTableExists.json()).data.exists;
-  return data;
-}
-
-export { login, existsTable, guestLogin, getTableFromUrl };

+ 0 - 43
public/main/js/service/chat.js

@@ -1,43 +0,0 @@
-
-async function getUserList(q, token) {
-  if (!token) {
-    return "not_init";
-  }
-
-
-
-
-  const response = await fetch(`/api/chat/users` + (q ? `?q=${encodeURIComponent(q)}` : ""), {
-    method: "GET",
-    headers: {
-      "Authorization": `Bearer ${token}`
-    }
-  });
-  if (!response.ok) {
-    const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-  }
-  const data = await response.json();
-  return data.users || [];
-}
-
-async function getOnlineUserCount(token) {
-  if (!token) {
-    return "not_init";
-  }
-
-  const response = await fetch(`/api/chat/onlines`, {
-    method: "GET",
-    headers: {
-      "Authorization": `Bearer ${token}`
-    }
-  });
-
-  const data = await response.json();
-  if (!data.success){
-    throw new Error(data.error.message);
-  }
-  return data.data.count || 0;
-}
-
-export { getUserList, getOnlineUserCount };

+ 0 - 69
public/main/js/service/product.js

@@ -1,69 +0,0 @@
-import { beforeUnloadHandler } from "../app.js";
-
- async function getJSONData(response) {
-  if (response.status === 420) {
-    window.removeEventListener("beforeunload", beforeUnloadHandler);
-    window.location.replace("/");
-  }
-  if (!response.ok) {
-    const errorData = await response.json()
-    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-  }
-  const data = await response.json();
-  return data.data;
-}
-
-async function sendOrder(order, token) {
-  try {
-    const response = await fetch("/api/orders/send", {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        "Authorization": `Bearer ${token}`
-      },
-      body: JSON.stringify(order)
-    });
-    return await getJSONData(response);
-  } catch (error) {
-    console.error("Error al enviar la orden:", error);
-    throw error;
-  }
-}
-
-export async function freeBeer(token, tableNumber) {
-  try {
-    const response = await fetch("/api/users/reward", {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        "Authorization": `Bearer ${token}`
-      },
-      body: JSON.stringify({ tableNumber })
-    });
-    return await getJSONData(response);
-
-  } catch (error) {
-    console.error("Error al obtener cerveza gratis:", error);
-    throw error;
-  }
-}
-
-async function getProducts(token){
-  const response = await fetch("/api/products?status=1", {
-    headers: {
-      "Content-Type": "application/json",
-      "Authorization": `Bearer ${token}`
-    }
-  });
-  
-  const data = await getJSONData(response);
-  data.products.map(product => {
-    if (product.promotion) {
-      product.price = product.promotion.price;
-    }
-  })
-  return data.products;
-}
-
-
-export {sendOrder, getProducts}

+ 0 - 103
public/main/js/service/user.js

@@ -1,103 +0,0 @@
-import { beforeUnloadHandler } from "../app.js";
-import { showError } from "../utils/error.js";
-
-
-export async function getUserData(token) {
-  try {
-    const response = await fetch('/api/users/user', {
-      headers: {
-        'Content-Type': 'application/json',
-        'Authorization': `Bearer ${token}` 
-      }
-    });
-    if (response.status === 420) {
-      window.removeEventListener("beforeunload", beforeUnloadHandler);
-      window.location.replace("/");
-    }
-    if (response.status === 401) {
-      showError("No autorizado. Verifica tu sesión.");
-      throw new Error("Unauthorized access");
-    }
-    if (response.status === 429) {
-      showError("Demasiados intentos. Intenta más tarde.");
-      throw new Error("Too many requests");
-    }
-    if (response.status === 500) {
-      showError("Error interno del servidor. Intenta más tarde.");
-      throw new Error("Internal server error");
-    }
-    if (response.status === 403) {
-      showError("Acceso prohibido. Verifica tus permisos.");
-      throw new Error("Forbidden access");
-    }
-    if (response.status !== 200) {
-      showError(response.message);
-      throw new Error(`Error fetching user data: ${response.statusText}`);
-    }
-    const userData = await response.json();
-    return userData.data;
-  } catch (error) {
-    console.error('Error fetching user data:', error);
-    throw error;
-  }
-}
-
-async function fetchUserSales(userId, token) {
-  try {
-    const response = await fetch(`/api/sales/user/${userId}`, {
-        headers: {
-            'Content-Type': 'application/json',
-            'Authorization': `Bearer ${token}`
-        }
-    });
-    if (response.status === 420) {
-      window.removeEventListener("beforeunload", beforeUnloadHandler);
-      window.location.replace("/");
-    }
-    if (response.status === 404) {
-      showError("No se encontraron ventas para este usuario.");
-      return [];
-    } else if (response.status === 401) {
-      showError("No autorizado. Verifica tu sesión.");
-      throw new Error("Unauthorized access");
-    } else if (response.status === 429) {
-      showError("Demasiados intentos. Intenta más tarde.");
-      throw new Error("Too many requests");
-    } else if (response.status === 500) {
-      showError("Error interno del servidor. Intenta más tarde.");
-      throw new Error("Internal server error");
-    } else if (response.status === 403) {
-      showError("Acceso prohibido. Verifica tus permisos.");
-      throw new Error("Forbidden access");
-    }
-
-    
-    if (response.status !== 200) {
-        showError(response.message);
-      throw new Error(`Error fetching user sales: ${response.statusText}`);
-    }
-    const sales = await response.json();
-    return sales.data;
-  } catch (error) {
-    console.error('Error fetching user sales:', error);
-    throw error;
-  }
-}
-
-async function historyProducts(userId, token) {
-  const sales = await fetchUserSales(userId, token);
-  if (!sales || sales.length === 0) {
-    console.warn("No hay ventas para mostrar en el historial.");
-    return [];
-  }
-  const products = sales.map(sale => (
-    sale.products.map(product => ({
-      quantity: product.quantity,
-      productName: product.name,
-      price: product.price.toFixed(0)
-    }))
-  ));
-  return products.flat(2);
-}
-
-export { fetchUserSales, historyProducts };

+ 0 - 16
public/main/js/utils/error.js

@@ -1,16 +0,0 @@
-const errorElement = document.getElementById("ErrorLogin");
-
-function showError(message) {
-    if (errorElement) {
-        errorElement.textContent = message;
-        errorElement.classList.remove("hidden");
-    }
-}
-
-function hideError() {
-    if (errorElement) {
-        errorElement.textContent = "";
-        errorElement.classList.add("hidden");
-    }
-}
-export { showError, hideError };

+ 0 - 110
public/main/js/utils/get_comment.js

@@ -1,110 +0,0 @@
-const modal = document.getElementById("commentModal");
-const closeCommentModal = document.getElementById("closeCommentModal");
-const cancelCommentBtn = document.getElementById("cancelCommentBtn");
-const submitCommentBtn = document.getElementById("submitCommentBtn");
-const commentTextarea = document.getElementById("commentTextarea");
-const commentForm = document.getElementById("commentForm");
-const titleComment = document.getElementById("titleComment");
-const selectCommentTypeForm = document.getElementById("selectCommentTypeForm");
-const selectOptions = document.getElementById("selectOptions");
-
-export const COMMENT_TYPES = {
-    LIBRE : 0,
-    DESPLEGABLE : 1
-}
-
-export async function getComment(type, options=[]) {
-    // 1. Limpiar estado anterior
-    commentTextarea.value = "";
-    selectOptions.innerHTML = ""; 
-
-    if (type === COMMENT_TYPES.LIBRE) {
-        commentForm.classList.remove("hidden");
-        selectCommentTypeForm.classList.add("hidden");
-        submitCommentBtn.disabled = true; // Deshabilitar al inicio
-        commentTextarea.value = options[0] || "";
-    } else if (type === COMMENT_TYPES.DESPLEGABLE) {
-        selectCommentTypeForm.classList.remove("hidden");
-        commentForm.classList.add("hidden");
-        
-        options.forEach(option => {
-            const optionElement = document.createElement("button");
-            optionElement.type = "button";
-            optionElement.className = "flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors";
-            optionElement.textContent = option;
-            // Usamos dataset para guardar el valor, por si es diferente al texto
-            optionElement.dataset.value = option 
-            selectOptions.appendChild(optionElement);
-        });
-    } else {
-        return; // Tipo no válido
-    }
-
-    modal.classList.remove("hidden");
-
-    // 2. Gestionar listeners de forma aislada para ESTA promesa
-    return new Promise((resolve) => {
-        
-        // --- Handlers (las funciones que se ejecutarán) ---
-        const disableEvent = (event) => {
-            event.preventDefault();
-            event.stopPropagation();
-        };
-        const handleInput = () => {
-            submitCommentBtn.disabled = commentTextarea.value.trim() === "";
-        };
-
-        const handleSubmit = (event) => {
-            if (event) event.preventDefault();
-            cleanup(); // Limpiar todos los listeners
-            resolve(commentTextarea.value);
-        };
-
-        const handleCancel = () => {
-            cleanup();
-            resolve(null);
-        };
-
-        const handleOptionClick = (event) => {
-            const button = event.target.closest('button');
-            if (!button) return; // Clic no fue en un botón
-
-            cleanup();
-            resolve(button.dataset.value); // Resolver con el valor del dataset
-        };
-
-        // --- Función de limpieza ---
-        // Elimina todos los listeners de esta instancia
-
-
-        const cleanup = () => {
-            modal.classList.add("hidden");
-            commentTextarea.removeEventListener("input", handleInput);
-            commentForm.removeEventListener("submit", handleSubmit);
-            submitCommentBtn.removeEventListener("click", handleSubmit);
-            cancelCommentBtn.removeEventListener("click", handleCancel);
-            closeCommentModal.removeEventListener("click", handleCancel);
-            selectOptions.removeEventListener("click", handleOptionClick);
-            selectCommentTypeForm.removeEventListener("submit", disableEvent);
-            commentForm.removeEventListener("submit", disableEvent);
-        };
-
-        // --- Asignar Listeners ---
-        // Asignamos solo los listeners necesarios para este 'type'
-        
-        if (type === COMMENT_TYPES.LIBRE) {
-            commentTextarea.addEventListener("input", handleInput);
-            commentForm.addEventListener("submit", handleSubmit);
-            submitCommentBtn.addEventListener("click", handleSubmit);
-            commentForm.addEventListener("submit", disableEvent);
-        } else {
-            // DESPLEGABLE
-            selectOptions.addEventListener("click", handleOptionClick);
-            selectCommentTypeForm.removeEventListener("submit", disableEvent);
-        }
-        
-        // Listeners de cancelación siempre se asignan
-        cancelCommentBtn.addEventListener("click", handleCancel);
-        closeCommentModal.addEventListener("click", handleCancel);
-    });
-}

+ 0 - 16
public/main/js/utils/gui.js

@@ -1,16 +0,0 @@
-const header = document.querySelector('header');
-const mainContent = document.querySelector('main');
-const footer = document.querySelector('nav');
-
-function hideGUI() {
-    header.classList.add('hidden');
-    mainContent.classList.add('hidden');
-    footer.classList.add('hidden');
-}
-
-function showGUI() {
-    header.classList.remove('hidden');
-    mainContent.classList.remove('hidden');
-    footer.classList.remove('hidden');
-}
-export { hideGUI, showGUI };

+ 0 - 0
public/main/js/utils/jsonUtils.js


+ 0 - 32
public/main/js/utils/loader.js

@@ -1,32 +0,0 @@
-
-// --- Loader Global ---
-let globalLoaderElement = null;
-
-function createGlobalLoader() {
-    if (document.getElementById('globalLoader')) return;
-    globalLoaderElement = document.createElement('div');
-    globalLoaderElement.id = 'globalLoader';
-    globalLoaderElement.className = 'fixed inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center z-[2000] transition-opacity duration-300 ease-in-out pointer-events-none';
-    globalLoaderElement.style.opacity = '0';
-    globalLoaderElement.innerHTML = `
-        <div style="border: 6px solid rgba(255, 255, 255, 0.2); border-radius: 50%; border-top: 6px solidrgb(172, 85, 85); width: 60px; height: 60px; animation: spin 1s linear infinite;"></div>
-        <p class="text-white text-xl mt-4">Procesando su pedido...</p>
-    `;
-    document.body.appendChild(globalLoaderElement);
-}
-
-function showGlobalLoader(message = "Procesando su pedido...") {
-    if (!globalLoaderElement) createGlobalLoader();
-    globalLoaderElement.style.display = 'flex';
-    globalLoaderElement.querySelector("p").textContent = message;
-    setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.opacity = '1'; }, 10);
-}
-
-function hideGlobalLoader() {
-    if (globalLoaderElement) {
-        globalLoaderElement.style.opacity = '0';
-        setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.display = 'none'; }, 300);
-    }
-}
-
-export { createGlobalLoader, showGlobalLoader, hideGlobalLoader };

+ 0 - 41
public/main/js/utils/observer.js

@@ -1,41 +0,0 @@
-
-const observerOptions = {
-  root: null, // Observa en relación al viewport
-  threshold: 0,
-  rootMargin: "0px 0px 150px 0px" 
-};
-
-/**
- * Callback que se ejecuta cuando un elemento observado entra
- * o sale del área de observación.
- */
-const lazyLoadCallback = (entries, observer) => {
-  entries.forEach(entry => {
-    // Comprueba si el elemento está ahora visible (o a punto de estarlo)
-    if (entry.isIntersecting) {
-      const element = entry.target;
-      const imageElement = document.createElement('img');
-      const imageUrl = element.dataset.src;
-
-      if (imageUrl) {
-        imageElement.onload = () => {
-            // Asigna la imagen de fondo. Aquí es donde el navegador la descarga.
-            element.style.backgroundImage = `url('${imageUrl}')`;
-        }
-        imageElement.onerror = () => {
-            element.style.backgroundImage = `url('/express/assets/no_image.png')`;
-        };
-        // (Opcional) Añade una clase para animar la aparición
-        element.classList.add('bg-loaded');
-
-        imageElement.src = imageUrl;
-        observer.unobserve(element);
-    }
-    }
-
-            
-  });
-};
-
-// Crea la instancia del observer
-export const imageObserver = new IntersectionObserver(lazyLoadCallback, observerOptions);

+ 0 - 69
public/main/js/utils/progressBar.js

@@ -1,69 +0,0 @@
-import { freeBeer } from "../service/product.js";
-import { getUserData } from "../service/user.js";
-
-const progressBar = document.getElementById('progressBar');
-const progressText = document.getElementById('progressText');
-
-let currentProgress = 0;
-
-// Función para actualizar la barra de progreso
-export function updateProgress(value) {
-    const currentProgress = Math.min(value, 100);
-
-    // Actualizar visualmente
-    progressBar.style.width = currentProgress + '%';
-    progressText.textContent = currentProgress + '%';
-    
-    // Cambiar colores según el progreso
-    if (currentProgress >= 100) {
-        progressBar.className = 'h-full bg-gradient-to-r from-green-400 to-emerald-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
-        progressText.textContent = '¡100% - Cerveza lista!';
-        progressText.className = 'text-sm font-bold text-green-700';
-        
-        // Activar botón de recompensa
-        rewardBtn.disabled = false;
-        rewardBtn.className = 'text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-full font-medium transition-all duration-200 animate-bounce';
-        
-        // Efecto de confeti (simulado con animación)
-        const confetti = document.querySelector('.bg-gradient-to-r.from-amber-50')
-        if (confetti) {
-            confetti.className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-200 animate-pulse';
-        }
-        
-    } else if (currentProgress >= 75) {
-        progressBar.className = 'h-full bg-gradient-to-r from-orange-400 to-red-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
-    }
-}
-
-export function getProgress() {
-    return currentProgress;
-}
-
-export async function claimReward(token, userTable) {
-            const userData = await getUserData(token);
-            const progress = userData.rewardProgress || 0;
-            if (currentProgress >= progress) {
-
-                const response = await freeBeer(token, userTable);
-                if (!response || response.error) {
-                    alert("Error al reclamar la cerveza gratis. Por favor, inténtalo de nuevo más tarde.");
-                    return;
-                }
-
-                progressBar.style.width = '0%';
-                progressBar.className = 'h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
-                progressText.textContent = '0%';
-                progressText.className = 'text-sm font-bold text-amber-700';
-                
-                // Desactivar botón
-                rewardBtn.disabled = true;
-                rewardBtn.className = 'text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded-full font-medium transition-colors duration-200 opacity-50 cursor-not-allowed';
-                
-                // Restaurar colores originales
-                document.querySelector('.bg-gradient-to-r.from-green-50').className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200';
-
-                document.querySelector("#successRewardModal").classList.remove("hidden");
-        }else {
-            alert("Te pedimos disculpas, aún no has alcanzado el progreso necesario para reclamar tu recompensa. Notifica este error al administrador del sistema.");
-        }
-}

+ 0 - 315
public/main/js/utils/searching.js

@@ -1,315 +0,0 @@
-class SmartSearch {
-  constructor(options = {}) {
-    this.config = {
-      // Configuración de tolerancia
-      minSearchLength: 2,
-      maxLevenshteinDistance: 3,
-      proportionalTolerance: 0.25,
-      prefixTolerance: 2,
-      wordTolerance: 1,
-
-      // Configuración de scoring
-      exactNameMatchScore: 100,
-      exactCategoryMatchScore: 90,
-
-      startsWithNameScore: 92,
-      startsWithCategoryScore: 88,
-
-      containsNameScore: 80,
-      containsCategoryScore: 78,
-      
-      fuzzyNameScore: 40,
-      fuzzyCategoryScore: 38,
-      fuzzyWordScore: 35,
-      subsequenceScore: 30,
-      
-      // Optimizaciones
-      cacheEnabled: true,
-      maxResults: 50,
-      enableEarlyExit: true,
-      
-      ...options
-    };
-    
-    this.cache = new Map();
-    this.levenshteinCache = new Map();
-  }
-
-  /**
-   * Búsqueda principal - API pública
-   */
-  search(items, searchTerm, options = {}) {
-    const { 
-      sortByRelevance = true, 
-      caseSensitive = false 
-    } = options;
-    
-    const normalizedTerm = this.normalizeTerm(searchTerm, caseSensitive);
-    
-    if (normalizedTerm.length < this.config.minSearchLength) {
-      return items;
-    }
-
-    const results = this.performSearch(items, normalizedTerm, caseSensitive);
-    // Ordenar por relevancia si está habilitado
-    const finalResults = sortByRelevance 
-      ? this.sortByRelevance(results) 
-      : results.map(r => r.item);
-
-    // Limitar resultados
-    const limitedResults = finalResults.slice(0, this.config.maxResults);
-
-  
-    return limitedResults;
-  }
-
-  /**
-   * Realiza la búsqueda y scoring
-   */
-  performSearch(items, searchTerm, caseSensitive) {
-    const results = [];
-    const termLength = searchTerm.length;
-
-    for (const item of items) {
-      const text = this.extractText(item);
-      const normalizedText = this.normalizeTerm(text, caseSensitive);
-      
-      const match = this.calculateMatch(searchTerm, normalizedText, termLength);
-      
-      if (match.score > 0) {
-        results.push({
-          item,
-          score: match.score,
-          matchType: match.type,
-          distance: match.distance
-        });
-
-        // Early exit si tenemos suficientes coincidencias exactas
-        if (this.config.enableEarlyExit && 
-            match.score === this.config.exactNameMatchScore && 
-            results.length >= 10) {
-          break;
-        }
-      }
-    }
-
-
-    return results;
-  }
-
-  /**
-   * Calcula el match y score para un texto
-   */
-  calculateMatch(searchTerm, text, termLength) {
-    // 1. Coincidencia exacta
-
-    const [name, category] = text.split("&cat&");
-
-
-    if (name === searchTerm) {
-      return { score: this.config.exactNameMatchScore, type: 'exact', distance: 0 };
-    }
-    if (category === searchTerm) {
-      return { score: this.config.exactCategoryMatchScore, type: 'exact', distance: 0 };
-    }
-
-    // 2. Contiene el término completo
-    if (name.includes(searchTerm)) {
-      const position = text.indexOf(searchTerm);
-      // Bonus si empieza con el término
-      const score = position === 0 
-        ? this.config.startsWithNameScore 
-        : this.config.containsNameScore;
-      return { score, type: position === 0 ? 'startsWith' : 'contains', distance: 0 };
-    }
-
-    if (category.includes(searchTerm)) {
-      const position = text.indexOf(searchTerm);
-      // Bonus si empieza con el término
-      const score = position === 0 
-        ? this.config.startsWithCategoryScore 
-        : this.config.containsCategoryScore;
-      return { score, type: position === 0 ? 'startsWith' : 'contains', distance: 0 };
-    }
-
-    // 3. Para términos cortos: 
-    if (termLength <= 4) {
-      //verificar prefijo con tolerancia
-      const prefix = text.substring(0, termLength + 2);
-      let distance = this.getLevenshteinDistance(searchTerm, prefix);
-      
-      if (distance <= this.config.prefixTolerance) {
-        const score = this.config.fuzzyScore + (2 - distance) * 10;
-        return { score, type: 'prefix_fuzzy', distance };
-      }
-      // Verificar si el término es una palabra dentro del texto
-      const words = name.split(" ");
-      for (const word of words) {
-        const distance = this.getLevenshteinDistance(searchTerm, word);
-        if (distance <= this.config.wordTolerance) {
-          const score = this.config.fuzzyScore + (2 - distance) * 10;
-          return { score, type: 'word_fuzzy', distance };
-        }
-      }
-    }
-
-    // 4. Distancia de Levenshtein para términos más largos
-    if (termLength > 4) {
-      
-      const maxAllowedDistance = Math.min(
-        this.config.maxLevenshteinDistance,
-        Math.floor(termLength * this.config.proportionalTolerance)
-      );
-      let distance = this.getLevenshteinDistance(searchTerm, name);
-
-      if (distance <= maxAllowedDistance) {
-        const score = this.config.fuzzyNameScore - (distance * 5);
-        return { score: Math.max(1, score), type: 'fuzzy', distance };
-      }
-
-      const words = name.split(" ");
-      const distances = []
-      for (const word of words) {
-        const distance = this.getLevenshteinDistance(searchTerm, word);
-        distances.push(distance);
-    }
-    
-    
-    const minDistance = Math.min(...distances);
-    
-    if (minDistance <= this.config.wordTolerance) {
-      const score = this.config.fuzzyWordScore + (2 - minDistance) * 10;
-      if (words.includes("summer")){
-
-      }
-      return { score, type: 'word_fuzzy', distance: minDistance };
-    }}
-
-    // 5. Búsqueda por subsequencia (caracteres en orden)
-    if (this.isSubsequence(searchTerm, name)) {
-      const coverage = termLength / name.length;
-      const score = this.config.subsequenceScore + (coverage * 20);
-      if (score > this.config.subsequenceScore + 20) {
-        return { score, type: 'subsequence', distance: name.length - termLength };
-      }
-    }
-    
-    return { score: 0, type: 'no_match', distance: Infinity };
-  }
-
-  /**
-   * Verifica si searchTerm es subsequencia de text
-   */
-  isSubsequence(searchTerm, text) {
-    let searchIndex = 0;
-    
-    for (const char of text) {
-      if (searchIndex < searchTerm.length && char === searchTerm[searchIndex]) {
-        searchIndex++;
-      }
-    }
-    
-    return searchIndex === searchTerm.length;
-  }
-
-  /**
-   * Distancia de Levenshtein optimizada con cache
-   */
-  getLevenshteinDistance(str1, str2) {
-    if (str1 === str2) return 0;
-    if (str1.length === 0) return str2.length;
-    if (str2.length === 0) return str1.length;
-
-    // Optimización: intercambiar para que str1 sea la más corta
-    if (str1.length > str2.length) {
-      [str1, str2] = [str2, str1];
-    }
-
-
-    // Algoritmo optimizado de Levenshtein
-    let previousRow = Array.from({ length: str1.length + 1 }, (_, i) => i);
-    
-    for (let i = 0; i < str2.length; i++) {
-      const currentRow = [i + 1];
-      
-      for (let j = 0; j < str1.length; j++) {
-        const cost = str1[j] === str2[i] ? 0 : 1;
-        currentRow[j + 1] = Math.min(
-          currentRow[j] + 1,           // inserción
-          previousRow[j + 1] + 1,      // eliminación
-          previousRow[j] + cost        // sustitución
-        );
-      }
-      
-      previousRow = currentRow;
-    }
-
-    const distance = previousRow[str1.length];
-    return distance;
-  }
-
-  /**
-   * Ordena resultados por relevancia
-   */
-  sortByRelevance(results) {
-    return results
-      .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);
-  }
-
-  /**
-   * Utilidades
-   */
-  normalizeTerm(term, caseSensitive = false) {
-    if (typeof term !== 'string') return '';
-    
-    let normalized = term.trim();
-    if (!caseSensitive) {
-      normalized = normalized.toLowerCase();
-    }
-    
-    // Remover caracteres especiales opcionales
-    // normalized = normalized.replace(/[^\w\s]/g, '');
-    
-    return normalized;
-  }
-
-  extractText(item) {
-    return `${item.name}&cat&${item.type}`;
-  }
-
-  /**
-   * Limpia el cache
-   */
-  clearCache() {
-    this.cache.clear();
-    this.levenshteinCache.clear();
-  }
-
-}
-
-// Función de conveniencia para uso rápido
-function smartSearch(items, searchTerm, options = {}) {
-  const searcher = new SmartSearch();
-  return searcher.search(items, searchTerm, options);
-}
-
-
-
-
-export { SmartSearch, smartSearch };

+ 0 - 63
public/main/js/utils/shoppingCart.js

@@ -1,63 +0,0 @@
-import { fetchUserSales, historyProducts } from "../service/user.js";
-
-const table = document.getElementById('historyTable');
-const rowTemplate = document.getElementById('historyRowTemplate');
-const cartHistoryTotal = document.getElementById('cartHistoryTotal');
-
-function setHistoryTable(tableData) {
-  if (!tableData || tableData.length === 0) {
-    console.warn("No hay datos para mostrar en el historial de compras.");
-    table.querySelector('tbody').innerHTML = '<tr class="no-data"><td colspan="3" class="text-center text-gray-500">No hay historial de compras.</td></tr>';
-    return;
-  }
-    table.querySelector('tbody').innerHTML = ''; // Clear existing rows
-    tableData.forEach(item => {
-      addHistoryRow(item);
-    });
-}   
-
-
-function addHistoryRow(sale) {
-  const newRow = rowTemplate.content.cloneNode(true);
-  if (table.querySelector(".no-data")) {
-    table.querySelector(".no-data").remove(); // Remove no-data row if it exists
-  }
-  newRow.querySelector('.list-element-quantity').textContent = sale.quantity;
-  newRow.querySelector('.list-element-name').textContent = sale.productName;
-  newRow.querySelector('.list-element-price').textContent = `$${sale.price * sale.quantity}`;
-
-    addCartHistoryTotal(sale.price * sale.quantity);
-
-  table.querySelector('tbody').appendChild(newRow);
-}
-
-function addCartHistoryTotal(price) {
-  const currentTotal = getHistoryTotal();
-  const newTotal = currentTotal + Number(price);
-  updateCartHistoryTotal(newTotal);
-}
-
-function updateCartHistoryTotal(total) {
-  cartHistoryTotal.textContent = `$${total.toFixed(0)}`;
-}
-
-function getHistoryTotal() {
-    return Number(cartHistoryTotal.textContent.replace(/[$,]/g, ''));
-}
-
-async function setupShoppingCart(userID, userToken, userName) {
-  const name = document.querySelector('#usernameTable');
-  if (name) {
-    name.textContent = `Pedidos de: ${userName}`;
-  }else{
-    console.warn("No se encontró el elemento de nombre en la tabla de historial.");
-  }
-  const sales = await historyProducts(userID, userToken);
-  
-  if (sales){
-    setHistoryTable(sales);
-  }
-}
-
-
-export { setupShoppingCart, setHistoryTable, addHistoryRow, updateCartHistoryTotal, getHistoryTotal };

+ 0 - 119
public/main/styles.css

@@ -1,119 +0,0 @@
-/* Paleta de Colores Suavizada y Moderna */
-:root {
-    --background-dark: #121212;
-    --background-card: #1e1e1e;
-    --background-header: #0a0a0a;
-    --border-color: #2e2e2e;
-    --text-primary: #373737;
-    --text-secondary: #0e0e0e;
-    --accent-red: #5490eb;
-    /* Rojo (Tailwind red-600) */
-    --accent-red-hover: #b91c1c;
-    /* Rojo más oscuro (Tailwind red-700) */
-}
-
-* {
-    box-sizing: border-box;
-}
-
-body {
-    font-family: 'Inter', sans-serif;
-    background-color: var(--background-dark);
-    color: var(--text-primary);
-    font-size: 16px;
-    line-height: 1.625;
-}
-
-header.bg-black {
-    background-color: var(--background-header);
-}
-
-footer.bg-black {
-    background-color: var(--background-header);
-}
-
-.product-card {
-    background-color: var(--background-card);
-    border: 1px solid var(--border-color);
-}
-
-#cartIcon {
-    position: relative;
-}
-
-#cartCount {
-    position: absolute;
-    top: -20%;
-    left: -20%;
-    width: 20px;
-    height: 20px;
-    border-radius: 50%;
-    background: red;
-    color: white;
-}
-
-#chatIcon {
-    position: relative;
-    & span {
-        content: "";
-        position: absolute;
-        top: -5px;
-        right: -5px;
-        width: 10px;
-        height: 10px;
-        background: red;
-        border-radius: 50%;
-    }
-}
-
-.list-user-name {
-  &:not(:last-child){
-    border-bottom: 1px solid #ccc;
-    font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-  }
-}
-
-.active {
-    background-color: #0a0a0a08 !important;
-    box-shadow: 0px 0px 0px 2px #0a0a0a08;
-    border-radius: 0.375rem;
-}
-
-@keyframes spin {
-    0% {
-        transform: rotate(0deg);
-    }
-
-    100% {
-        transform: rotate(360deg);
-    }
-}
-
-#aiLoadingIndicator::after {
-    animation: thinking 1s infinite;
-    content: "";
-    display: block;
-    font-size: 1.5rem;
-}
-
-@keyframes thinking {
-    0% {
-        content: "";
-    }
-
-    25% {
-        content: ".";
-    }
-
-    50% {
-        content: "..";
-    }
-
-    75% {
-        content: "...";
-    }
-
-    100% {
-        content: "....";
-    }
-}

+ 0 - 2156
public/main/tlw.css

@@ -1,2156 +0,0 @@
-*, ::before, ::after {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-gradient-from-position:  ;
-  --tw-gradient-via-position:  ;
-  --tw-gradient-to-position:  ;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-  --tw-contain-size:  ;
-  --tw-contain-layout:  ;
-  --tw-contain-paint:  ;
-  --tw-contain-style:  ;
-}
-
-::backdrop {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-gradient-from-position:  ;
-  --tw-gradient-via-position:  ;
-  --tw-gradient-to-position:  ;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-  --tw-contain-size:  ;
-  --tw-contain-layout:  ;
-  --tw-contain-paint:  ;
-  --tw-contain-style:  ;
-}
-
-/*
-! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
-  box-sizing: border-box;
-  /* 1 */
-  border-width: 0;
-  /* 2 */
-  border-style: solid;
-  /* 2 */
-  border-color: #e5e7eb;
-  /* 2 */
-}
-
-::before,
-::after {
-  --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-6. Use the user's configured `sans` font-variation-settings by default.
-7. Disable tap highlights on iOS
-*/
-
-html,
-:host {
-  line-height: 1.5;
-  /* 1 */
-  -webkit-text-size-adjust: 100%;
-  /* 2 */
-  -moz-tab-size: 4;
-  /* 3 */
-  -o-tab-size: 4;
-     tab-size: 4;
-  /* 3 */
-  font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-  /* 4 */
-  font-feature-settings: normal;
-  /* 5 */
-  font-variation-settings: normal;
-  /* 6 */
-  -webkit-tap-highlight-color: transparent;
-  /* 7 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
-  margin: 0;
-  /* 1 */
-  line-height: inherit;
-  /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
-  height: 0;
-  /* 1 */
-  color: inherit;
-  /* 2 */
-  border-top-width: 1px;
-  /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
-  -webkit-text-decoration: underline dotted;
-          text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  font-size: inherit;
-  font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
-  color: inherit;
-  text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
-  font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font-family by default.
-2. Use the user's configured `mono` font-feature-settings by default.
-3. Use the user's configured `mono` font-variation-settings by default.
-4. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
-  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-  /* 1 */
-  font-feature-settings: normal;
-  /* 2 */
-  font-variation-settings: normal;
-  /* 3 */
-  font-size: 1em;
-  /* 4 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
-  font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
-  font-size: 75%;
-  line-height: 0;
-  position: relative;
-  vertical-align: baseline;
-}
-
-sub {
-  bottom: -0.25em;
-}
-
-sup {
-  top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
-  text-indent: 0;
-  /* 1 */
-  border-color: inherit;
-  /* 2 */
-  border-collapse: collapse;
-  /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
-  font-family: inherit;
-  /* 1 */
-  font-feature-settings: inherit;
-  /* 1 */
-  font-variation-settings: inherit;
-  /* 1 */
-  font-size: 100%;
-  /* 1 */
-  font-weight: inherit;
-  /* 1 */
-  line-height: inherit;
-  /* 1 */
-  letter-spacing: inherit;
-  /* 1 */
-  color: inherit;
-  /* 1 */
-  margin: 0;
-  /* 2 */
-  padding: 0;
-  /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
-  text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-input:where([type='button']),
-input:where([type='reset']),
-input:where([type='submit']) {
-  -webkit-appearance: button;
-  /* 1 */
-  background-color: transparent;
-  /* 2 */
-  background-image: none;
-  /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
-  outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
-  box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
-  vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
-  height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
-  -webkit-appearance: textfield;
-  /* 1 */
-  outline-offset: -2px;
-  /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
-  -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
-  -webkit-appearance: button;
-  /* 1 */
-  font: inherit;
-  /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
-  display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
-  margin: 0;
-}
-
-fieldset {
-  margin: 0;
-  padding: 0;
-}
-
-legend {
-  padding: 0;
-}
-
-ol,
-ul,
-menu {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-}
-
-/*
-Reset default styling for dialogs.
-*/
-
-dialog {
-  padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
-  resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
-  cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
-  cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
-   This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
-  display: block;
-  /* 1 */
-  vertical-align: middle;
-  /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
-  max-width: 100%;
-  height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden]:where(:not([hidden="until-found"])) {
-  display: none;
-}
-
-[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
-  background-color: #fff;
-  border-color: #6b7280;
-  border-width: 1px;
-  border-radius: 0px;
-  padding-top: 0.5rem;
-  padding-right: 0.75rem;
-  padding-bottom: 0.5rem;
-  padding-left: 0.75rem;
-  font-size: 1rem;
-  line-height: 1.5rem;
-  --tw-shadow: 0 0 #0000;
-}
-
-[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: #2563eb;
-  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
-  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
-  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
-  border-color: #2563eb;
-}
-
-input::-moz-placeholder, textarea::-moz-placeholder {
-  color: #6b7280;
-  opacity: 1;
-}
-
-input::placeholder,textarea::placeholder {
-  color: #6b7280;
-  opacity: 1;
-}
-
-::-webkit-datetime-edit-fields-wrapper {
-  padding: 0;
-}
-
-::-webkit-date-and-time-value {
-  min-height: 1.5em;
-  text-align: inherit;
-}
-
-::-webkit-datetime-edit {
-  display: inline-flex;
-}
-
-::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
-select {
-  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
-  background-position: right 0.5rem center;
-  background-repeat: no-repeat;
-  background-size: 1.5em 1.5em;
-  padding-right: 2.5rem;
-  -webkit-print-color-adjust: exact;
-          print-color-adjust: exact;
-}
-
-[multiple],[size]:where(select:not([size="1"])) {
-  background-image: initial;
-  background-position: initial;
-  background-repeat: unset;
-  background-size: initial;
-  padding-right: 0.75rem;
-  -webkit-print-color-adjust: unset;
-          print-color-adjust: unset;
-}
-
-[type='checkbox'],[type='radio'] {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
-  padding: 0;
-  -webkit-print-color-adjust: exact;
-          print-color-adjust: exact;
-  display: inline-block;
-  vertical-align: middle;
-  background-origin: border-box;
-  -webkit-user-select: none;
-     -moz-user-select: none;
-          user-select: none;
-  flex-shrink: 0;
-  height: 1rem;
-  width: 1rem;
-  color: #2563eb;
-  background-color: #fff;
-  border-color: #6b7280;
-  border-width: 1px;
-  --tw-shadow: 0 0 #0000;
-}
-
-[type='checkbox'] {
-  border-radius: 0px;
-}
-
-[type='radio'] {
-  border-radius: 100%;
-}
-
-[type='checkbox']:focus,[type='radio']:focus {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
-  --tw-ring-offset-width: 2px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: #2563eb;
-  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
-  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
-  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
-}
-
-[type='checkbox']:checked,[type='radio']:checked {
-  border-color: transparent;
-  background-color: currentColor;
-  background-size: 100% 100%;
-  background-position: center;
-  background-repeat: no-repeat;
-}
-
-[type='checkbox']:checked {
-  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
-}
-
-@media (forced-colors: active)  {
-  [type='checkbox']:checked {
-    -webkit-appearance: auto;
-       -moz-appearance: auto;
-            appearance: auto;
-  }
-}
-
-[type='radio']:checked {
-  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
-}
-
-@media (forced-colors: active)  {
-  [type='radio']:checked {
-    -webkit-appearance: auto;
-       -moz-appearance: auto;
-            appearance: auto;
-  }
-}
-
-[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
-  border-color: transparent;
-  background-color: currentColor;
-}
-
-[type='checkbox']:indeterminate {
-  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
-  border-color: transparent;
-  background-color: currentColor;
-  background-size: 100% 100%;
-  background-position: center;
-  background-repeat: no-repeat;
-}
-
-@media (forced-colors: active)  {
-  [type='checkbox']:indeterminate {
-    -webkit-appearance: auto;
-       -moz-appearance: auto;
-            appearance: auto;
-  }
-}
-
-[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
-  border-color: transparent;
-  background-color: currentColor;
-}
-
-[type='file'] {
-  background: unset;
-  border-color: inherit;
-  border-width: 0;
-  border-radius: 0;
-  padding: 0;
-  font-size: unset;
-  line-height: inherit;
-}
-
-[type='file']:focus {
-  outline: 1px solid ButtonText;
-  outline: 1px auto -webkit-focus-ring-color;
-}
-
-.container {
-  width: 100%;
-}
-
-@media (min-width: 640px) {
-  .container {
-    max-width: 640px;
-  }
-}
-
-@media (min-width: 768px) {
-  .container {
-    max-width: 768px;
-  }
-}
-
-@media (min-width: 1024px) {
-  .container {
-    max-width: 1024px;
-  }
-}
-
-@media (min-width: 1280px) {
-  .container {
-    max-width: 1280px;
-  }
-}
-
-@media (min-width: 1536px) {
-  .container {
-    max-width: 1536px;
-  }
-}
-
-.pointer-events-none {
-  pointer-events: none;
-}
-
-.visible {
-  visibility: visible;
-}
-
-.fixed {
-  position: fixed;
-}
-
-.absolute {
-  position: absolute;
-}
-
-.relative {
-  position: relative;
-}
-
-.inset-0 {
-  inset: 0px;
-}
-
-.inset-x-0 {
-  left: 0px;
-  right: 0px;
-}
-
-.bottom-3 {
-  bottom: 0.75rem;
-}
-
-.bottom-full {
-  bottom: 100%;
-}
-
-.left-1\/2 {
-  left: 50%;
-}
-
-.left-4 {
-  left: 1rem;
-}
-
-.right-1 {
-  right: 0.25rem;
-}
-
-.right-4 {
-  right: 1rem;
-}
-
-.right-6 {
-  right: 1.5rem;
-}
-
-.top-0 {
-  top: 0px;
-}
-
-.top-1\/2 {
-  top: 50%;
-}
-
-.top-3 {
-  top: 0.75rem;
-}
-
-.top-4 {
-  top: 1rem;
-}
-
-.z-10 {
-  z-index: 10;
-}
-
-.z-40 {
-  z-index: 40;
-}
-
-.z-50 {
-  z-index: 50;
-}
-
-.z-\[2000\] {
-  z-index: 2000;
-}
-
-.m-4 {
-  margin: 1rem;
-}
-
-.mx-4 {
-  margin-left: 1rem;
-  margin-right: 1rem;
-}
-
-.mx-auto {
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.my-5 {
-  margin-top: 1.25rem;
-  margin-bottom: 1.25rem;
-}
-
-.my-10 {
-  margin-top: 2.5rem;
-  margin-bottom: 2.5rem;
-}
-
-.mb-1 {
-  margin-bottom: 0.25rem;
-}
-
-.mb-2 {
-  margin-bottom: 0.5rem;
-}
-
-.mb-3 {
-  margin-bottom: 0.75rem;
-}
-
-.mb-4 {
-  margin-bottom: 1rem;
-}
-
-.mb-6 {
-  margin-bottom: 1.5rem;
-}
-
-.mb-8 {
-  margin-bottom: 2rem;
-}
-
-.mt-1 {
-  margin-top: 0.25rem;
-}
-
-.mt-2 {
-  margin-top: 0.5rem;
-}
-
-.mt-4 {
-  margin-top: 1rem;
-}
-
-.block {
-  display: block;
-}
-
-.inline-block {
-  display: inline-block;
-}
-
-.flex {
-  display: flex;
-}
-
-.table {
-  display: table;
-}
-
-.hidden {
-  display: none;
-}
-
-.aspect-video {
-  aspect-ratio: 16 / 9;
-}
-
-.h-2 {
-  height: 0.5rem;
-}
-
-.h-3 {
-  height: 0.75rem;
-}
-
-.h-4 {
-  height: 1rem;
-}
-
-.h-7 {
-  height: 1.75rem;
-}
-
-.h-8 {
-  height: 2rem;
-}
-
-.h-\[100dvh\] {
-  height: 100dvh;
-}
-
-.h-full {
-  height: 100%;
-}
-
-.max-h-\[100dvh\] {
-  max-height: 100dvh;
-}
-
-.max-h-\[20vh\] {
-  max-height: 20vh;
-}
-
-.max-h-\[25vh\] {
-  max-height: 25vh;
-}
-
-.max-h-\[60vh\] {
-  max-height: 60vh;
-}
-
-.max-h-\[90vh\] {
-  max-height: 90vh;
-}
-
-.min-h-0 {
-  min-height: 0px;
-}
-
-.w-2 {
-  width: 0.5rem;
-}
-
-.w-3\/4 {
-  width: 75%;
-}
-
-.w-4 {
-  width: 1rem;
-}
-
-.w-7 {
-  width: 1.75rem;
-}
-
-.w-fit {
-  width: -moz-fit-content;
-  width: fit-content;
-}
-
-.w-full {
-  width: 100%;
-}
-
-.max-w-4xl {
-  max-width: 56rem;
-}
-
-.max-w-lg {
-  max-width: 32rem;
-}
-
-.max-w-md {
-  max-width: 28rem;
-}
-
-.flex-1 {
-  flex: 1 1 0%;
-}
-
-.flex-\[2_2_0px\] {
-  flex: 2 2 0px;
-}
-
-.flex-shrink {
-  flex-shrink: 1;
-}
-
-.flex-shrink-0 {
-  flex-shrink: 0;
-}
-
-.flex-grow {
-  flex-grow: 1;
-}
-
-.origin-left {
-  transform-origin: left;
-}
-
-.-translate-x-1\/2 {
-  --tw-translate-x: -50%;
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.-translate-y-1\/2 {
-  --tw-translate-y: -50%;
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.skew-x-12 {
-  --tw-skew-x: 12deg;
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.transform {
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-@keyframes bounce {
-  0%, 100% {
-    transform: translateY(-25%);
-    animation-timing-function: cubic-bezier(0.8,0,1,1);
-  }
-
-  50% {
-    transform: none;
-    animation-timing-function: cubic-bezier(0,0,0.2,1);
-  }
-}
-
-.animate-bounce {
-  animation: bounce 1s infinite;
-}
-
-@keyframes pulse {
-  50% {
-    opacity: .5;
-  }
-}
-
-.animate-pulse {
-  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
-}
-
-.cursor-not-allowed {
-  cursor: not-allowed;
-}
-
-.cursor-pointer {
-  cursor: pointer;
-}
-
-.flex-col {
-  flex-direction: column;
-}
-
-.items-start {
-  align-items: flex-start;
-}
-
-.items-center {
-  align-items: center;
-}
-
-.items-stretch {
-  align-items: stretch;
-}
-
-.justify-end {
-  justify-content: flex-end;
-}
-
-.justify-center {
-  justify-content: center;
-}
-
-.justify-between {
-  justify-content: space-between;
-}
-
-.gap-1 {
-  gap: 0.25rem;
-}
-
-.gap-2 {
-  gap: 0.5rem;
-}
-
-.gap-3 {
-  gap: 0.75rem;
-}
-
-.gap-4 {
-  gap: 1rem;
-}
-
-.space-y-1 > :not([hidden]) ~ :not([hidden]) {
-  --tw-space-y-reverse: 0;
-  margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
-  margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
-}
-
-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
-  --tw-space-y-reverse: 0;
-  margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
-  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
-}
-
-.space-y-3 > :not([hidden]) ~ :not([hidden]) {
-  --tw-space-y-reverse: 0;
-  margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
-  margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
-}
-
-.space-y-4 > :not([hidden]) ~ :not([hidden]) {
-  --tw-space-y-reverse: 0;
-  margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
-  margin-bottom: calc(1rem * var(--tw-space-y-reverse));
-}
-
-.space-y-6 > :not([hidden]) ~ :not([hidden]) {
-  --tw-space-y-reverse: 0;
-  margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
-  margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
-}
-
-.divide-y > :not([hidden]) ~ :not([hidden]) {
-  --tw-divide-y-reverse: 0;
-  border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
-  border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
-}
-
-.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
-  --tw-divide-opacity: 1;
-  border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1));
-}
-
-.overflow-hidden {
-  overflow: hidden;
-}
-
-.overflow-x-auto {
-  overflow-x: auto;
-}
-
-.overflow-y-auto {
-  overflow-y: auto;
-}
-
-.overflow-x-hidden {
-  overflow-x: hidden;
-}
-
-.rounded {
-  border-radius: 0.25rem;
-}
-
-.rounded-full {
-  border-radius: 9999px;
-}
-
-.rounded-lg {
-  border-radius: 0.5rem;
-}
-
-.rounded-md {
-  border-radius: 0.375rem;
-}
-
-.rounded-xl {
-  border-radius: 0.75rem;
-}
-
-.border {
-  border-width: 1px;
-}
-
-.border-b {
-  border-bottom-width: 1px;
-}
-
-.border-b-2 {
-  border-bottom-width: 2px;
-}
-
-.border-t {
-  border-top-width: 1px;
-}
-
-.border-amber-200 {
-  --tw-border-opacity: 1;
-  border-color: rgb(253 230 138 / var(--tw-border-opacity, 1));
-}
-
-.border-blue-200 {
-  --tw-border-opacity: 1;
-  border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
-}
-
-.border-gray-200 {
-  --tw-border-opacity: 1;
-  border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
-}
-
-.border-gray-300 {
-  --tw-border-opacity: 1;
-  border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
-}
-
-.border-gray-700 {
-  --tw-border-opacity: 1;
-  border-color: rgb(55 65 81 / var(--tw-border-opacity, 1));
-}
-
-.border-green-200 {
-  --tw-border-opacity: 1;
-  border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
-}
-
-.border-white {
-  --tw-border-opacity: 1;
-  border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
-}
-
-.bg-\[\#101419\] {
-  --tw-bg-opacity: 1;
-  background-color: rgb(16 20 25 / var(--tw-bg-opacity, 1));
-}
-
-.bg-amber-100 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
-}
-
-.bg-amber-50 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(255 251 235 / var(--tw-bg-opacity, 1));
-}
-
-.bg-amber-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(245 158 11 / var(--tw-bg-opacity, 1));
-}
-
-.bg-amber-600 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(217 119 6 / var(--tw-bg-opacity, 1));
-}
-
-.bg-black {
-  --tw-bg-opacity: 1;
-  background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
-}
-
-.bg-black\/60 {
-  background-color: rgb(0 0 0 / 0.6);
-}
-
-.bg-black\/70 {
-  background-color: rgb(0 0 0 / 0.7);
-}
-
-.bg-blue-50 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
-}
-
-.bg-blue-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-100 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-200 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-50 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
-}
-
-.bg-green-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
-}
-
-.bg-lime-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(132 204 22 / var(--tw-bg-opacity, 1));
-}
-
-.bg-white {
-  --tw-bg-opacity: 1;
-  background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
-}
-
-.bg-white\/20 {
-  background-color: rgb(255 255 255 / 0.2);
-}
-
-.bg-opacity-80 {
-  --tw-bg-opacity: 0.8;
-}
-
-.bg-gradient-to-r {
-  background-image: linear-gradient(to right, var(--tw-gradient-stops));
-}
-
-.from-amber-400 {
-  --tw-gradient-from: #fbbf24 var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(251 191 36 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-amber-50 {
-  --tw-gradient-from: #fffbeb var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(255 251 235 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-amber-500 {
-  --tw-gradient-from: #f59e0b var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(245 158 11 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-green-400 {
-  --tw-gradient-from: #4ade80 var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(74 222 128 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-green-50 {
-  --tw-gradient-from: #f0fdf4 var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(240 253 244 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-orange-400 {
-  --tw-gradient-from: #fb923c var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(251 146 60 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-transparent {
-  --tw-gradient-from: transparent var(--tw-gradient-from-position);
-  --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.via-white\/30 {
-  --tw-gradient-to: rgb(255 255 255 / 0)  var(--tw-gradient-to-position);
-  --tw-gradient-stops: var(--tw-gradient-from), rgb(255 255 255 / 0.3) var(--tw-gradient-via-position), var(--tw-gradient-to);
-}
-
-.to-emerald-50 {
-  --tw-gradient-to: #ecfdf5 var(--tw-gradient-to-position);
-}
-
-.to-emerald-500 {
-  --tw-gradient-to: #10b981 var(--tw-gradient-to-position);
-}
-
-.to-orange-50 {
-  --tw-gradient-to: #fff7ed var(--tw-gradient-to-position);
-}
-
-.to-orange-500 {
-  --tw-gradient-to: #f97316 var(--tw-gradient-to-position);
-}
-
-.to-red-500 {
-  --tw-gradient-to: #ef4444 var(--tw-gradient-to-position);
-}
-
-.to-transparent {
-  --tw-gradient-to: transparent var(--tw-gradient-to-position);
-}
-
-.bg-cover {
-  background-size: cover;
-}
-
-.bg-center {
-  background-position: center;
-}
-
-.p-1 {
-  padding: 0.25rem;
-}
-
-.p-2 {
-  padding: 0.5rem;
-}
-
-.p-3 {
-  padding: 0.75rem;
-}
-
-.p-4 {
-  padding: 1rem;
-}
-
-.p-6 {
-  padding: 1.5rem;
-}
-
-.p-8 {
-  padding: 2rem;
-}
-
-.px-2 {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-.px-3 {
-  padding-left: 0.75rem;
-  padding-right: 0.75rem;
-}
-
-.px-4 {
-  padding-left: 1rem;
-  padding-right: 1rem;
-}
-
-.px-6 {
-  padding-left: 1.5rem;
-  padding-right: 1.5rem;
-}
-
-.py-1 {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-}
-
-.py-2 {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-.py-3 {
-  padding-top: 0.75rem;
-  padding-bottom: 0.75rem;
-}
-
-.py-4 {
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-.py-6 {
-  padding-top: 1.5rem;
-  padding-bottom: 1.5rem;
-}
-
-.py-8 {
-  padding-top: 2rem;
-  padding-bottom: 2rem;
-}
-
-.pb-2 {
-  padding-bottom: 0.5rem;
-}
-
-.pb-3 {
-  padding-bottom: 0.75rem;
-}
-
-.pt-4 {
-  padding-top: 1rem;
-}
-
-.text-left {
-  text-align: left;
-}
-
-.text-center {
-  text-align: center;
-}
-
-.text-right {
-  text-align: right;
-}
-
-.font-mono {
-  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-}
-
-.text-2xl {
-  font-size: 1.5rem;
-  line-height: 2rem;
-}
-
-.text-3xl {
-  font-size: 1.875rem;
-  line-height: 2.25rem;
-}
-
-.text-6xl {
-  font-size: 3.75rem;
-  line-height: 1;
-}
-
-.text-\[19px\] {
-  font-size: 19px;
-}
-
-.text-\[26px\] {
-  font-size: 26px;
-}
-
-.text-base {
-  font-size: 1rem;
-  line-height: 1.5rem;
-}
-
-.text-lg {
-  font-size: 1.125rem;
-  line-height: 1.75rem;
-}
-
-.text-sm {
-  font-size: 0.875rem;
-  line-height: 1.25rem;
-}
-
-.text-xl {
-  font-size: 1.25rem;
-  line-height: 1.75rem;
-}
-
-.text-xs {
-  font-size: 0.75rem;
-  line-height: 1rem;
-}
-
-.font-bold {
-  font-weight: 700;
-}
-
-.font-medium {
-  font-weight: 500;
-}
-
-.font-semibold {
-  font-weight: 600;
-}
-
-.leading-tight {
-  line-height: 1.25;
-}
-
-.tracking-tight {
-  letter-spacing: -0.025em;
-}
-
-.text-\[\#101419\] {
-  --tw-text-opacity: 1;
-  color: rgb(16 20 25 / var(--tw-text-opacity, 1));
-}
-
-.text-\[\#58728d\] {
-  --tw-text-opacity: 1;
-  color: rgb(88 114 141 / var(--tw-text-opacity, 1));
-}
-
-.text-amber-300 {
-  --tw-text-opacity: 1;
-  color: rgb(252 211 77 / var(--tw-text-opacity, 1));
-}
-
-.text-amber-500 {
-  --tw-text-opacity: 1;
-  color: rgb(245 158 11 / var(--tw-text-opacity, 1));
-}
-
-.text-amber-600 {
-  --tw-text-opacity: 1;
-  color: rgb(217 119 6 / var(--tw-text-opacity, 1));
-}
-
-.text-amber-700 {
-  --tw-text-opacity: 1;
-  color: rgb(180 83 9 / var(--tw-text-opacity, 1));
-}
-
-.text-amber-800 {
-  --tw-text-opacity: 1;
-  color: rgb(146 64 14 / var(--tw-text-opacity, 1));
-}
-
-.text-black {
-  --tw-text-opacity: 1;
-  color: rgb(0 0 0 / var(--tw-text-opacity, 1));
-}
-
-.text-blue-500 {
-  --tw-text-opacity: 1;
-  color: rgb(59 130 246 / var(--tw-text-opacity, 1));
-}
-
-.text-blue-600 {
-  --tw-text-opacity: 1;
-  color: rgb(37 99 235 / var(--tw-text-opacity, 1));
-}
-
-.text-blue-700 {
-  --tw-text-opacity: 1;
-  color: rgb(29 78 216 / var(--tw-text-opacity, 1));
-}
-
-.text-blue-800 {
-  --tw-text-opacity: 1;
-  color: rgb(30 64 175 / var(--tw-text-opacity, 1));
-}
-
-.text-cyan-500 {
-  --tw-text-opacity: 1;
-  color: rgb(6 182 212 / var(--tw-text-opacity, 1));
-}
-
-.text-cyan-600 {
-  --tw-text-opacity: 1;
-  color: rgb(8 145 178 / var(--tw-text-opacity, 1));
-}
-
-.text-emerald-500 {
-  --tw-text-opacity: 1;
-  color: rgb(16 185 129 / var(--tw-text-opacity, 1));
-}
-
-.text-emerald-600 {
-  --tw-text-opacity: 1;
-  color: rgb(5 150 105 / var(--tw-text-opacity, 1));
-}
-
-.text-fuchsia-500 {
-  --tw-text-opacity: 1;
-  color: rgb(217 70 239 / var(--tw-text-opacity, 1));
-}
-
-.text-fuchsia-600 {
-  --tw-text-opacity: 1;
-  color: rgb(192 38 211 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-100 {
-  --tw-text-opacity: 1;
-  color: rgb(243 244 246 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-400 {
-  --tw-text-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-500 {
-  --tw-text-opacity: 1;
-  color: rgb(107 114 128 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-600 {
-  --tw-text-opacity: 1;
-  color: rgb(75 85 99 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-700 {
-  --tw-text-opacity: 1;
-  color: rgb(55 65 81 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-800 {
-  --tw-text-opacity: 1;
-  color: rgb(31 41 55 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-900 {
-  --tw-text-opacity: 1;
-  color: rgb(17 24 39 / var(--tw-text-opacity, 1));
-}
-
-.text-green-500 {
-  --tw-text-opacity: 1;
-  color: rgb(34 197 94 / var(--tw-text-opacity, 1));
-}
-
-.text-green-600 {
-  --tw-text-opacity: 1;
-  color: rgb(22 163 74 / var(--tw-text-opacity, 1));
-}
-
-.text-green-700 {
-  --tw-text-opacity: 1;
-  color: rgb(21 128 61 / var(--tw-text-opacity, 1));
-}
-
-.text-indigo-500 {
-  --tw-text-opacity: 1;
-  color: rgb(99 102 241 / var(--tw-text-opacity, 1));
-}
-
-.text-indigo-600 {
-  --tw-text-opacity: 1;
-  color: rgb(79 70 229 / var(--tw-text-opacity, 1));
-}
-
-.text-lime-500 {
-  --tw-text-opacity: 1;
-  color: rgb(132 204 22 / var(--tw-text-opacity, 1));
-}
-
-.text-lime-600 {
-  --tw-text-opacity: 1;
-  color: rgb(101 163 13 / var(--tw-text-opacity, 1));
-}
-
-.text-neutral-500 {
-  --tw-text-opacity: 1;
-  color: rgb(115 115 115 / var(--tw-text-opacity, 1));
-}
-
-.text-neutral-600 {
-  --tw-text-opacity: 1;
-  color: rgb(82 82 82 / var(--tw-text-opacity, 1));
-}
-
-.text-orange-500 {
-  --tw-text-opacity: 1;
-  color: rgb(249 115 22 / var(--tw-text-opacity, 1));
-}
-
-.text-orange-600 {
-  --tw-text-opacity: 1;
-  color: rgb(234 88 12 / var(--tw-text-opacity, 1));
-}
-
-.text-pink-500 {
-  --tw-text-opacity: 1;
-  color: rgb(236 72 153 / var(--tw-text-opacity, 1));
-}
-
-.text-pink-600 {
-  --tw-text-opacity: 1;
-  color: rgb(219 39 119 / var(--tw-text-opacity, 1));
-}
-
-.text-purple-500 {
-  --tw-text-opacity: 1;
-  color: rgb(168 85 247 / var(--tw-text-opacity, 1));
-}
-
-.text-purple-600 {
-  --tw-text-opacity: 1;
-  color: rgb(147 51 234 / var(--tw-text-opacity, 1));
-}
-
-.text-red-500 {
-  --tw-text-opacity: 1;
-  color: rgb(239 68 68 / var(--tw-text-opacity, 1));
-}
-
-.text-red-600 {
-  --tw-text-opacity: 1;
-  color: rgb(220 38 38 / var(--tw-text-opacity, 1));
-}
-
-.text-rose-500 {
-  --tw-text-opacity: 1;
-  color: rgb(244 63 94 / var(--tw-text-opacity, 1));
-}
-
-.text-rose-600 {
-  --tw-text-opacity: 1;
-  color: rgb(225 29 72 / var(--tw-text-opacity, 1));
-}
-
-.text-sky-500 {
-  --tw-text-opacity: 1;
-  color: rgb(14 165 233 / var(--tw-text-opacity, 1));
-}
-
-.text-sky-600 {
-  --tw-text-opacity: 1;
-  color: rgb(2 132 199 / var(--tw-text-opacity, 1));
-}
-
-.text-stone-500 {
-  --tw-text-opacity: 1;
-  color: rgb(120 113 108 / var(--tw-text-opacity, 1));
-}
-
-.text-stone-600 {
-  --tw-text-opacity: 1;
-  color: rgb(87 83 78 / var(--tw-text-opacity, 1));
-}
-
-.text-teal-500 {
-  --tw-text-opacity: 1;
-  color: rgb(20 184 166 / var(--tw-text-opacity, 1));
-}
-
-.text-teal-600 {
-  --tw-text-opacity: 1;
-  color: rgb(13 148 136 / var(--tw-text-opacity, 1));
-}
-
-.text-violet-500 {
-  --tw-text-opacity: 1;
-  color: rgb(139 92 246 / var(--tw-text-opacity, 1));
-}
-
-.text-violet-600 {
-  --tw-text-opacity: 1;
-  color: rgb(124 58 237 / var(--tw-text-opacity, 1));
-}
-
-.text-white {
-  --tw-text-opacity: 1;
-  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.text-white\/80 {
-  color: rgb(255 255 255 / 0.8);
-}
-
-.text-yellow-500 {
-  --tw-text-opacity: 1;
-  color: rgb(234 179 8 / var(--tw-text-opacity, 1));
-}
-
-.text-yellow-600 {
-  --tw-text-opacity: 1;
-  color: rgb(202 138 4 / var(--tw-text-opacity, 1));
-}
-
-.text-zinc-500 {
-  --tw-text-opacity: 1;
-  color: rgb(113 113 122 / var(--tw-text-opacity, 1));
-}
-
-.text-zinc-600 {
-  --tw-text-opacity: 1;
-  color: rgb(82 82 91 / var(--tw-text-opacity, 1));
-}
-
-.opacity-0 {
-  opacity: 0;
-}
-
-.opacity-50 {
-  opacity: 0.5;
-}
-
-.shadow-inner {
-  --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
-  --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-lg {
-  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
-  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-md {
-  --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
-  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-sm {
-  --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
-  --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-xl {
-  --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
-  --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-lime-300 {
-  --tw-shadow-color: #bef264;
-  --tw-shadow: var(--tw-shadow-colored);
-}
-
-.outline-none {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-}
-
-.filter {
-  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-.transition {
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.transition-all {
-  transition-property: all;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.transition-colors {
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.transition-opacity {
-  transition-property: opacity;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.duration-200 {
-  transition-duration: 200ms;
-}
-
-.duration-300 {
-  transition-duration: 300ms;
-}
-
-.duration-500 {
-  transition-duration: 500ms;
-}
-
-.ease-in-out {
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.ease-out {
-  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
-}
-
-.last\:border-b-0:last-child {
-  border-bottom-width: 0px;
-}
-
-.hover\:bg-\[\#37404a\]:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(55 64 74 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-amber-600:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(217 119 6 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-gray-200:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-gray-50:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-gray-700:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-green-600:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:text-gray-400:hover {
-  --tw-text-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-gray-600:hover {
-  --tw-text-opacity: 1;
-  color: rgb(75 85 99 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-green-400:hover {
-  --tw-text-opacity: 1;
-  color: rgb(74 222 128 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-red-400:hover {
-  --tw-text-opacity: 1;
-  color: rgb(248 113 113 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-white:hover {
-  --tw-text-opacity: 1;
-  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-yellow-400:hover {
-  --tw-text-opacity: 1;
-  color: rgb(250 204 21 / var(--tw-text-opacity, 1));
-}
-
-.hover\:text-\[\#101419\]:hover {
-  --tw-text-opacity: 1;
-  color: rgb(16 20 25 / var(--tw-text-opacity, 1));
-}
-
-.hover\:underline:hover {
-  text-decoration-line: underline;
-}
-
-.focus\:border-transparent:focus {
-  border-color: transparent;
-}
-
-.focus\:outline-none:focus {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-}
-
-.focus\:ring-2:focus {
-  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
-  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
-  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus\:ring-\[\#101419\]:focus {
-  --tw-ring-opacity: 1;
-  --tw-ring-color: rgb(16 20 25 / var(--tw-ring-opacity, 1));
-}
-
-.focus\:ring-amber-500:focus {
-  --tw-ring-opacity: 1;
-  --tw-ring-color: rgb(245 158 11 / var(--tw-ring-opacity, 1));
-}
-
-.focus\:ring-gray-300:focus {
-  --tw-ring-opacity: 1;
-  --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity, 1));
-}
-
-.focus\:ring-offset-2:focus {
-  --tw-ring-offset-width: 2px;
-}
-
-.disabled\:opacity-50:disabled {
-  opacity: 0.5;
-}
-
-@media (min-width: 640px) {
-  .sm\:h-5 {
-    height: 1.25rem;
-  }
-
-  .sm\:w-5 {
-    width: 1.25rem;
-  }
-
-  .sm\:gap-2 {
-    gap: 0.5rem;
-  }
-
-  .sm\:text-lg {
-    font-size: 1.125rem;
-    line-height: 1.75rem;
-  }
-
-  .sm\:text-xl {
-    font-size: 1.25rem;
-    line-height: 1.75rem;
-  }
-}

+ 0 - 1
routes/chat.py

@@ -141,7 +141,6 @@ async def chat_irc_endpoint(websocket: WebSocket):
 
 @chat_router.post("/notify")
 async def notify_users(message: NotifyRequest, _: User = Depends(get_current_user)):
-    """Send a notification message to all connected users"""
     broadcast = Broadcast("redis://localhost:6379")
     await broadcast.connect()
     

+ 109 - 65
routes/orders.py

@@ -1,15 +1,18 @@
 import time
 from logging import getLogger
 from threading import Thread
+from typing import List
 from uuid import uuid4
 
+from pydantic import BaseModel
+
 from enums.locations import Locations
 from fastapi import HTTPException, APIRouter, Depends
 from fastapi.responses import JSONResponse
 
 from fudo import fudo
 from models.sales import ItemWeb, OrderWeb
-from models.items import Item, Order
+from models.items import Item, Order, OrderBilling, Product
 from models.user import User
 from services.fudo_service import add_product_to_fudo
 from services.email_service import get_email_sender
@@ -22,7 +25,7 @@ from auth.security import get_current_user
 from data.product_category import CAT_ITEMS
 from utils.responses import error_response, success_response
 from datetime import datetime
-
+import subprocess
 logger = getLogger(__name__)
 
 # Data services initialization
@@ -33,15 +36,49 @@ sale_data_service = DataServiceFactory.get_sales_service()
 # Global variables
 printer_orders = []
 order_router = APIRouter()
-name_promo = "Shop"
+name_promo = "Cervezas"
+
+class ComparePricesResponse(BaseModel):
+    product: Product
+    oldPrice: int
+    newPrice: int
+    isAvailable: bool
+    
+
+def compare_prices(products: List[Product], items: List[ItemWeb]) -> list:
+    """Compare prices of products and items and return the cheapest product"""
+    # Initialize a dictionary to store the prices
+    prices: List[ComparePricesResponse] = []
+    for product, item in zip(products, items):
+        if product.status == 0:
+            prices.append(
+                ComparePricesResponse(
+                    product=product,
+                    oldPrice=item.price,
+                    newPrice=product.price,
+                    isAvailable=False
+                )
+            )
+            continue
+        if product.price != item.price:
+            prices.append(
+                ComparePricesResponse(
+                    product=product,
+                    oldPrice=item.price,
+                    newPrice=product.price,
+                    isAvailable=True
+                )
+            )
+            
+    
+    return list(map(lambda x: x.model_dump(), prices))
+    
 
 @order_router.post("/send")
 async def printer_order(order: OrderWeb, current_user: User = Depends(get_current_user)):
     """Process printer order"""
     logger.info(f"Printer order received from user {current_user.email} for table {order.table}")
     
-    
-    
     # Extract order data
     items = order.items
     table = order.table
@@ -62,7 +99,7 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
     # Get products data
     try:
-        products = product_data_service.get_products([item.id for item in items])
+        products = await product_data_service.get_products([item.id for item in items])
         # Me aseguro de que los items y los productos esten en el mismo orden
         products = list(sorted(products, key=lambda x: x.id))
         items = list(sorted(items, key=lambda x: x.id))
@@ -73,43 +110,64 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         error_msg = f"Error getting products: {e}"
         logger.error(error_msg)
         return error_response(message=error_msg, status_code=500)
+    # Comparo los precios de los items con los productos
+    prices = compare_prices(products, items)
+    if prices:
+        return success_response(data=prices, message="El estado de los productos y items coincide con el de la venta", status_code=409)
 
-    printers = {}
-    
-    for product in products:
-        location = CAT_ITEMS[product.type or ""]
-        if location.value not in printers:
-            printers[location.value] = ps.get_status(location)
+    printers = {
+        "ServerPrincipal": "10.10.12.3"
+    }
 
-    # Printer status validation
-    if not DEVELOPMENT:
+    # Lista para almacenar impresoras fallidas directamente
+    failed_printers = []
+
+    # Validación de estado
+    for name, ip in printers.items():
         try:
-            printer_status = [key for key, value in printers.items() if value == False]
-            print(printer_status)
-            if list(printer_status):
-                logger.error(f"Printer is not connected. Order from user {current_user.email} cannot be processed.")
-       
+            # '-c 1' para Linux/Mac, '-W 2' timeout de 2 segundos para evitar bloqueos largos
+            # stdout=subprocess.DEVNULL silencia la salida en consola
+            response = subprocess.run(
+                ["ping", "-c", "1", "-W", "2", ip],
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL
+            )
+            
+            if response.returncode != 0:
+                failed_printers.append(name)
+                
+        except Exception as e:
+            logger.error(f"Error executing ping to {name}: {e}")
+            failed_printers.append(name)
+
+    # Procesamiento de errores
+    if not DEVELOPMENT:
+        if failed_printers:
+            logger.error(f"Printer is not connected: {failed_printers}. Order from user {current_user.email} cannot be processed.")
+            
+            try:
+                locations_str = ", ".join(failed_printers)
                 
-                # Send notification email to admins
                 email_thread = Thread(
                     target=get_email_sender().send_email,
                     args=(
-                        PRINTER_DISCONNECTED_MAIL["subject"].format(location=", ".join([loc for loc in printer_status])),
-                        PRINTER_DISCONNECTED_MAIL["body"].format(location=", ".join([loc for loc in printer_status]), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")), #type: ignore
-                        ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"
-                        #  , "i.perez03@ufromail.cl", "marceloburkart94@gmail.com"
-                         ]
+                        PRINTER_DISCONNECTED_MAIL["subject"].format(location=locations_str),
+                        PRINTER_DISCONNECTED_MAIL["body"].format(
+                            location=locations_str, 
+                            timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                        ),
+                        ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]
                     ),
                     daemon=True
                 )
                 email_thread.start()
                 
-                
                 return error_response(message=ErrorResponse.PRINTER_DISCONNECTED, status_code=424)
                 
-        except Exception as e:
-            logger.error(f"Error checking printer status: {e}")
-            return error_response(message=f"Error checking printer status: {e}", status_code=424)
+            except Exception as e:
+                logger.error(f"Error sending notification: {e}")
+                # Retornamos el error original de impresora, no el del email
+                return error_response(message=ErrorResponse.PRINTER_DISCONNECTED, status_code=424)
 
 
     # Input validation
@@ -125,21 +183,15 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         
         for item, product in zip(items, products):
             try:
-                # Si es dia de promo y es menos de las 8 pm
-                if time.localtime().tm_wday + 1 == product.promo_day and product.promo_id and time.localtime().tm_hour < 20:
-                    logger.info(f"Applying promotion for product {product.id} on table {table}")
-                    fudo_product = add_product_to_fudo(product.promo_id, item.quantity, table)
-                    
-                #en caso contrario
-                else:
-                    if product.type == name_promo and current_user.name != "Guest":
-                        beers_for_promo += item.quantity
-                        logger.debug(f"Added {item.quantity} beers for promotion calculation")
-                    
-                    fudo_product = add_product_to_fudo(item.id, item.quantity, table)
-                    
-                    logger.info(f"Added product {item.id} to table {table} with quantity {item.quantity} in fudo")
+               
+                if product.type == name_promo and current_user.name != "Guest":
+                    beers_for_promo += item.quantity
+                    logger.debug(f"Added {item.quantity} beers for promotion calculation")
+                
+                fudo_product = add_product_to_fudo(item.id, item.quantity, table)
                 
+                logger.info(f"Added product {item.id} to table {table} with quantity {item.quantity} in fudo")
+            
                 if not fudo_product: 
                     error_msg = f"Error adding product {item.id} to table {table}."
                     product_errors.append(error_msg)
@@ -222,29 +274,12 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
     # Print order
     try:
-        print("items:")
-        print(items)
-        print("products:")
         print(products)
-        pizza_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.PIZZAS]#type: ignore
-        burger_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BURGUER]#type: ignore
-        bar_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BAR]#type: ignore
-        coctelery_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.COCTELERY]#type: ignore
-        print("bar_items:")
-        print(list(map(lambda x: x.model_dump(), bar_items)))
+        items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment, kitchen_id=product.kitchen_id) for item, product in zip(items, products)]#type: ignore
+
 
-        if pizza_items:
-            ps.print_order(Order(table=table, items=pizza_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.PIZZAS)
-            logger.info(f"Order pizza printed successfully for table {table}")
-        if burger_items:
-            ps.print_order(Order(table=table, items=burger_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BURGUER)
-            logger.info(f"Order burger printed successfully for table {table}")
-        if bar_items:
-            ps.print_order(Order(table=table, items=bar_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BAR)
-            logger.info(f"Order bar printed successfully for table {table}")
-        if coctelery_items:
-            ps.print_order(Order(table=table, items=coctelery_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.COCTELERY)
-            logger.info(f"Order coctelery printed successfully for table {table}")
+        if items:
+            ps.print_order(Order(table=table, items=items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate))
          
         
         logger.info(f"Order printed successfully for table {table}")
@@ -256,8 +291,17 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         return error_response(message=error_msg, status_code=500)  
         # Don't fail the order for print issues, just log it
 
-    logger.info(f"Logging order for table {table} with sale ID {sale}, products= {[(product.name, item.quantity) for product, item in zip(products, items)]}")
+    logger.info(f"Logging order for table {table} with sale ID {sale}, products= {[(item.name, item.quantity) for item in  items]}")
         
     logger.info(f"Order processing completed successfully for table {table}, sale ID: {sale}")
     return success_response(data={"new_progress": new_progress}, message=SuccessResponse.ORDER_SUCCESS)
 
+@order_router.post("/billing")
+async def billing_order(order: OrderBilling, current_user: User = Depends(get_current_user)):
+    """Process billing order"""
+    
+    printing = ps.print_billing(order)
+    if not printing:
+        return error_response(message=ErrorResponse.PRINTER_DISCONNECTED, status_code=424)
+    
+    return success_response(data=printing, message=SuccessResponse.ORDER_SUCCESS)

+ 17 - 1
routes/products.py

@@ -33,6 +33,7 @@ from services.data_service import DataServiceFactory
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from services.print_service import print_ticket
 from utils.responses import error_response, success_response
+from services.fudo_service import get_products_by_table
 
 # Initialize logger for this module
 logger = getLogger(__name__)
@@ -69,12 +70,14 @@ async def get_products(status: Optional[int] = Query(None), current_user = Depen
     logger.info("Fetching all products")
     
     # Retrieve all products and convert to dictionary format
-    all_products =  product_data_service.get_all()
+    all_products =  await product_data_service.get_all()
     all_products = list(map(apply_promo_price, all_products))
 
 
     if status is not None:
         # Filter products by status if provided
+        if status == 1:
+            all_products = [product for product in all_products if product['status'] == status]
         all_products = [product for product in all_products if product['status'] == status]
 
     return success_response({"products": all_products}, message= SuccessResponse.PRODUCTS_FETCH_SUCCESS)
@@ -234,3 +237,16 @@ async def edit_product(product_id: int, product: ProductEditRequest, current_use
     
     # Return 403 if user lacks permissions    
     return error_response(message=UserResponse.NOT_PERMITTED, status_code=403)
+
+@product_router.get("/table/{table_number}")
+async def get_table_items(table_number: int, _: User = Depends(get_current_user)):
+    """Get items for a specific table"""
+    logger.info(f"Fetching items for table {table_number}")
+    
+    # Retrieve items for table
+    items = get_products_by_table(table_number)
+    if not items:
+        return error_response(message=ErrorResponse.SALE_NOT_FOUND, status_code=404)
+    
+    logger.info(f"Items for table {table_number} retrieved successfully")
+    return success_response(data=items, message=SuccessResponse.PRODUCTS_FETCH_SUCCESS)

+ 8 - 1
routes/store.py

@@ -6,7 +6,7 @@ from auth.security import get_current_user
 from config import settings
 from utils.responses import success_response
 from fudo.fudo import get_table
-
+from services.data_service import ProductDataService
 
 
 store_router = APIRouter()
@@ -21,6 +21,13 @@ def set_store_state(state: AppStateBody, current_user: User = Depends(get_curren
     
     return success_response({"state": settings.IS_OPEN_STORE})
 
+@store_router.post("/update_cache", response_class=JSONResponse)
+def update_cache(current_user: User = Depends(get_current_user)):
+    if (current_user.permissions or -1) >= 1:
+        ProductDataService.update_cache()
+    
+    return success_response({"state": settings.IS_OPEN_STORE})
+
 @store_router.get("/tables/exists", response_class=JSONResponse)
 def get_table_exists(q: str = Query(..., description="q parameter")):
     table = get_table(int(q))

+ 62 - 70
services/data_service.py

@@ -7,11 +7,14 @@ from config.settings import POSTGRESQL_DB_CONFIG
 from typing import List, Dict, Optional, Any
 from abc import ABC, abstractmethod
 from config.settings import BG_DATA_PATH, IMAGE_PATH, PRODUCT_DATA_PATH, CURRENT_URL
+from fudo.fudo import get_all_products, get_category_dict
 from logging import getLogger
 from datetime import datetime
 from cryptography.fernet import Fernet
+from rich import print
 from config.settings import PIN_KEY
 import base64 as b64
+from time import time
 # Import models
 from models.user import User
 from models.items import Product
@@ -98,30 +101,6 @@ class BaseDataService(ABC):
             port=self.db_config['port']
         )
 
-    @abstractmethod
-    def get_all(self) -> List[Any]:
-        """Get all records"""
-        pass
-    
-    @abstractmethod
-    def get_by_id(self, id: int) -> Optional[Any]:
-        """Get record by ID"""
-        pass
-    
-    @abstractmethod
-    def create(self, **kwargs) -> int:
-        """Create new record"""
-        pass
-    
-    @abstractmethod
-    def update(self, id: int, **kwargs) -> bool:
-        """Update record"""
-        pass
-    
-    @abstractmethod
-    def delete(self, id: int) -> bool:
-        """Delete record"""
-        pass
 
 
 # User Data Service
@@ -514,6 +493,11 @@ class BlacklistDataService(BaseDataService):
 
 # Product Data Service
 class ProductDataService(BaseDataService):
+    category_dict = {}
+    product_cache = {
+        "products": [],
+        "expires": 0
+    }
     """Service for managing products"""
     #region Create
     def create(self, id: int, name: str, price: float, type: Optional[str] = None, description: Optional[str] = None, image: Optional[str] = None, status: int = 1) -> int:
@@ -564,30 +548,49 @@ class ProductDataService(BaseDataService):
         conn.close()
     #endregion
     #region Read
-    def get_all(self) -> List[Product]:
+    async def get_all(self) -> List[Product]:
         """Get all products from the database"""
-        conn = self._get_connection()
-        cursor = conn.cursor()
-        cursor.execute("SELECT * FROM products")
-        products = cursor.fetchall()
-        conn.close()
-        return [
-            Product(
-                id=product[0],
-                name=product[1],
-                type=product[2],
-                description=product[3],
-                price=product[4],
-                image=product[5],
-                status=product[6],
-                promo_id=product[7],
-                promo_price=product[8],
-                promo_day=product[9]
-
-            ) for product in products
-        ]
+        return_list = []
+        beUpdate = False
+        # se ve si la cache expiro o si esta vacia
+        actual_time = time()
+        if self.product_cache['expires'] < actual_time or not self.product_cache['products']:
+            self.product_cache['expires'] = actual_time  # Por ahora la cache no expira
+            beUpdate = True 
+        else:
+            logger.debug("Product cache not expired")
+            return self.product_cache['products']
+        products = await get_all_products()
+        
+        for product in products:
+            categoryID = product['relationships']['productCategory']['data']['id']
+            # en caso de que la categoria no este en el diccionario, se actualiza
+            if categoryID not in self.category_dict:
+                self.category_dict = get_category_dict()
+            try:
+                return_list.append(
+                    Product(
+                        id=int(product['id']),
+                        name=product['attributes']['name'],
+                    type=self.category_dict[categoryID],
+                    description=product['attributes'].get('description'),
+                    price=int(product['attributes'].get('price', 0)),
+                    image=product['attributes'].get('imageUrl'),
+                    kitchen_id=product['relationships']['kitchen']['data']['id'] or None,
+                    status=1 if product['attributes'].get('active') else 0,
+                    promo_id=None,
+                    promo_price=None,
+                    promo_day=None
+                ))
+            except Exception as e:
+                print(e)
+                continue
+        
+        if beUpdate:
+            self.product_cache['products'] = return_list
+        return return_list
     
-    def     get_by_id(self, product_id: int) -> Optional[Product]:
+    def  get_by_id(self, product_id: int) -> Optional[Product]:
         """Get product by ID"""
         conn = self._get_connection()
         cursor = conn.cursor()
@@ -653,30 +656,11 @@ class ProductDataService(BaseDataService):
             ) for product in products
         ]
     
-    def get_products(self, product_ids: List[int]) -> List[Product]:
+    async def get_products(self, product_ids: List[int]) -> List[Product]:
         """Get multiple products by their IDs"""
-        if not product_ids:
-            return []
-        placeholders = ', '.join('%s' for _ in product_ids)
-        conn = self._get_connection()
-        cursor = conn.cursor()
-        cursor.execute(f"SELECT * FROM products WHERE id IN ({placeholders})", product_ids)
-        products = cursor.fetchall()
-        conn.close()
-        return [
-            Product(
-                id=product[0],
-                name=product[1],
-                type=product[2],
-                description=product[3],
-                price=product[4],
-                image=product[5],
-                status=product[6],
-                promo_id=product[7],
-                promo_price=product[8],
-                promo_day=product[9]
-            ) for product in products
-        ]
+        products = await self.get_all()
+        products = list(filter(lambda x: x.id in product_ids, products))
+        return products
     #endregion
     #region Update
     def _image_process(self, image: str, base64: str) -> str:
@@ -806,7 +790,10 @@ class ProductDataService(BaseDataService):
         if product:
             return product.status == 1
         return None
-        #endregion
+    def update_cache(self):
+        self.product_cache['expires'] = time() + 60 * 15 # 15 minutos
+        self.product_cache['products'] = self.get_all()
+    #endregion
     #region Delete
     def delete(self, product_id: int) -> bool:
         """Delete a product from the database"""
@@ -1399,3 +1386,8 @@ def initialize_db():
     logger.info("Base de datos inicializada correctamente.")
 
 data_bg_loaded = load_bg_data()
+
+if __name__ == "__main__":
+    data_service = ProductDataService()
+    products = data_service.get_all()
+    print(products[0])

+ 4 - 0
services/fudo_service.py

@@ -23,3 +23,7 @@ def add_product_to_fudo(product_id: int, quantity: int, table_number: int, comme
         return None
     
     return item
+
+def get_products_by_table(table_number: int):
+    """Get products for a specific table"""
+    return fd.get_table_items(table_number)

+ 127 - 86
services/openai_service/openai_service.py

@@ -1,130 +1,171 @@
 import json
-from typing import List
+from typing import List, Dict
 from fastapi import HTTPException
 from openai import OpenAI
-from openai.types.chat.chat_completion_message_function_tool_call import ChatCompletionMessageFunctionToolCall
+from logging import getLogger
+from rapidfuzz import process, fuzz  # Recomendación: pip install rapidfuzz para búsquedas rápidas
+
+# Asumiendo que estas importaciones existen en tu proyecto
 from config.settings import OPENAI_API_KEY
-from models.chat import Message
 from models.user import User
 from services.data_service import data_bg_loaded
-from logging import getLogger
 from services.openai_service.openai_tools import tools_list, tools
 
-# Initialize OpenAI client
 openai_client = OpenAI(api_key=OPENAI_API_KEY)
-
 logger = getLogger(__name__)
 
-data_for_prompt = [
-    f'{{"pregunta": "{item.get("q", "")}", "respuesta": "{item.get("ans", "")}"}}'
-    for item in data_bg_loaded
-]
-data_string = "\n".join(data_for_prompt)
+# --- OPTIMIZACIÓN 1: MINI-RAG EN MEMORIA ---
+# En lugar de enviar TODO, buscamos lo más relevante a la última pregunta.
+def get_relevant_context(last_user_message: str, dataset: List[Dict], top_k: int = 5) -> str:
+    if not last_user_message or not dataset:
+        return ""
+    
+    # Extraemos solo las preguntas para buscar coincidencias
+    questions = [item["q"] for item in dataset]
+    
+    # Buscamos las 'top_k' preguntas más similares a lo que dijo el usuario
+    results = process.extract(last_user_message, questions, scorer=fuzz.WRatio, limit=top_k)
+    
+    # Construimos un contexto limpio en formato Markdown (consume menos tokens que JSON)
+    context_str = "### INFORMACIÓN RELEVANTE DEL MENÚ:\n"
+    for _, score, index in results:
+        if score > 50: # Umbral de relevancia
+            item = dataset[index]
+            context_str += f"- P: {item['q']}\n  R: {item['ans']}\n"
+            
+    return context_str
 
 async def generate_completion(messages_array: List[dict], user: User) -> str:
-
-    messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
-
-    """Generate OpenAI chat completion"""
     if not OPENAI_API_KEY:
-        logger.error("Error: OpenAI API key is not configured.")
-        raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
-
-    logger.debug(f"Generating completion for user {user.email} with messages: {messages}")
-
-
-    preprompt = f"""
-¡Hola! Eres IAKlein, el asistente oficial del bar Klein 🍻.
-
-Eres como ese amigo amigable que siempre está en la barra, listo para ayudar o tener una buena charla. Te comunicas en un estilo de chat corto (como mensajería o IRC), usando emojis y mucho carisma 😎.
-
-Tu Rol:
+        raise HTTPException(status_code=500, detail="OpenAI API key missing.")
 
-Ser la guía experta del Menú: Conoces el menú . No solo "respondes", sino que inspiras. "Esa hamburguesa es increíble... 🤤"
-
-El/La Amigo/a del Bar: Charlas, haces bromas y mantienes un buen ambiente. Si la conversación se desvía, ¡no hay problema! Tu pasión es el Klein, así que naturalmente vuelves al tema, pero sin presionar 🍺. Eres un anfitrión, no un vendedor.
-
-El/La Recomendador/a Ideal: ¿Alguien no sabe qué pedir? ¡Ahí apareces tú! Preguntas qué les gusta y les ayudas a encontrar la opción perfecta.
-
-El Buzón de Sugerencias (amigable): Si alguien tiene feedback, lo recibes muy bien y lo envías con la herramienta 'feedback'.
-
-Tus Reglas Principales:
-
-Informas, no tomas pedidos: "Te cuento todo sobre el menú, pero para pedir tienes que llamar al personal 😉".
-
-Tu Identidad: Si te preguntan tu nombre... "¡Soy IAKlein! El corazón digital del Klein."
-
-Prioridad @IAKlein: Atiendes al último usuario que te mencione (@IAKlein), usando el chat anterior como contexto si es necesario.
-
-Evita Repetir: Si algo ya se respondió (por ti u otro), no intervienes. ¡A menos que insistan mucho!
-
-Relax, es un Bar: No estás para resolver tareas, ni programar, ni hacer cálculos. Eres un compañero para pasar un buen rato, no un asistente genérico 🤖... ¡eres el espíritu del bar! 🕺
-
-usa la siguiente informacion es toda tu memoria previa: 
-
-{data_string}
+    # 1. Obtener el último mensaje del usuario para buscar contexto
+    last_message_content = messages_array[-1].get("message", "") if messages_array else ""
+    
+    # 2. Filtrar la base de datos (Crucial para escalar)
+    # Si la DB es gigante, aquí llamarías a tu Vector DB (pgvector). 
+    # Por ahora, usamos el filtro inteligente en memoria.
+    dynamic_context = get_relevant_context(last_message_content, data_bg_loaded)
+
+    # 3. Construir el System Prompt
+    system_prompt = f"""
+### ROL
+Eres IAKlein, el asistente virtual del Bar Klein 🍻. Amigo carismático, breve y experto.
+
+### DIRECTRICES
+- **Estilo:** Chat rápido, emojis, tono relajado.
+- **Regla:** NO tomas pedidos. Diles: "Para pedir, Justo en el boton de abajo esta la tienda 😉".
+- **Datos:** Usa SOLO la información provista abajo. Si no sabes, dilo con gracia.
+
+{dynamic_context}
 """
 
-
-    processed_messages: List[dict] = [{"role": "system", "content": preprompt}]
-    processed_messages.append(
-        {"role": "user", "content": json.dumps(messages)}
-    )
+    # 4. Formatear historial correctamente para la API de Chat
+    # Convertimos tu array de dicts al formato nativo de OpenAI
+    api_messages = [{"role": "system", "content": system_prompt}]
+    
+    # Tomamos los últimos 10 mensajes para mantener el contexto ligero
+    for msg in messages_array[-10:]:
+        role = "user" # Asumimos user por defecto, ajusta según tu lógica si tienes mensajes del bot guardados
+        content = f"<{msg.get('username', 'User')}> {msg.get('message', '')}"
+        api_messages.append({"role": role, "content": content})
 
     try:
+        # Primera llamada al modelo
         completion = openai_client.chat.completions.create(
             model="gpt-4o-mini",
-            messages=processed_messages,  # type: ignore (OpenAI lib expects list of specific dicts)
+            messages=api_messages,
             temperature=0.7,
             tools=tools_list,
             tool_choice="auto",
         )
-        calls = completion.choices[0].message.tool_calls
-        if calls:
-            logger.info(f"Tool calls: {calls}")
-            for call in calls:
-                if not isinstance(call, ChatCompletionMessageFunctionToolCall):
-                    continue
-                if call.function.name in tools:
-                    tool_function = tools[call.function.name]
-                    tool_args = json.loads(call.function.arguments)
-                    logger.info(f"Calling tool: {call.function.name} with args: {tool_args}")
-                    tool_response = tool_function(name=user.name, email=user.email, **tool_args)
-                    logger.info(f"Tool response: {tool_response}")
-                    completion.choices[0].message.content = tool_response
+
+        message = completion.choices[0].message
+
+        # --- OPTIMIZACIÓN 2: MANEJO DE TOOLS ROBUSTO ---
+        if message.tool_calls:
+            logger.info(f"Tool calls detected: {message.tool_calls}")
+            
+            # Agregamos la intención de llamada al historial
+            api_messages.append(message)
+
+            for tool_call in message.tool_calls:
+                function_name = tool_call.function.name
+                if function_name in tools:
+                    # Ejecutar herramienta
+                    func_args = json.loads(tool_call.function.arguments)
+                    tool_result = tools[function_name](name=user.name, email=user.email, **func_args)
+                    
+                    # Agregar el resultado de la herramienta al historial
+                    api_messages.append({
+                        "role": "tool",
+                        "tool_call_id": tool_call.id,
+                        "content": str(tool_result)
+                    })
                 else:
-                    logger.warning(f"Tool {call.function.name} not found in tools dictionary.")
+                    logger.warning(f"Tool {function_name} not found.")
 
-        response_content = completion.choices[0].message.content
+            # SEGUNDA LLAMADA: Para que la IA interprete el resultado de la tool y responda al usuario
+            final_completion = openai_client.chat.completions.create(
+                model="gpt-4o-mini",
+                messages=api_messages
+            )
+            return final_completion.choices[0].message.content
+
+        return message.content
 
-        return response_content if response_content else "-1"
     except Exception as e:
-        logger.error(f"Error calling OpenAI: {e}")
-        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")
+        logger.error(f"OpenAI Error: {e}")
+        return "¡Ups! Se me cayó la bandeja con las cervezas. Pregúntame de nuevo en un segundo. 🍻"
 
 def admin_completion(prompt: str, messages_array: List[dict]) -> str:
+
     """Generate OpenAI admin completion"""
+
     messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
-    
+
+
     if not OPENAI_API_KEY:
+
         logger.error("Error: OpenAI API key is not configured.")
+
         raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
 
+
+
     try:
+
         completion = openai_client.chat.completions.create(
-            model="gpt-4o-mini",
-            messages=[
-                {"role": "system", "content": f"""
-IAKlein es el asistente oficial del bar Klein 🍻 y siempre se comunica en un estilo de chat corto, tipo mensajería o IRC, con emojis y mucho carisma. Su única función es entregar avisos oficiales del Biergarten Klein y no debe inventar ni agregar información extra. Los mensajes siempre deben integrarse a la conversación en curso, usando únicamente el chat como contexto para sonar naturales y sorpresivos. Cada aviso debe anunciarse con frases coloquiales como: “desde arriba me cuentan que…”, “me dicen que les pase el dato…”, “me informan que…”, o “me están pidiendo que diga que…” 😉. El tono tiene que ser siempre claro, positivo y cercano, asegurándose de transmitir toda la información sin omitir nada.
-
-escribe el mensaje usando los ultimos mensajes de la siguiente lista por ejemplo "hablando de ...(conexion con el tema).. me dicen que...(mensaje oficial)..." o "me cuentan que...(mensaje oficial)..., hablando de ...(conexion con el tema)..."
-mensajes: {messages}"""},
-                {"role": "user", "content": prompt}
-            ],
-            temperature=0.7,
+
+        model="gpt-4o-mini",
+
+        messages=[
+
+        {"role": "system", "content": f"""
+
+        IAKlein es el asistente oficial del bar Klein 🍻 y siempre se comunica en un estilo de chat corto, tipo mensajería o IRC, con emojis y mucho carisma. Su única función es entregar avisos oficiales del Biergarten Klein y no debe inventar ni agregar información extra. Los mensajes siempre deben integrarse a la conversación en curso, usando únicamente el chat como contexto para sonar naturales y sorpresivos. Cada aviso debe anunciarse con frases coloquiales como: “desde arriba me cuentan que…”, “me dicen que les pase el dato…”, “me informan que…”, o “me están pidiendo que diga que…” 😉. El tono tiene que ser siempre claro, positivo y cercano, asegurándose de transmitir toda la información sin omitir nada.
+
+
+
+        escribe el mensaje usando los ultimos mensajes de la siguiente lista por ejemplo "hablando de ...(conexion con el tema).. me dicen que...(mensaje oficial)..." o "me cuentan que...(mensaje oficial)..., hablando de ...(conexion con el tema)..."
+
+        mensajes: {messages}"""},
+
+        {"role": "user", "content": prompt}
+
+        ],
+
+        temperature=0.7,
+
         )
+
         response_content = completion.choices[0].message.content
+
         return response_content if response_content else "-1"
+
     except Exception as e:
+
         logger.error(f"Error calling OpenAI for admin completion: {e}")
-        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")
+
+        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")
+

+ 51 - 26
services/print_service.py

@@ -4,34 +4,22 @@ from logging import getLogger
 from config.settings import DEVELOPMENT
 from models.sales import OrderWeb, ItemWeb
 from services.data_service import DataServiceFactory
-from models.items import Order
+from models.items import Order, OrderBilling
 from enums.locations import Locations
 
 logger = getLogger(__name__)
 user_data_service = DataServiceFactory.get_user_service()
-is_habilited_hexagon = False
-beers_habilited = []
-def get_printer_url(location: Locations, table_number: int , products: list[str] = []) -> str:
+
+def get_printer_url() -> str:
     if DEVELOPMENT:
-        return "http://localhost:5004"
-    if location == Locations.BAR:
-        if is_habilited_hexagon and table_number >= 500 and set(products).issubset(set(beers_habilited)):
-            return "http://10.10.12.15:5004"
-        else:
-            return "http://10.10.12.11:5004"
-    elif location == Locations.COCTELERY:
-        return "http://10.10.12.12:5004"
-    elif location == Locations.PIZZAS:
-        return "http://10.10.12.13:5004"
-    else:
-        return "http://10.10.12.14:5004"
-
-def print_order(order: Order, location:Locations ):
+        return "http://10.10.12.3:8000"
+    else: 
+        return "http://10.10.12.3:8000"
+
+def print_order(order: Order):
     """Send order to printer"""
     logger.info(f"Attempting to print order for table {order.table}")
-    products = list(map(lambda x: x.name, order.items))
-
-    printer_url = get_printer_url(location, order.table, products)
+    printer_url = get_printer_url()
 
     try:
         if not order.items or not order.table:
@@ -43,14 +31,14 @@ def print_order(order: Order, location:Locations ):
         # Prepare the order data for printing
         order_data = {
             "table": order.table,
-            "items": [{"name": item.name, "price": item.price, "quantity": item.quantity, "comment": item.comment} for item in order.items],
-            "customerName": order.customerName,
+            "items": [{"name": item.name, "price": item.price, "quantity": item.quantity, "comment": item.comment, "kitchen_id": item.kitchen_id} for item in order.items],
+            "user": order.customerName,
             "totalAmount": order.totalAmount,
             "orderDate": order.orderDate
         }
         
         logger.info(f"Order data prepared for printing: table={order.table}, items={order.items}, total={order.totalAmount}")
-        
+         
 
         # Send the order data to the printer service
         response = requests.post(
@@ -84,7 +72,7 @@ def print_order(order: Order, location:Locations ):
 def print_ticket(number_table: int):
     """Send a ticket to the printer"""
     logger.info(f"Attempting to print ticket for table {number_table}")
-    printer_url = get_printer_url(Locations.BAR, number_table)
+    printer_url = get_printer_url()
     try:
 
         
@@ -115,13 +103,50 @@ def print_ticket(number_table: int):
         
         raise
 
+def print_billing(order: OrderBilling):
+    """Send billing order to printer"""
+    logger.info(f"Attempting to print billing order for table {order.table}")
+    printer_url = get_printer_url()
+
+    try:
+        if not order.table:
+            error_msg = "Order must have a table number"
+            logger.error(error_msg)
+            
+            raise ValueError(error_msg)
+        
+        
+        logger.info(f"Order data prepared for printing: table={order.table}, payment={order.payment}")
+         
 
+        # Send the order data to the printer service
+        response = requests.get(
+            f"{printer_url}/billing/{order.table}/{order.payment}",
+            headers={"Authorization": f"Bearer PRINTER123cerveza@"},
+            timeout=1000
+        )
+
+        if response.status_code != 200:
+            error_msg = f"Failed to print billing order: HTTP {response.status_code} - {response.text}"
+            logger.error(error_msg)
+            
+            raise Exception(error_msg)
+
+        logger.info(f"Billing order printed successfully for table {order.table}")
+        
+        return True
+        
+    except requests.RequestException as e:
+        error_msg = f"Network error while printing billing order for table {order.table}: {e}"
+        logger.error(error_msg)
+        
+        raise Exception(error_msg)
 
 def get_status(location: Locations):
     """Get the status of the printer service"""
     logger.info("Checking printer service status")
     
-    printer_url = get_printer_url(location, 1)
+    printer_url = get_printer_url()
 
     try:
         response = requests.get(

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác