Selaa lähdekoodia

mejoras varias - diseño, conexion fudo, id interna

latapp 10 kuukautta sitten
vanhempi
sitoutus
00cea4a677
11 muutettua tiedostoa jossa 650 lisäystä ja 148 poistoa
  1. 2 0
      .env
  2. 1 0
      .gitignore
  3. 257 0
      fudo/fudo.py
  4. 0 1
      llm_logs.txt
  5. 38 5
      main.py
  6. 8 8
      products.json
  7. BIN
      public/assets/summer.jpeg
  8. 211 54
      public/index.html
  9. 93 74
      public/js/app.js
  10. 0 2
      public/js/service.js
  11. 40 4
      public/styles.css

+ 2 - 0
.env

@@ -2,3 +2,5 @@ PORT = 6001
 SECRET_KEY = 866B3F5EE90BFED7EDAD0FCB0A9C0FC866F03166C05648478ECEF6148C9E13BEBF8E429BB9B06DCF
 OPENAI_API_KEY = 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
 NODE_ENV = development
+FUDO_API_KEY=NzZAMTEzMzc4
+FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP

+ 1 - 0
.gitignore

@@ -5,4 +5,5 @@ __pycache__/
 .venv
 users.json
 logs.csv
+llm_logs.*
 *.pyc

+ 257 - 0
fudo/fudo.py

