Ver Fonte

sale la version v2

Erwin Jacimino há 4 meses atrás
pai
commit
579361020e

+ 1 - 1
app.py

@@ -43,7 +43,7 @@ def create_app() -> FastAPI:
         logger.info("Adding CORS middleware")
         logger.info("Adding CORS middleware")
         app.add_middleware(
         app.add_middleware(
             CORSMiddleware,
             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_credentials=True,
             allow_methods=["*"],
             allow_methods=["*"],
             allow_headers=["*"],
             allow_headers=["*"],

+ 137 - 23
fudo/fudo.py

@@ -1,3 +1,4 @@
+import itertools
 import math
 import math
 from time import time
 from time import time
 import requests
 import requests
@@ -6,7 +7,9 @@ import os
 import redis
 import redis
 from logging import getLogger
 from logging import getLogger
 from models.items import Product
 from models.items import Product
-
+from concurrent.futures import ThreadPoolExecutor
+import aiohttp
+import asyncio
 logger = getLogger(__name__)
 logger = getLogger(__name__)
 
 
 api_token = os.getenv('FUDO_API_KEY')
 api_token = os.getenv('FUDO_API_KEY')
@@ -80,6 +83,15 @@ def get_token():
     
     
     return 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():
 def get_categories():
     token = get_token()
     token = get_token()
     url = 'https://api.fu.do/v1alpha1/product-categories'
     url = 'https://api.fu.do/v1alpha1/product-categories'
@@ -87,7 +99,14 @@ def get_categories():
         'Authorization': 'Bearer ' + token
         'Authorization': 'Bearer ' + token
     }
     }
     r = requests.get(url, headers=headers)
     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):
 def get_category(id_category:int):
     token = get_token()
     token = get_token()
@@ -125,6 +144,7 @@ def get_product(id_category:int):
         image=data["attributes"]["imageUrl"],
         image=data["attributes"]["imageUrl"],
         description=data["attributes"]["description"],
         description=data["attributes"]["description"],
         status=1 if data["attributes"]["active"] and data else 0,
         status=1 if data["attributes"]["active"] and data else 0,
+        kitchen_id=data["relationships"]["kitchen"]["data"]["id"],
         promo_day=None,
         promo_day=None,
         promo_price=None,
         promo_price=None,
         promo_id=None
         promo_id=None
@@ -140,30 +160,78 @@ def get_products(page: int = 1):
     r = requests.get(url, headers=headers)
     r = requests.get(url, headers=headers)
     return r.json()['data']
     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 = {}
     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:
             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
 N_PER_PAGE = 100
 
 
 def _get_page_bounds(page: int, token: str):
 def _get_page_bounds(page: int, token: str):
@@ -276,6 +344,52 @@ def get_table(number: int):
         return None
         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):
 def get_sale(sale_id:int):
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     token = get_token()
     token = get_token()
@@ -395,4 +509,4 @@ Configuración de Redis:
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     from rich import print
     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__":
 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__)
 logger = getLogger(__name__)
 
 
-start_time = datetime.time(10, 0, 0)
+start_time = datetime.time(5, 0, 0)
 end_time = datetime.time(23, 30, 0)
 end_time = datetime.time(23, 30, 0)
 
 
 class EmptyUser:
 class EmptyUser:

+ 6 - 1
models/items.py

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

Diff do ficheiro suprimidas por serem muito extensas
+ 16 - 0
public/main/assets/index-BBKc1DLf.js


Diff do ficheiro suprimidas por serem muito extensas
+ 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>
 <!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>
 </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>
 </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")
 @chat_router.post("/notify")
 async def notify_users(message: NotifyRequest, _: User = Depends(get_current_user)):
 async def notify_users(message: NotifyRequest, _: User = Depends(get_current_user)):
-    """Send a notification message to all connected users"""
     broadcast = Broadcast("redis://localhost:6379")
     broadcast = Broadcast("redis://localhost:6379")
     await broadcast.connect()
     await broadcast.connect()
     
     

+ 109 - 65
routes/orders.py

@@ -1,15 +1,18 @@
 import time
 import time
 from logging import getLogger
 from logging import getLogger
 from threading import Thread
 from threading import Thread
+from typing import List
 from uuid import uuid4
 from uuid import uuid4
 
 
+from pydantic import BaseModel
+
 from enums.locations import Locations
 from enums.locations import Locations
 from fastapi import HTTPException, APIRouter, Depends
 from fastapi import HTTPException, APIRouter, Depends
 from fastapi.responses import JSONResponse
 from fastapi.responses import JSONResponse
 
 
 from fudo import fudo
 from fudo import fudo
 from models.sales import ItemWeb, OrderWeb
 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 models.user import User
 from services.fudo_service import add_product_to_fudo
 from services.fudo_service import add_product_to_fudo
 from services.email_service import get_email_sender
 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 data.product_category import CAT_ITEMS
 from utils.responses import error_response, success_response
 from utils.responses import error_response, success_response
 from datetime import datetime
 from datetime import datetime
-
+import subprocess
 logger = getLogger(__name__)
 logger = getLogger(__name__)
 
 
 # Data services initialization
 # Data services initialization