@@ -0,0 +1,257 @@
+import math
+import requests
+from rich import print
+import os
+api_token = os.getenv('FUDO_API_KEY')
+api_secret = os.getenv('FUDO_API_SECRET')
+
+def get_token():
+    url = 'https://auth.fu.do/api'
+    data = {
+        "apiKey": api_token,
+        "apiSecret": api_secret
+    }
+    r = requests.post(url, data=data)
+    return r.json()['token']
+    
+def get_categorys():
+    """
+{
+"data": [
+{
+"id": "1",
+"type": "ProductCategory",
+"attributes": {
+"enableOnlineMenu": true,
+"name": "Drinks",
+"preparationTime": 0,
+"position": 50
+},
+"relationships": {
+"kitchen": {
+"data": {
+"id": "1",
+"type": "Kitchen"
+}
+},
+"parentCategory": {
+"data": {
+"id": "1",
+"type": "ProductCategory"
+}
+}
+}
+}
+]
+}"""
+    token = get_token()
+    url = 'https://api.fu.do/v1alpha1/product-categories'
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    return r.json()
+
+def get_product(id_category:int):
+    """
+        Response Example:
+                [{
+            'type': 'Product',
+            'id': '206',
+            'attributes': {
+                'active': True,
+                'code': None,
+                'cost': 364.0,
+                'description': '',
+                'enableOnlineMenu': None,
+                'enableQrMenu': None,
+                'favourite': False,
+                'imageUrl': None,
+                'name': 'Pollo 70 g',
+                'position': 28800000,
+                'preparationTime': None,
+                'price': 1500.0,
+                'sellAlone': True,
+                'stock': None,
+                'stockControl': False
+            },
+            'relationships': {'kitchen': {'data': {'type': 'Kitchen', 'id': '4'}}, 'productCategory': {'data': {'type': 'ProductCategory', 'id': '28'}}, 'productModifiersGroups': {'data': []}, 'productProportions': {'data': []}}
+        }]
+    """
+    url = 'https://api.fu.do/v1alpha1/products/{}'.format(id_category)
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    return r.json()
+
+def get_products():
+    """
+        Response Example:{
+            'type': 'Product',
+            'id': '206',
+            'attributes': {
+                'active': True,
+                'code': None,
+                'cost': 364.0,
+                'description': '',
+                'enableOnlineMenu': None,
+                'enableQrMenu': None,
+                'favourite': False,
+                'imageUrl': None,
+                'name': 'Pollo 70 g',
+                'position': 28800000,
+                'preparationTime': None,
+                'price': 1500.0,
+                'sellAlone': True,
+                'stock': None,
+                'stockControl': False
+            },
+            'relationships': {'kitchen': {'data': {'type': 'Kitchen', 'id': '4'}}, 'productCategory': {'data': {'type': 'ProductCategory', 'id': '28'}}, 'productModifiersGroups': {'data': []}, 'productProportions': {'data': []}}
+        }
+    """
+    url = 'https://api.fu.do/v1alpha1/products'
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    return list(filter(lambda x: x['relationships']['productCategory']['data']['id'] == '1', r.json()['data']))
+
+def get_table(number:int):
+    n_per_page = 10
+    page = math.ceil(number / n_per_page)
+    url = 'https://api.fu.do/v1alpha1/tables?page[number]={}&page[size]={}&include=activeSales&sort=number'.format(page, n_per_page)
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    if r.status_code != 200:
+        print('Error al obtener tablas:' + str(r.json()['errors']))
+        return None
+    try:
+        return list(filter(lambda x: x['attributes']['number'] == number, r.json()['data']))[0]
+    except:
+        print('Error al obtener tabla')
+        print(r.json())
+        return None
+
+def get_sale(sale_id:int):
+
+    url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    r = requests.get(url, headers=headers)
+    if r.status_code != 200:
+        print('Error al obtener tablas:' + str(r.json()['errors']))
+        return None
+    return r.json()
+
+def create_sale(table_id:int):
+    url = 'https://api.fu.do/v1alpha1/sales'
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    data = {
+        "data": {
+            "type": "Sale",
+            "attributes": {
+                "people": 1,
+                "saleType": "EAT-IN",
+                "comment": "Pedido desde la app pedidos express"
+            },
+            "relationships": {
+                "table": {
+                    "data": {
+                        "id": str(table_id),
+                        "type": "Table"
+                    }
+                },
+                "waiter": {
+                    "data": {
+                        "type": "User",
+                        "id": "76"
+                    }
+                }
+            }
+        }
+    }
+    r = requests.post(url, headers=headers, json=data)
+    if r.status_code != 201:
+        print('Error al crear la venta:', r.json())
+        return None
+    return r.json()["data"]
+
+def create_item(product_id:int, quantity:int, sale_id:int, comment:str|None = None):
+    url = 'https://api.fu.do/v1alpha1/items'
+    token = get_token()
+    headers = {
+        'Authorization': 'Bearer ' + token
+    }
+    data = {
+        "quantity": quantity,
+        "origin": "MOBILE",  
+        "comment": "[DESARROLLO NO SACAR]Pedido desde pedidos express" + (f" - {comment}" if comment else ""),
+    }
+    data = {
+        "data":{
+            "type": "Item",
+            "attributes": data,
+            "relationships": {
+                "product": {
+                    "data": {
+                        "type": "Product",
+                        "id": str(product_id)
+                    }
+                },
+                "sale": {
+                    "data": {
+                        "type": "Sale",
+                        "id": str(sale_id)
+                    }
+                }
+            },
+        }
+    }
+    r = requests.post(url, headers=headers, json=data)
+    if r.status_code != 201:
+        print(r.json())
+        return None
+    return r.json()["data"]
+
+def get_active_sale(table):
+    data = table['relationships']['activeSales']['data']
+    if len(data) == 0:
+        return None
+    return data[0]
+
+if __name__ == "__main__":
+    table = get_table(107)
+    if table is None:
+        print('No se pudo obtener la mesa')
+        exit()
+    activeSale = get_active_sale(table)
+    if not activeSale:
+        print('No hay una venta activa para la mesa')
+        activeSale = create_sale(table['id'])
+        if activeSale is None:
+            print('No se pudo crear la venta')
+            exit()
+    else:
+        activeSale = activeSale[0]
+    print('Venta activa:', activeSale['id'])
+
+
+"""
+Intrucciones para hacer un pedido:
+
+1. Obtener el token de autenticación con `get_token()`.
+2. obtener la mesa con `get_table(numero_de_mesa)`.
+3. ver si tiene una activeSale, en caso contrario crear una con `create_sale(id_mesa)`.
+4. agregar los items a la venta con `create_item(id_producto, cantidad, id_venta, comentario)`.
+"""

+ 0 - 1
llm_logs.txt

@@ -1 +0,0 @@
-asd: ¡Parece que hubo un pequeño error! 😄 Si tienes alguna pregunta sobre nuestro menú o necesitas recomendaciones, ¡estaré encantado de ayudarte! 🍻✨

+ 38 - 5
main.py

@@ -17,10 +17,10 @@ from impresora.order import *
 
 import smtplib
 from email.message import EmailMessage
-
 # Load environment variables from .env file
 load_dotenv()
-#pruebitas4
+import fudo.fudo as fd
+
 
 
 # Configuration
@@ -59,6 +59,22 @@ app.add_middleware(
 BG_DATA_PATH = os.path.join(os.path.dirname(__file__), 'data.json')
 PRODUCTS_PATH = os.path.join(os.path.dirname(__file__), 'products.json')
 
+def add_product_to_fudo(product_id: int, quantity: int, table_number:int, comment: str | None = None):
+    table = fd.get_table(table_number)
+    if not table:
+        print(f"Error: Table {table_number} not found.")
+        return None
+    activeSale = fd.get_active_sale(table)
+    if not activeSale:
+        activeSale = fd.create_sale(table['id'])
+        if not activeSale:
+            print(f"Error: Could not create sale for table {table_number}.")
+            return None
+    item = fd.create_item(product_id, quantity, activeSale['id'], comment)
+    if not item:
+        print(f"Error: Could not create item for product {product_id}.")
+        return None
+    return item
 
 def send_email():
     # Datos del remitente
@@ -248,6 +264,7 @@ async def protect_chat_api(
 @app.get("/api/get_products", summary="Get products")
 async def get_products():
     return JSONResponse({"products": all_products})
+
 # --- API Endpoints ---
 @app.get("/api/chat/init-chat", summary="Initialize chat and get anti-abuse token")
 async def init_chat(request: Request):
@@ -278,26 +295,42 @@ async def exists_user(request: UserCodeRequest):
             "success": True,
             "userName": request.user_code
         })
+    
 @app.post("/api/printer/order", summary="Printer order", dependencies=[Depends(protect_chat_api)])
 async def printer_order(order: OrderWeb):
     print("Printer order received")
     print(order)
     items = order.items
     table = order.table
+    if not items or not table:
+        return JSONResponse(status_code=400, content={"message": "Items and table are required."})
+    if not isinstance(table, int):
+        return JSONResponse(status_code=400, content={"message": "Table must be an integer."})
+    product_errors = []
+    for item in items:
+        product = add_product_to_fudo(item.id, item.quantity, table)
+        if not product:
+            product_errors.append(f"Error adding product {item.id} to table {table}.")
+    if product_errors:
+        return JSONResponse(status_code=424, content={"message": "Error adding products to table.", "errors": product_errors})
+    # en caso de que no alla error, imprimimos el pedido
     printer = PrinterUSB(0xfe6,0x811e)
     print_order = Order(order.customerName,[Item(item.name, item.price, item.quantity) for item in items])
     try:
         printer.print_order(print_order, table)
     except:
+        #Si la impresora no esta conectada, enviamos un correo
         send_email()
         return JSONResponse(status_code=424, content={"message": "No se pudo imprimir el Pedido, impresora desconectada"})
+    # Logs de pedidos
     if not os.path.exists('logs.csv'):
         with open('logs.csv', 'w', newline='') as f:
             writer = csv.writer(f)
             writer.writerow(['userName', 'table', 'orderDate', 'items'])
-    with open('logs.csv', 'a', newline='') as f:
-        writer = csv.writer(f)
-        writer.writerow([order.customerName, order.table, order.orderDate, list(map(lambda item: item.name, items))])
+    else:
+        with open('logs.csv', 'a', newline='') as f:
+            writer = csv.writer(f)
+            writer.writerow([order.customerName, order.table, order.orderDate, list(map(lambda item: item.name, items))])
 
 
 @app.post("/api/chat/completions",

+ 8 - 8
products.json

@@ -1,6 +1,6 @@
 [
   {
-    "id": 1,
+    "id": 6,
     "name": "Burlesque",
     "type": "Cerveza",
     "description": "Cerveza Ale ámbar, 5.0º - IBU 12",
@@ -8,14 +8,14 @@
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/6"
   },
   {
-    "id": 2,
+    "id": 15,
     "name": "Bendicion Gitana",
     "type": "Cerveza",
     "description": "Pale Ale - 5,0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/15"
   },{
-    "id":3,
+    "id":163,
     "name":"Hoppy Mosh",
     "type":"Cerveza",
     "description":"IPA - 6.0º - IBU 38",
@@ -23,21 +23,21 @@
     "image":"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/163"
   },
   {
-    "id": 4,
+    "id": 12,
     "name": "Black Mamba",
     "type": "Cerveza",
     "description": "Porter - 6.0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/12"
   },{
-    "id": 5,
+    "id": 665,
     "name": "Marzen",
     "type": "Cerveza",
     "description": " Estilo Märzenbier, 5.0º - IBU 22",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/665"
   },{
-    "id": 6,
+    "id": 1,
     "name": "24k Gold",
     "type": "Cerveza",
     "description": "Golden Ale - 4,5º - IBU 20",
@@ -45,11 +45,11 @@
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/1"
   },
   {
-    "id": 7,
+    "id": 655,
     "name": "🌟 Summer Klein",
     "type": "Coctel",
     "description": "Gin Juno, jugo de naranja, maracuya, limon y Ginger Beer",
     "price": 6500,
-    "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/655"
+    "image": "/assets/summer.jpeg"
   }
 ]

BIN
public/assets/summer.jpeg


+ 211 - 54
public/index.html

@@ -9,41 +9,104 @@
   <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">
   <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
-
+   <!--Tailwind-->
+   <script>
+        tailwind.config = {
+            theme: {
+                extend: {
+                    colors: {
+                        'custom-dark': '#101419',
+                        'custom-dark-hover': '#37404a',
+                        'gray-50': '#f9fafb',
+                        'gray-100': '#f3f4f6',
+                    }
+                }
+            }
+        }
+    </script>
   <!-- Markdown -->
   <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-
-  <!-- Lógica principal -->
   <script src="js/app.js" type="module"></script>
   <link rel="stylesheet" href="styles.css">
+  <!-- Animaciones -->
   <style>
-    @keyframes slideIn   {from {transform:translateY(-8px); opacity:0;} to {transform:translateY(0); opacity:1;}}
-    @keyframes slideOut  {from {transform:translateY(0);   opacity:1;} to {transform:translateY(-8px); opacity: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%);
+      }
+    }
   </style>
 </head>
 