@@ -33,15 +36,49 @@ sale_data_service = DataServiceFactory.get_sales_service()
 # Global variables
 # Global variables
 printer_orders = []
 printer_orders = []
 order_router = APIRouter()
 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")
 @order_router.post("/send")
 async def printer_order(order: OrderWeb, current_user: User = Depends(get_current_user)):
 async def printer_order(order: OrderWeb, current_user: User = Depends(get_current_user)):
     """Process printer order"""
     """Process printer order"""
     logger.info(f"Printer order received from user {current_user.email} for table {order.table}")
     logger.info(f"Printer order received from user {current_user.email} for table {order.table}")
     
     
-    
-    
     # Extract order data
     # Extract order data
     items = order.items
     items = order.items
     table = order.table
     table = order.table
@@ -62,7 +99,7 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
 
     # Get products data
     # Get products data
     try:
     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
         # Me aseguro de que los items y los productos esten en el mismo orden
         products = list(sorted(products, key=lambda x: x.id))
         products = list(sorted(products, key=lambda x: x.id))
         items = list(sorted(items, 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}"
         error_msg = f"Error getting products: {e}"
         logger.error(error_msg)
         logger.error(error_msg)
         return error_response(message=error_msg, status_code=500)
         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:
         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(
                 email_thread = Thread(
                     target=get_email_sender().send_email,
                     target=get_email_sender().send_email,
                     args=(
                     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
                     daemon=True
                 )
                 )
                 email_thread.start()
                 email_thread.start()
                 
                 
-                
                 return error_response(message=ErrorResponse.PRINTER_DISCONNECTED, status_code=424)
                 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
     # 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):
         for item, product in zip(items, products):
             try:
             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: 
                 if not fudo_product: 
                     error_msg = f"Error adding product {item.id} to table {table}."
                     error_msg = f"Error adding product {item.id} to table {table}."
                     product_errors.append(error_msg)
                     product_errors.append(error_msg)
@@ -222,29 +274,12 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
 
     # Print order
     # Print order
     try:
     try:
-        print("items:")
-        print(items)
-        print("products:")
         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}")
         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)  
         return error_response(message=error_msg, status_code=500)  
         # Don't fail the order for print issues, just log it
         # 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}")
     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)
     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 config.messages import ErrorResponse, SuccessResponse, UserResponse
 from services.print_service import print_ticket
 from services.print_service import print_ticket
 from utils.responses import error_response, success_response
 from utils.responses import error_response, success_response
+from services.fudo_service import get_products_by_table
 
 
 # Initialize logger for this module
 # Initialize logger for this module
 logger = getLogger(__name__)
 logger = getLogger(__name__)
@@ -69,12 +70,14 @@ async def get_products(status: Optional[int] = Query(None), current_user = Depen
     logger.info("Fetching all products")
     logger.info("Fetching all products")
     
     
     # Retrieve all products and convert to dictionary format
     # 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))
     all_products = list(map(apply_promo_price, all_products))
 
 
 
 
     if status is not None:
     if status is not None:
         # Filter products by status if provided
         # 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]
         all_products = [product for product in all_products if product['status'] == status]
 
 
     return success_response({"products": all_products}, message= SuccessResponse.PRODUCTS_FETCH_SUCCESS)
     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 403 if user lacks permissions    
     return error_response(message=UserResponse.NOT_PERMITTED, status_code=403)
     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 config import settings
 from utils.responses import success_response
 from utils.responses import success_response
 from fudo.fudo import get_table
 from fudo.fudo import get_table
-
+from services.data_service import ProductDataService
 
 
 
 
 store_router = APIRouter()
 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})
     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)
 @store_router.get("/tables/exists", response_class=JSONResponse)
 def get_table_exists(q: str = Query(..., description="q parameter")):
 def get_table_exists(q: str = Query(..., description="q parameter")):
     table = get_table(int(q))
     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 typing import List, Dict, Optional, Any
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from config.settings import BG_DATA_PATH, IMAGE_PATH, PRODUCT_DATA_PATH, CURRENT_URL
 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 logging import getLogger
 from datetime import datetime
 from datetime import datetime
 from cryptography.fernet import Fernet
 from cryptography.fernet import Fernet
+from rich import print
 from config.settings import PIN_KEY
 from config.settings import PIN_KEY
 import base64 as b64
 import base64 as b64
+from time import time
 # Import models
 # Import models
 from models.user import User
 from models.user import User
 from models.items import Product
 from models.items import Product
@@ -98,30 +101,6 @@ class BaseDataService(ABC):
             port=self.db_config['port']
             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
 # User Data Service
@@ -514,6 +493,11 @@ class BlacklistDataService(BaseDataService):
 
 
 # Product Data Service
 # Product Data Service
 class ProductDataService(BaseDataService):
 class ProductDataService(BaseDataService):
+    category_dict = {}
+    product_cache = {
+        "products": [],
+        "expires": 0
+    }
     """Service for managing products"""
     """Service for managing products"""
     #region Create
     #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:
     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()
         conn.close()
     #endregion
     #endregion
     #region Read
     #region Read