-<body class="min-h-screen flex flex-col bg-gray-50 overflow-x-hidden"
+<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="fixed top-0 inset-x-0 z-10 bg-gray-50 p-4 flex justify-center items-center border-b border-gray-200">
-    <h1 class="text-[26px] font-bold text-[#101419] tracking-tight">
-      Biergarten Klein
+  <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">
+    <h1 id="mainTitle" class="text-[26px] font-bold text-[#101419] tracking-tight">
+      KleinBot
     </h1>
   </header>
 
   <!-- ---------- MAIN  ---------- -->
-  <main class="flex-1 pt-16 pb-20">  
+  <main class="relative flex-1 flex flex-col min-h-0 overflow-x-hidden">  
     <!-- ===== MENÚ tab ===== -->
-    <section id="menuTab" data-tab>
-      <div class="px-4 pt-4 pb-3 "></div>
-        <h2 class="text-[22px] mx-4 font-bold text-[#101419]">
-          PIDE TU SHOP EXPRESS  ⚡🍺
+    <section id="menuTab" data-index="0" class=" min-h-0 overflow-y-auto h-full" data-tab>
+      <div class="pt-4 pb-3 ">
+        <h2 class="text-[19px] mx-4 font-bold text-[#101419]">
+          Pide tu shop express 🍺
         </h2>
         <p class="product-type mx-4 text-[#58728d] text-sm pb-4 mb-4 border-b border-gray-200">*solo lo más vendido</p>
       </div>
-      <div class="px-4">
-        <ul id="productList" class="space-y-6"></ul>
+      <div class="px-4 overflow-y-auto">
+      <ul id="productList" class="space-y-6"></ul>
       </div>
 
       <template id="product-card-template">
@@ -74,24 +137,62 @@
           <div class="product-image flex-1 aspect-video bg-cover bg-center rounded-xl"></div>
         </li>
       </template>
-    </section>
+    </section>  
 
     <!-- ===== CHAT ===== -->
-    <section id="chatTab" data-tab class="hidden flex flex-col h-full">
-      <div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-3 text-sm leading-relaxed"></div>
-      <div id="aiLoadingIndicator" class="hidden px-4 py-2 text-center text-xs text-gray-500">Pensando…</div>
-      <form class="flex gap-2 p-3 border-t border-gray-200" onsubmit="event.preventDefault();">
-        <input id="chatInput" class="flex-1 text-sm px-3 py-2 rounded-md border border-gray-300 focus:outline-none text-neutral-800" autocomplete="off"
-               placeholder="Escribe tu mensaje...">
-        <button id="sendChatButton"
-                class="bg-[#101419] hover:bg-[#37404a] text-white px-3 py-2 rounded-md text-sm">
-          Enviar
-        </button>
-      </form>
+    <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col flex-1 min-h-0">
+        <!-- Contenedor de mensajes que puede crecer y hacer scroll -->
+        <div id="chatMessages" class="flex-1 overflow-y-auto px-5 md:px-8 py-4 flex flex-col gap-4">
+            <!-- Sugerencias y Bajada -->
+            <div class="text-center text-gray-600 px-5 text-sm leading-relaxed max-w-full">
+                <h2 class="text-md text-gray-500 mt-5">Conversa con nuestro asistente IA: descubre nuestras cervezas 🍻 y sugiere mejoras para nuestra app ✨<span class="mx-0.5">🍻</span></h2>
+                <!-- Ideas de mensajes -->
+                <div id="chatSuggestions" class="grid grid-cols-1 gap-3 max-w-2xl mx-auto mt-5">
+                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
+                        <div class="text-lg mb-1">🍺</div>
+                        <div class="chat-suggestion text-gray-400 font-medium text-xs">¿Que me puedes contar de la burlesque?</div>
+                    </div>
+                    
+                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
+                        <div class="text-lg mb-1">⁉️</div>
+                        <div class="chat-suggestion text-gray-400 font-medium text-xs">Mi sugerencia para la aplicacion es...</div>
+                    </div>  
+                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
+                        <div class="text-lg mb-1">🍕</div>
+                        <div class="chat-suggestion text-gray-400 font-medium text-xs">¿Qué pizza queda bien con la hoppy mosh?</div>
+                    </div>
+                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
+                        <div class="text-lg mb-1">📱</div>
+                        <div class="chat-suggestion text-gray-400 font-medium text-xs">La orden no se envio bien</div>
+                    </div>               
+                </div>
+            </div>
+        </div>
+        
+        <!-- Indicador de carga - FIJO arriba del input -->
+        <div id="aiLoadingIndicator" class="hidden flex px-5 py-3 text-left text-sm text-gray-500 italic flex-shrink-0">
+            Pensando
+        </div>
+        
+        <!-- Input del chat - FIJO en la parte inferior -->
+        <div class="px-4 py-4 md:px-5 md:py-5 bg-white border-t border-gray-200 flex-shrink-0">
+            <form id="chatForm" class="flex items-center bg-gray-50 border border-gray-300 rounded-3xl px-5 py-1 transition-colors focus-within:border-gray-400">
+                <input 
+                    id="chatInput" 
+                    class="flex-1 sticky flex-shrink-0 bg-transparent border-none outline-none text-gray-900 text-sm py-3 placeholder-gray-500"
+                    autocomplete="off"
+                    placeholder="Pregunta lo que quieras"
+                    maxlength="2000"
+                >
+                <button id="sendChatButton" type="submit" class="bg-custom-dark hover:bg-custom-dark-hover text-white border-none rounded-2xl px-4 py-2.5 text-sm transition-colors ml-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
+                    Enviar
+                </button>
+            </form>
+        </div>
     </section>
 
     <!-- ===== CARRITO ===== -->
-    <section id="cartTab" data-tab class="hidden flex flex-col h-full">
+    <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>
@@ -106,33 +207,22 @@
         </div>
         <button id="checkoutButton"
                 class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 text-white py-2 rounded-md"
+                onclick="processOrder()"
                 disabled>
-          Finalizar Pedido
+          Envia tu orden
         </button>
       </footer>
     </section>
   </main>
 
   <!-- ---------- NAVBAR ---------- -->
-  <footer class="fixed bottom-0 inset-x-0 z-10 border-t border-gray-200 bg-gray-50 px-4 py-2">
+  <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="chatTab" class="tab-btn flex-1 flex flex-col items-center text-[#58728d]">
-        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
-             viewBox="0 0 256 256" class="h-8">
-          <path
-            d="M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128ZM84,116a12,12,0,1,0,12,12A12,12,0,0,0,84,116Zm88,0a12,12,0,1,0,12,12A12,12,0,0,0,172,116Zm60,12A104,104,0,0,1,79.12,219.82L45.07,231.17a16,16,0,0,1-20.24-20.24l11.35-34.05A104,104,0,1,1,232,128Z" />
-        </svg>
-        <span class="text-xs font-medium">Chat</span>
+      <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="menuTab" class="tab-btn flex-1 flex flex-col items-center text-[#101419]">
-         <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
-             viewBox="0 0 256 256" class="h-8">
-          <path
-            d="M56,128a16,16,0,1,1-16-16A16,16,0,0,1,56,128ZM40,48A16,16,0,1,0,56,64,16,16,0,0,0,40,48Zm0,128a16,16,0,1,0,16,16A16,16,0,0,0,40,176Zm176-64H88a8,8,0,0,0-8,8v16a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V120A8,8,0,0,0,216,112Zm0-64H88a8,8,0,0,0-8,8V72a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V56A8,8,0,0,0,216,48Zm0,128H88a8,8,0,0,0-8,8v16a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V184A8,8,0,0,0,216,176Z" />
-        </svg>
-        <span class="text-xs font-medium">Menú</span>
-      </button>
-      <button data-target="cartTab" class="tab-btn flex-1 flex flex-col items-center text-[#58728d]">
+      
+      <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"
@@ -143,18 +233,25 @@
         </div>
         <span class="text-xs font-medium">Carrito</span>
       </button>
+      <button data-target="chatTab" data-title="KleinBot" class="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="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>
+        <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"></div>
+              rounded-md px-4 py-2 shadow-lg opacity-0 pointer-events-none z-50
+              origin-left">
+  </div>
 
   
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
-     class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
+     class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50">
   <div class="bg-white w-full max-w-sm p-6 rounded-lg space-y-4 text-center">
     <h2 class="text-xl font-bold">¡Bienvenido!</h2>
     <p class="text-sm text-gray-600">
@@ -179,13 +276,69 @@
 
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
   <script>
+
+    const animation_time = 200
+    let transitioning = false;
     // conmutar pestañas
-    document.querySelectorAll('.tab-btn').forEach(btn => {
+    const buttons = document.querySelectorAll('.tab-btn')
+    buttons.forEach(btn => {
       btn.addEventListener('click', () => {
         const target = btn.dataset.target;
-        document.querySelectorAll('[data-tab]').forEach(tab => {
-          tab.classList.toggle('hidden', tab.id !== target);
+        const active = document.querySelector(':not(.hidden)[data-tab]');
+        const activeIndex = active.dataset.index;
+        const to = document.querySelector(`#${target}[data-tab]`);
+        const toIndex = to.dataset.index;
+        const height = to.offsetHeight;
+        
+        if (activeIndex === toIndex || transitioning) return;
+        buttons.forEach(button => {
+          button.classList.remove('active')
+        })
+        btn.classList.add('active')
+        
+        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');
+        // Animate tab transition
+        if (activeIndex < toIndex) {
+          // Slide left
+          active.classList.add(`animate-[slideLeft_${animation_time}ms_ease-out]`);
+          to.classList.add(`animate-[slideRightIn_${animation_time}ms_ease-out]`);
+        } else if (activeIndex > toIndex) {
+          // Slide right
+          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);
+
+        // Update header title if needed
+        const title = btn.dataset.title;
+        if (title) {
+          document.getElementById('mainTitle').textContent = title;
+        }
+
+
+
+
       });
     });
 
@@ -196,8 +349,12 @@
       toast.style.animation = 'none';      // reset
       void toast.offsetWidth;              // reflow
       toast.style.opacity = '1';
-      toast.style.animation = 'slideIn 0.2s ease-out, slideOut 0.2s ease-in 0.8s forwards';
+      toast.style.animation = 'popup 1s ease-out';
+      toast.addEventListener('animationend', () => {
+        toast.style.animation = 'none';
+        toast.style.opacity = '0';
+      });
     };
   </script>
 </body>
-</html>
+</html>

+ 93 - 74
public/js/app.js

@@ -1,29 +1,33 @@
 import { initializeChat as serviceInitializeChat, sendMessage as serviceSendMessage, sendOrder, getProducts, existsUser } from './service.js';
-
+// --- Variables de Usuario ---
 let userName = '';
 let userTable = null;
 
+// --- Datos de Productos y Carrito ---
 let products = [];
-let itsEmpty = true;
 let cart = [];
+let itsEmpty = true;
+
+// --- Historial de Chat ---
 let chatHistory = [
     { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" }
 ];
 
-// --- Elementos del DOM ---
+// --- Elementos del DOM: Productos y Carrito ---
 const productListElement = document.getElementById("productList");
 const cartItemsElement = document.getElementById("cartItems");
 const cartTotalElement = document.getElementById("cartTotal");
 const emptyCartTextElement = document.getElementById("emptyCartText");
 const checkoutButton = document.getElementById("checkoutButton");
 const originalCheckoutButtonText = checkoutButton ? checkoutButton.textContent : "Finalizar Pedido";
-console.log(originalCheckoutButtonText)
+const cartCountElement = document.getElementById("cartCount");
+
+// --- Elementos del DOM: Chat ---
 const chatMessagesElement = document.getElementById("chatMessages");
 const chatInputElement = document.getElementById("chatInput");
-const sendChatButton = document.getElementById("sendChatButton");
+const chatForm = document.getElementById("chatForm");
 const aiLoadingIndicator = document.getElementById("aiLoadingIndicator");
-
-const cartCountElement = document.getElementById("cartCount");
+const chatSuggestionsElement = document.getElementById("chatSuggestions");
 
 // --- Loader Global ---
 let globalLoaderElement = null;
@@ -54,12 +58,11 @@ function hideGlobalLoader() {
     }
 }
 
-
 function formatPrice(price) {
     return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
 }
 
-async function processOrder() {
+window.processOrder = async () => {
     if (cart.length === 0) return;
     showGlobalLoader();
     if (checkoutButton) {
@@ -75,7 +78,6 @@ async function processOrder() {
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
         };
-        console.log("Enviando Pedido:", orderData);
         await sendOrder(orderData);
         alert("Pedido enviado con éxito.");
         cart = []
@@ -83,8 +85,8 @@ async function processOrder() {
     } 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;
@@ -125,7 +127,6 @@ async function renderProducts() {
     });
 }
 
-
 window.addToCart = async (productId, buttonElement = null) => {
     const product = products.find(p => p.id === productId);
     if (!product) return;
@@ -139,15 +140,11 @@ window.addToCart = async (productId, buttonElement = null) => {
     if (buttonElement) {
         const originalHTML = buttonElement.innerHTML;
         buttonElement.textContent = "✔ Agregado!";
-        buttonElement.classList.replace('bg-accent-red', 'bg-green-500');
-        buttonElement.classList.remove("hover:bg-red-700");
         buttonElement.disabled = true;
         setTimeout(() => {
             buttonElement.innerHTML = originalHTML;
-            buttonElement.classList.replace('bg-green-500', 'bg-accent-red');
-            buttonElement.classList.add("hover:bg-red-700");
             buttonElement.disabled = false;
-        }, 1500);
+        }, 300);
     }
     updateCartDisplay();
     // Dentro de window.addToCart (después de updateCartDisplay())
@@ -183,12 +180,12 @@ function updateCartDisplay() {
             cartCountElement.animate([
                 { transform: 'scale(0)' },
                 { transform: 'scale(1)' }
-            ],{
+            ], {
                 duration: 300,
                 iterations: 1,
                 easing: 'ease-in-out'
             })
-        }else {
+        } else {
             cartCountElement.animate([
                 { transform: 'scale(1) rotate(0deg)' },
                 { transform: 'scale(1.2) rotate(180deg)' },
@@ -241,12 +238,20 @@ function displayChatMessage(sender, message) {
     chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
 }
 
+async function sendSuggestion(suggestion) {
+    if (!chatInputElement || !aiLoadingIndicator) return;
+    chatInputElement.value = suggestion;
+    chatInputElement.focus();
+
+}
+
 async function sendMessageToAI() {
     if (!chatInputElement || !aiLoadingIndicator) return;
     const userInput = chatInputElement.value.trim();
     if (!userInput) return;
 
     displayChatMessage("user", userInput);
+    chatSuggestionsElement.classList.add("hidden");
     chatInputElement.value = '';
     aiLoadingIndicator.classList.remove("hidden");
 
@@ -264,7 +269,7 @@ async function sendMessageToAI() {
                     displayChatMessage("ai", "Hubo un problema al enviar el mensaje.");
                 }
             } else {
-                 displayChatMessage("ai", "Fallo la reconexión. Por favor, refresca la página.");
+                displayChatMessage("ai", "Fallo la reconexión. Por favor, refresca la página.");
             }
         } else if (response.assistantResponse) {
             chatHistory = response.messageList;
@@ -279,67 +284,81 @@ async function sendMessageToAI() {
     }
 }
 
-// --- Event Listeners ---
-document.addEventListener("DOMContentLoaded", async () => {
-
-    createGlobalLoader();
-    updateCartDisplay();
-
-    async function initializeApp() {
-        await renderProducts();
-        try {
-            if (await serviceInitializeChat()) {
-                console.log("Chat AI Asistente inicializado exitosamente.");
-            } else {
-                console.warn("Chat AI no pudo inicializarse.");
-                displayChatMessage("ai", "No se pudo conectar con el Chef IA en este momento.");
-            }
-        } catch (error) {
-            console.error("Error durante la inicialización del Chat AI:", error);
-            displayChatMessage("ai", "Error al iniciar la IA.");
+async function initializeApp() {
+    await renderProducts();
+    try {
+        if (await serviceInitializeChat()) {
+        } else {
+            console.warn("Chat AI no pudo inicializarse.");
+            displayChatMessage("ai", "No se pudo conectar con el Chef IA en este momento.");
         }
+    } catch (error) {
+        console.error("Error durante la inicialización del Chat AI:", error);
+        displayChatMessage("ai", "Error al iniciar la IA.");
     }
-    if (checkoutButton) checkoutButton.addEventListener("click", processOrder);
 
-    /* ---------- MANEJO DEL POPUP INICIAL ---------- */
-    const sessionModal      = document.getElementById('sessionModal');
-    const sessionAcceptBtn  = document.getElementById('sessionAcceptBtn');
-    const tableInput        = document.getElementById('tableInput');
-    const clientCodeInput   = document.getElementById('clientCodeInput');
+    // #region Sugerencias
+    const chatSuggestions = Array.from(chatSuggestionsElement.children);
 
-    sessionAcceptBtn.addEventListener('click', async () => {
-    const mesa   = parseInt(tableInput.value, 10);
-    const codigo = clientCodeInput.value.trim();
-
-    if (!mesa || !codigo) {
-        alert('Por favor completa ambos campos.');
-        return;
-    }
-    const existUser = await existsUser(codigo);
-    if (!existUser.success) {
-        alert('El código de cliente no existe.');
-        return;
-    }
-    userName = existUser.userName;
-    //destruye el modal
-    sessionModal.remove();
-    startSession(mesa);                
+    chatSuggestions.forEach(suggestion => {
+        suggestion.addEventListener("click", () => {
+            sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
+        });
     });
+}
 
-    /* ---- FUNCIÓN que recibe los dos parámetros ---- */
-    function startSession(mesa) {
-    userTable = mesa;
-    initializeApp();   
-    }
+function popupConfig(){
+    const sessionModal = document.getElementById('sessionModal');
+    const sessionAcceptBtn = document.getElementById('sessionAcceptBtn');
+    const tableInput = document.getElementById('tableInput');
+    const clientCodeInput = document.getElementById('clientCodeInput');
+    sessionModal.classList.remove('hidden');
+    sessionAcceptBtn.addEventListener('click', async () => {
+        const mesa = parseInt(tableInput.value, 10);
+        const codigo = clientCodeInput.value.trim();
 
+        if (!mesa || !codigo) {
+            alert('Por favor completa ambos campos.');
+            return;
+        }
+        const existUser = await existsUser(codigo);
+        if (!existUser.success) {
+            alert('El código de cliente no existe.');
+            return;
+        }
+        userName = existUser.userName;
+        //destruye el modal
+        sessionModal.remove();
+        userTable = mesa;
+        initializeApp();
     });
+}
+
+function chatConfig() {
+  if (!chatForm) return;
+    chatForm.addEventListener("submit", (event) => {
+        event.preventDefault();
+        if (chatInputElement.value.trim() === "") return;
 
-    if (sendChatButton) sendChatButton.addEventListener("click", sendMessageToAI);
-    if (chatInputElement) {
-        chatInputElement.addEventListener("keypress", (event) => {
-            if (event.key === "Enter") {
-                event.preventDefault();
-                sendMessageToAI();
+        sendMessageToAI();
+        chatInputElement.addEventListener("input", () => {
+            if (chatInputElement.value.trim() === "") {
+                chatSuggestionsElement.classList.remove("hidden");
+            } else {
+                chatSuggestionsElement.classList.add("hidden");
             }
         });
-}
+    });
+}
+
+// --- Event Listeners ---
+document.addEventListener("DOMContentLoaded", async () => {
+
+    createGlobalLoader();
+    updateCartDisplay();
+    chatConfig()
+    popupConfig();
+    // initializeApp();
+
+});
+

+ 0 - 2
public/js/service.js

@@ -25,7 +25,6 @@ async function sendMessage(message, messageList, userName) {
   }
   messageList.push({ role: "user", content: message });
   const cuerpo = { messages: messageList, user: userName }
-  console.log(cuerpo)
   const response = await fetch("/api/chat/completions", {
     method: "POST",
     headers: {
@@ -49,7 +48,6 @@ async function sendMessage(message, messageList, userName) {
 }
 
 async function sendOrder(order) {
-  console.log(order)
   try {
     const response = await fetch("/api/printer/order", {
       method: "POST",

+ 40 - 4
public/styles.css

@@ -24,7 +24,6 @@ body {
 
 .accent-red { color: var(--accent-red); }
 .bg-accent-red { background-color: var(--accent-red); }
-.bg-accent-red.hover\:bg-red-700:hover { background-color: var(--accent-red-hover); }
 .border-accent-red { border-color: var(--accent-red); }
 
 header.bg-black { background-color: var(--background-header); }
@@ -81,7 +80,7 @@ footer.bg-black { background-color: var(--background-header); }
     border-bottom-right-radius: 5px;
 }
 .chat-bubble-ai {
-    background-color: #ffffff;
+    /* background-color: #ffffff; */
     margin-right: auto;
     border-bottom-left-radius: 5px;
     & p {
@@ -164,9 +163,14 @@ footer.bg-black { background-color: var(--background-header); }
     border-radius: 50%;
     background: red;
     color: white;
-    
+}
 
+.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); }
@@ -181,4 +185,36 @@ footer.bg-black { background-color: var(--background-header); }
     .chat-content-area {
         height: 40vh; /* Altura específica para el contenido del chat */
     }
-}
+}
+/* Scrollbar personalizado */
+#chatMessages::-webkit-scrollbar {
+    width: 6px;
+}
+
+#chatMessages::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+#chatMessages::-webkit-scrollbar-thumb {
+    background-color: #d1d5db;
+    border-radius: 3px;
+}
+
+#chatMessages::-webkit-scrollbar-thumb:hover {
+    background-color: #9ca3af;
+}
+
+#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: "...."; }
+}