-    def get_all(self) -> List[Product]:
+    async def get_all(self) -> List[Product]:
         """Get all products from the database"""
         """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"""
         """Get product by ID"""
         conn = self._get_connection()
         conn = self._get_connection()
         cursor = conn.cursor()
         cursor = conn.cursor()
@@ -653,30 +656,11 @@ class ProductDataService(BaseDataService):
             ) for product in products
             ) 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"""
         """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
     #endregion
     #region Update
     #region Update
     def _image_process(self, image: str, base64: str) -> str:
     def _image_process(self, image: str, base64: str) -> str:
@@ -806,7 +790,10 @@ class ProductDataService(BaseDataService):
         if product:
         if product:
             return product.status == 1
             return product.status == 1
         return None
         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
     #region Delete
     def delete(self, product_id: int) -> bool:
     def delete(self, product_id: int) -> bool:
         """Delete a product from the database"""
         """Delete a product from the database"""
@@ -1399,3 +1386,8 @@ def initialize_db():
     logger.info("Base de datos inicializada correctamente.")
     logger.info("Base de datos inicializada correctamente.")
 
 
 data_bg_loaded = load_bg_data()
 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 None
     
     
     return item
     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
 import json
-from typing import List
+from typing import List, Dict
 from fastapi import HTTPException
 from fastapi import HTTPException
 from openai import OpenAI
 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 config.settings import OPENAI_API_KEY
-from models.chat import Message
 from models.user import User
 from models.user import User
 from services.data_service import data_bg_loaded
 from services.data_service import data_bg_loaded
-from logging import getLogger
 from services.openai_service.openai_tools import tools_list, tools
 from services.openai_service.openai_tools import tools_list, tools
 
 
-# Initialize OpenAI client
 openai_client = OpenAI(api_key=OPENAI_API_KEY)
 openai_client = OpenAI(api_key=OPENAI_API_KEY)
-
 logger = getLogger(__name__)
 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:
 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:
     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:
     try:
+        # Primera llamada al modelo
         completion = openai_client.chat.completions.create(
         completion = openai_client.chat.completions.create(
             model="gpt-4o-mini",
             model="gpt-4o-mini",
-            messages=processed_messages,  # type: ignore (OpenAI lib expects list of specific dicts)
+            messages=api_messages,
             temperature=0.7,
             temperature=0.7,
             tools=tools_list,
             tools=tools_list,
             tool_choice="auto",
             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:
                 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:
     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:
 def admin_completion(prompt: str, messages_array: List[dict]) -> str:
+
     """Generate OpenAI admin completion"""
     """Generate OpenAI admin completion"""
+
     messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
     messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
-    
+
+
     if not OPENAI_API_KEY:
     if not OPENAI_API_KEY:
+
         logger.error("Error: OpenAI API key is not configured.")
         logger.error("Error: OpenAI API key is not configured.")
+
         raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
         raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
 
 
+
+
     try:
     try:
+
         completion = openai_client.chat.completions.create(
         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
         response_content = completion.choices[0].message.content
+
         return response_content if response_content else "-1"
         return response_content if response_content else "-1"
+
     except Exception as e:
     except Exception as e:
+
         logger.error(f"Error calling OpenAI for admin completion: {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 config.settings import DEVELOPMENT
 from models.sales import OrderWeb, ItemWeb
 from models.sales import OrderWeb, ItemWeb
 from services.data_service import DataServiceFactory
 from services.data_service import DataServiceFactory
-from models.items import Order
+from models.items import Order, OrderBilling
 from enums.locations import Locations
 from enums.locations import Locations
 
 
 logger = getLogger(__name__)
 logger = getLogger(__name__)
 user_data_service = DataServiceFactory.get_user_service()
 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:
     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"""
     """Send order to printer"""
     logger.info(f"Attempting to print order for table {order.table}")
     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:
     try:
         if not order.items or not order.table:
         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
         # Prepare the order data for printing
         order_data = {
         order_data = {
             "table": order.table,
             "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,
             "totalAmount": order.totalAmount,
             "orderDate": order.orderDate
             "orderDate": order.orderDate
         }
         }
         
         
         logger.info(f"Order data prepared for printing: table={order.table}, items={order.items}, total={order.totalAmount}")
         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
         # Send the order data to the printer service
         response = requests.post(
         response = requests.post(
@@ -84,7 +72,7 @@ def print_order(order: Order, location:Locations ):
 def print_ticket(number_table: int):
 def print_ticket(number_table: int):
     """Send a ticket to the printer"""
     """Send a ticket to the printer"""
     logger.info(f"Attempting to print ticket for table {number_table}")
     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:
     try:
 
 
         
         
@@ -115,13 +103,50 @@ def print_ticket(number_table: int):
         
         
         raise
         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):
 def get_status(location: Locations):
     """Get the status of the printer service"""
     """Get the status of the printer service"""
     logger.info("Checking printer service status")
     logger.info("Checking printer service status")
     
     
-    printer_url = get_printer_url(location, 1)
+    printer_url = get_printer_url()
 
 
     try:
     try:
         response = requests.get(
         response = requests.get(

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff