Quellcode durchsuchen

refactor: add not hardcode messages, and exptime

latapp vor 9 Monaten
Ursprung
Commit
a298bdca49

+ 1 - 1
app.py

@@ -39,7 +39,7 @@ def setup_routes(app: FastAPI):
     
     # Static routes
     from fastapi.responses import HTMLResponse
-    app.add_api_route("/express", static.serve_app_html, methods=["GET"], 
+    app.add_api_route("/", static.serve_app_html, methods=["GET"], 
                      response_class=HTMLResponse, include_in_schema=False)
     app.add_api_route("/register", static.serve_register_html, methods=["GET"],
                      response_class=HTMLResponse, include_in_schema=False)

+ 255 - 2
config/mails.py

@@ -1,13 +1,13 @@
 
 
 REGISTER_MAIL = {
-    "subject": "Welcome to Pedidos Express",
+    "subject": "Bienvenido a Pedidos Express",
     "body": """
 <html>
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>¡Bienvenido a KlowApp!</title>
+    <title>¡Bienvenido a {app_name}!</title>
     <style>
         body {{
             margin: 0;
@@ -258,5 +258,258 @@ REGISTER_MAIL = {
 PRINTER_DISCONNECTED_MAIL = {
     "subject": "Printer Disconnected",
     "body": """
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Alerta: Impresora Desconectada - KlowApp</title>
+</head>
+<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f3f4f6;">
+    <table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 20px;">
+        <tr>
+            <td align="center">
+                <table width="600" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
+                    
+                    <!-- Header -->
+                    <tr>
+                        <td style="background-color: #dc2626; color: white; padding: 32px; text-align: center;">
+                            <h1 style="margin: 0; font-size: 28px; font-weight: bold;">🚨 Alerta del Sistema</h1>
+                            <p style="margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">Notificación para Desarrolladores</p>
+                        </td>
+                    </tr>
+                    
+                    <!-- Content -->
+                    <tr>
+                        <td style="padding: 32px;">
+                            
+                            <!-- Alert Message -->
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <tr>
+                                    <td style="text-align: center; margin-bottom: 32px;">
+                                        <h2 style="color: #dc2626; font-size: 24px; margin: 0 0 12px 0; font-weight: bold;">Impresora Desconectada</h2>
+                                        <p style="color: #6b7280; font-size: 16px; line-height: 1.6; margin: 0;">Se ha detectado una desconexión en el sistema de impresión de {restaurant_name}.</p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Alert Details -->
+                            <table width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;">
+                                <tr>
+                                    <td style="background-color: #fef2f2; border-radius: 8px; padding: 24px; border-left: 4px solid #dc2626;">
+                                        <h3 style="color: #dc2626; font-size: 18px; font-weight: 600; margin: 0 0 16px 0;">Detalles del Problema</h3>
+                                        <ul style="list-style: none; padding: 0; margin: 0;">
+                                            <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
+                                                <span style="color: #dc2626; font-weight: bold; position: absolute; left: 0;">🖨️</span>
+                                                <strong>Impresora:</strong> {printer_name}
+                                            </li>
+                                            <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
+                                                <span style="color: #dc2626; font-weight: bold; position: absolute; left: 0;">⏰</span>
+                                                <strong>Hora de desconexión:</strong> {timestamp}
+                                            </li>
+                                        </ul>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Priority Section -->
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <tr>
+                                    <td style="background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); border-radius: 12px; padding: 32px; text-align: center; margin: 32px 0; color: white;">
+                                        <p style="font-size: 18px; margin: 0 0 16px 0; opacity: 0.95;">Estado del Sistema</p>
+                                        <div style="font-size: 32px; font-weight: bold; margin: 16px 0; border: 3px solid white; border-radius: 8px; padding: 16px; display: inline-block; min-width: 200px; background-color: rgba(255,255,255,0.1);">
+                                            🔴 CRÍTICO
+                                        </div>
+                                        <p style="font-size: 16px; margin: 16px 0 0 0; opacity: 0.9;">Requiere atención inmediata</p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Impact Assessment -->
+                            <table width="100%" cellpadding="0" cellspacing="0" style="margin: 32px 0;">
+                                <tr>
+                                    <td style="background-color: #fff7ed; border-radius: 8px; padding: 24px; border-left: 4px solid #f59e0b;">
+                                        <h3 style="color: #92400e; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">Impacto Potencial</h3>
+                                        <p style="color: #374151; font-size: 14px; line-height: 1.5; margin: 0;">
+                                            ⚠️ Los pedidos no se están imprimiendo automáticamente<br>
+                                            ⚠️ El restaurante puede estar perdiendo órdenes<br>
+                                            ⚠️ Se requiere intervención manual inmediata<br>
+                                            ⚠️ Posible pérdida de ingresos y experiencia del cliente
+                                        </p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Action Items -->
+                            <table width="100%" cellpadding="0" cellspacing="0" style="margin: 32px 0;">
+                                <tr>
+                                    <td style="background-color: #f0f9ff; border-radius: 8px; padding: 24px; border-left: 4px solid #3b82f6;">
+                                        <h3 style="color: #1e40af; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">Acciones Requeridas</h3>
+                                        <p style="color: #374151; font-size: 14px; line-height: 1.5; margin: 0;">
+                                            1. <strong>Verificar</strong> la conexión de red de la impresora<br>
+                                            2. <strong>Contactar</strong> al restaurante para confirmar el estado<br>
+                                            3. <strong>Reiniciar</strong> el servicio de impresión si es necesario<br>
+                                            4. <strong>Monitorear</strong> la reconexión y funcionalidad<br>
+                                            5. <strong>Documentar</strong> la resolución en el sistema
+                                        </p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                        </td>
+                    </tr>
+                    
+                    <!-- Footer -->
+                    <tr>
+                        <td style="background-color: #f9fafb; padding: 24px 32px; text-align: center; border-top: 1px solid #e5e7eb;">
+                            <p style="color: #6b7280; font-size: 14px; margin: 0; line-height: 1.5;">
+                                <strong>Express Klein - Sistema de Monitoreo</strong><br>
+                                Alerta automática generada el {timestamp}<br>
+                                Este es un email automático del sistema de monitoreo.<br>
+                                © 2025 Express Klein. Todos los derechos reservados.
+                            </p>
+                        </td>
+                    </tr>
+                    
+                </table>
+            </td>
+        </tr>
+    </table>
+</body>
+</html>
 """
+}
+
+PIN_RECOVERTY_MAIL = {
+    "subject": "Recuperación de PIN | Pedidos Express",
+    "body": """
+        <!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Recupera tu PIN </title>
+</head>
+<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f3f4f6;">
+    <table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 20px;">
+        <tr>
+            <td align="center">
+                <table width="600" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
+                    
+                    <!-- Header -->
+                    <tr>
+                        <td style="background-color: #101419; color: white; padding: 32px; text-align: center;">
+                            <h1 style="margin: 0; font-size: 28px; font-weight: bold;">¡Hola {name}!</h1>
+                            <p style="margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">Recuperación de PIN</p>
+                        </td>
+                    </tr>
+                    
+                    <!-- Content -->
+                    <tr>
+                        <td style="padding: 32px;">
+                            
+                            <!-- Welcome Message -->
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <tr>
+                                    <td style="text-align: center; margin-bottom: 32px;">
+                                        <h2 style="color: #101419; font-size: 24px; margin: 0 0 12px 0; font-weight: bold;">¿Olvidaste tu PIN?</h2>
+                                        <p style="color: #6b7280; font-size: 16px; line-height: 1.6; margin: 0;">No te preocupes, puedes crear un nuevo PIN de acceso a {app_name}.</p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Security Info -->
+                            <table width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;">
+                                <tr>
+                                    <td style="background-color: #f9fafb; border-radius: 8px; padding: 24px;">
+                                        <h3 style="color: #101419; font-size: 18px; font-weight: 600; margin: 0 0 16px 0; text-align: center;">Información Importante</h3>
+                                        <ul style="list-style: none; padding: 0; margin: 0;">
+                                            <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
+                                                <span style="color: #10b981; font-weight: bold; position: absolute; left: 0;">✓</span>
+                                                Este enlace es válido por 24 horas
+                                            </li>
+                                            <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
+                                                <span style="color: #10b981; font-weight: bold; position: absolute; left: 0;">✓</span>
+                                                Podrás elegir tu nuevo PIN
+                                            </li>
+                                            <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
+                                                <span style="color: #10b981; font-weight: bold; position: absolute; left: 0;">✓</span>
+                                                Si no solicitaste este cambio, ignora este correo
+                                            </li>
+                                        </ul>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Reset PIN Section -->
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <tr>
+                                    <td style="background: linear-gradient(135deg, #101419 0%, #37404a 100%); border-radius: 12px; padding: 32px; text-align: center; margin: 32px 0; color: white;">
+                                        <p style="font-size: 18px; margin: 0 0 16px 0; opacity: 0.95;">Haz clic en el botón para crear tu nuevo PIN</p>
+                                        <div style="font-size: 24px; font-weight: bold; margin: 16px 0; border: 3px solid white; border-radius: 8px; padding: 16px; display: inline-block; min-width: 200px; background-color: rgba(255,255,255,0.1);">
+                                            🔐 Restablecer PIN
+                                        </div>
+                                        <p style="font-size: 16px; margin: 16px 0 0 0; opacity: 0.9;">Podrás elegir un nuevo PIN de tu preferencia</p>
+                                        <table width="100%" cellpadding="0" cellspacing="0" style="margin-top: 20px;">
+                                            <tr>
+                                                <td align="center">
+                                                    <a href="{reset_pin_url}" style="background-color: white; color: #101419; padding: 16px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px; display: inline-block;">
+                                                        Crear Nuevo PIN
+                                                    </a>
+                                                </td>
+                                            </tr>
+                                        </table>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Next Steps -->
+                            <table width="100%" cellpadding="0" cellspacing="0" style="margin: 32px 0;">
+                                <tr>
+                                    <td style="background-color: #f0f9ff; border-radius: 8px; padding: 24px; border-left: 4px solid #3b82f6;">
+                                        <h3 style="color: #101419; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">¿Qué sigue?</h3>
+                                        <p style="color: #374151; font-size: 14px; line-height: 1.5; margin: 0;">
+                                            1. Haz clic en "Crear Nuevo PIN"<br>
+                                            2. Elige un PIN fácil de recordar pero seguro<br>
+                                            3. Ingresa a {app_name} con tu nuevo PIN<br>
+                                            4. ¡Disfruta de todos nuestros beneficios!
+                                        </p>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                            <!-- Website Section -->
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <tr>
+                                    <td style="text-align: center; margin: 32px 0; padding: 24px; background-color: #f9fafb; border-radius: 8px;">
+                                        <p style="color: #6b7280; margin: 0 0 12px 0;">Visita nuestra plataforma:</p>
+                                        <a href="https://www.expressklein.com" style="color: #101419; font-size: 20px; font-weight: 600; text-decoration: none; border-bottom: 2px solid #101419; padding-bottom: 4px;">
+                                            www.expressklein.com
+                                        </a>
+                                    </td>
+                                </tr>
+                            </table>
+                            
+                        </td>
+                    </tr>
+                    
+                    <!-- Footer -->
+                    <tr>
+                        <td style="background-color: #f9fafb; padding: 24px 32px; text-align: center; border-top: 1px solid #e5e7eb;">
+                            <p style="color: #6b7280; font-size: 14px; margin: 0; line-height: 1.5;">
+                                <strong>Express Klein</strong> - Tu aplicación de pedidos favorita<br>
+                                Este enlace de restablecimiento expira en 24 horas.<br>
+                                Si no solicitaste este cambio, puedes ignorar este correo.<br>
+                                © 2025 Express Klein. Todos los derechos reservados.
+                            </p>
+                        </td>
+                    </tr>
+                    
+                </table>
+            </td>
+        </tr>
+    </table>
+</body>
+</html>
+    """
 }

+ 31 - 0
config/messages.py

@@ -0,0 +1,31 @@
+class ErrorResponse:
+    """Class to handle error messages in the response."""
+
+    PRINTER_DISCONNECTED = "La impresora se encuentra desconectada, ya se ha informado al personal."
+    MISSING_FIELDS = "Los campos 'items' y 'table' son obligatorios."
+    INVALID_TABLE_TYPE = "El campo 'table' debe ser un número entero."
+    PRODUCT_ADD_ERROR = "Ha ocurrido un error al agregar los productos a la mesa."
+    USER_CREATION_ERROR = "Hubo un error al crear el usuario."
+    INVALID_CREDENTIALS = "Correo electrónico o PIN inválidos."
+    TOO_MANY_ATTEMPTS = "Demasiados intentos de inicio de sesión. Por favor, inténtelo más tarde."
+
+
+class SuccessResponse:
+    """Class to handle success messages in the response."""
+
+    ORDER_SUCCESS = "Orden enviada correctamente, se está procesando."
+    PRODUCTS_FETCH_SUCCESS = "Productos obtenidos correctamente."
+    CHAT_RESPONSE_SUCCESS = "Respuesta generada correctamente."
+    USER_CREATED_SUCCESS = "Usuario creado exitosamente."
+    LOGIN_SUCCESS = "Inicio de sesión exitoso."
+    USER_DELETED_SUCCESS = "Usuario eliminado exitosamente."
+
+
+class UserResponse:
+    """Class to handle user-related messages in the response."""
+
+    USER_NOT_FOUND = "Usuario con ID '{user_id}' no encontrado."
+    USER_EXISTS = "El usuario existe."
+    USER_DOES_NOT_EXIST = "El usuario no existe."
+    USER_ALREADY_EXISTS = "El usuario ya está registrado."
+    USER_FORMAT_BLOCKED = "Demasiados intentos de inicio de sesión. Usuario bloqueado por {time}."

+ 53 - 0
public/main/animations.css

@@ -0,0 +1,53 @@
+
+    @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%);
+      }
+    }

+ 2 - 56
public/main/index.html

@@ -9,6 +9,7 @@
   <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>
+  <link rel="stylesheet" href="/express/animations.css">
    <!--Tailwind-->
    <script>
         tailwind.config = {
@@ -28,61 +29,6 @@
   <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
   <script src="/express/js/app.js" type="module"></script>
   <link rel="stylesheet" href="/express/styles.css">
-  <!-- Animaciones -->
-  <style>
-    @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="h-[100dvh] max-h-[100dvh] flex flex-col bg-gray-50 overflow-x-hidden"
@@ -300,7 +246,7 @@
                required />
       </div>
     </div>
-
+    <span id="ErrorLogin" class="hidden text-red-500 text-sm">Todo cago, perdon</span>
     <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]">

+ 4 - 30
public/main/js/app.js

@@ -1,7 +1,8 @@
 import { sendMessage as serviceSendMessage } from './service/chat.js';
 import { getProducts, sendOrder } from './service/product.js';
 import { login } from './service/auth.js'
-
+import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
+import { showError } from './utils/error.js';
 // --- Variables de Usuario ---
 let userId = -1;
 let userName = "Cliente";
@@ -34,8 +35,6 @@ const chatForm = document.getElementById("chatForm");
 const aiLoadingIndicator = document.getElementById("aiLoadingIndicator");
 const chatSuggestionsElement = document.getElementById("chatSuggestions");
 
-// --- Loader Global ---
-let globalLoaderElement = null;
 
 //#region --- Inicialización y Configuracion ---
 async function initializeApp() {
@@ -66,16 +65,15 @@ function initializeLoginModal() {
         userTable = Number(fd.get('table').trim());
 
         if (!email || !pin || !userTable) {
-            alert("Por favor, completa todos los campos.");
+            showError("Por favor, completa todos los campos.");
             return;
         }
         try{
             const {data} = await login(email, pin)
             userToken = data.token;
             userName = data.name;
-            console.log("Usuario autenticado:", data);
             if (!userToken || data.id === undefined) {
-                alert("Error al iniciar sesión. Por favor, inténtalo de nuevo.");
+                showError(data.message || "Error al iniciar sesión.");
                 return;
             }
 
@@ -113,31 +111,7 @@ function setupBasicListeners() {
 //#endregion
 //#region ===== Utilidad =====
 
-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() {
-    if (!globalLoaderElement) createGlobalLoader();
-    globalLoaderElement.style.display = 'flex';
-    setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.opacity = '1'; }, 10);
-}
 
-function hideGlobalLoader() {
-    if (globalLoaderElement) {
-        globalLoaderElement.style.opacity = '0';
-        setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.display = 'none'; }, 300);
-    }
-}
 
 function formatPrice(price) {
     return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });

+ 11 - 4
public/main/js/service/auth.js

@@ -1,4 +1,6 @@
 
+import { showError } from "../utils/error.js";
+
 async function login(email,pin){
   const response = await fetch("/api/users/login", {
     method: "POST",
@@ -13,20 +15,25 @@ async function login(email,pin){
     throw new Error(errorData.message);
   }else if (response.status == 401) {
     const errorData = await response.json().catch(() => ({ message: "Credenciales incorrectas.", attempts: 0 }));
-    alert(errorData.message);
+    showError(`${errorData.message} Intentos restantes: ${errorData.attempts_remaining || 0}`);
+
+    throw new Error(errorData.message);
+  } else if (response.status == 429) {
+    const errorData = await response.json().catch(() => ({ message: "Demasiados intentos de inicio de sesión." }));
+    showError(`${errorData.message} Intenta más tarde.`);
     throw new Error(errorData.message);
   }else if (response.status != 200) {
     console.error(response.status, response.statusText);
     const errorData = await response.json().catch(() => ({ message: "Error al iniciar sesión." }));
-    alert(errorData.message, `Intentos restantes: ${errorData.attempts_remaining || 0}`);
+    showError(`${errorData.message}`);
     throw new Error(errorData.message);
   }
   const data = await response.json();
   if (!data || !data.data.token) {
-    alert("Error al iniciar sesión.");
+    showError("Error al iniciar sesión, Intenta mas tarde.");
     throw new Error("Error al iniciar sesión.");
   }
-  return data;
+  return {data};
 }
 
 

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

@@ -0,0 +1,16 @@
+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 };

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

@@ -0,0 +1,31 @@
+
+// --- 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() {
+    if (!globalLoaderElement) createGlobalLoader();
+    globalLoaderElement.style.display = 'flex';
+    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 };

+ 36 - 158
public/main/styles.css

@@ -6,8 +6,10 @@
     --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) */
+    --accent-red: #5490eb;
+    /* Rojo (Tailwind red-600) */
+    --accent-red-hover: #b91c1c;
+    /* Rojo más oscuro (Tailwind red-700) */
 }
 
 * {
@@ -22,139 +24,24 @@ body {
     line-height: 1.625;
 }
 
-.accent-red { color: var(--accent-red); }
-.bg-accent-red { background-color: var(--accent-red); }
-.border-accent-red { border-color: var(--accent-red); }
-
-header.bg-black { background-color: var(--background-header); }
-footer.bg-black { background-color: var(--background-header); }
-.border-gray-800 { border-color: var(--border-color); }
-
-.product-card {
-    background-color: var(--background-card);
-    border: 1px solid var(--border-color);
-}
-.category-title {
-    color: var(--accent-red);
-    border-bottom: 2px solid var(--accent-red);
+header.bg-black {
+    background-color: var(--background-header);
 }
 
-
-.chat-header {
-    background-color: #1a1a1a;
-    padding: 1rem 1.5rem;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    border-bottom: 1px solid var(--border-color);
+footer.bg-black {
+    background-color: var(--background-header);
 }
 
-.chat-content-area {
-    padding: 1rem;
-    flex-grow: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-}
-
-.scrollable-chat {
-    flex-grow: 1;
-    overflow-y: auto;
-    margin-bottom: 1rem;
-    padding-right: 0.5rem;
-}
-
-.chat-bubble {
-    max-width: 85%;
-    padding: 10px 15px;
-    border-radius: 15px;
-    margin-bottom: 10px;
-    word-wrap: break-word;
-    font-size: 0.95rem;
-    line-height: 1.5;
-}
-.chat-bubble-user {
-    background-color: var(--accent-red);
-    color: white;
-    margin-left: auto;
-    border-bottom-right-radius: 5px;
-}
-.chat-bubble-ai {
-    /* background-color: #ffffff; */
-    margin-right: auto;
-    border-bottom-left-radius: 5px;
-    & p {
-
-    color: rgb(17, 17, 17);
-    }
-}
-
-
-.chat-input-container {
-    display: flex;
-    gap: 0.5rem;
-    width: 100%;
-    margin-top: auto;
-}
-
-/* Scrollbar Styles */
-.scrollable-chat::-webkit-scrollbar { width: 8px; }
-.scrollable-chat::-webkit-scrollbar-track { background: #2d2d2d; border-radius: 10px; }
-.scrollable-chat::-webkit-scrollbar-thumb { background: #555; border-radius: 10px; }
-.scrollable-chat::-webkit-scrollbar-thumb:hover { background: var(--accent-red); }
-
-/* Modal Styles */
-.modal {
-    display: none;
-    position: fixed;
-    z-index: 1050;
-    left: 0;
-    top: 0;
-    width: 100%;
-    height: 100%;
-    overflow: auto;
-    background-color: rgba(0,0,0,0.85);
-    align-items: center;
-    justify-content: center;
-}
-.modal-content {
+.product-card {
     background-color: var(--background-card);
-    padding: 2rem;
     border: 1px solid var(--border-color);
-    width: 90%;
-    max-width: 450px;
-    border-radius: 12px;
-    position: relative;
-}
-.close-button {
-    color: #aaa;
-    font-size: 28px;
-    font-weight: bold;
-    cursor: pointer;
-    background: none;
-    border: none;
-}
-.close-button:hover,
-.close-button:focus {
-    color: var(--accent-red);
-    text-decoration: none;
-}
-
-/* Loader Styles */
-.loading-spinner {
-    border: 4px solid rgba(255, 255, 255, 0.3);
-    border-radius: 50%;
-    border-top: 4px solid var(--accent-red);
-    width: 24px;
-    height: 24px;
-    animation: spin 1s linear infinite;
-    margin: 0 auto;
 }
 
-#cartIcon{
+#cartIcon {
     position: relative;
 }
-#cartCount{
+
+#cartCount {
     position: absolute;
     top: -20%;
     left: -20%;
@@ -172,36 +59,13 @@ footer.bg-black { background-color: var(--background-header); }
 }
 
 @keyframes spin {
-    0% { transform: rotate(0deg); }
-    100% { transform: rotate(360deg); }
-}
-
-/* Media Queries para Responsividad */
-@media (max-width: 1023px) {
-    .chat-panel-embedded {
-        height: auto; /* Altura automática en móvil */
-        margin-bottom: 2rem;
-    }
-    .chat-content-area {
-        height: 40vh; /* Altura específica para el contenido del chat */
+    0% {
+        transform: rotate(0deg);
     }
-}
-/* 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;
+    100% {
+        transform: rotate(360deg);
+    }
 }
 
 #aiLoadingIndicator::after {
@@ -212,9 +76,23 @@ footer.bg-black { background-color: var(--background-header); }
 }
 
 @keyframes thinking {
-    0% { content: ""; }
-    25% { content: "."; }
-    50% { content: ".."; }
-    75% { content: "..."; }
-    100% { content: "...."; }
+    0% {
+        content: "";
+    }
+
+    25% {
+        content: ".";
+    }
+
+    50% {
+        content: "..";
+    }
+
+    75% {
+        content: "...";
+    }
+
+    100% {
+        content: "....";
+    }
 }

+ 33 - 2
public/register/app.js

@@ -27,7 +27,7 @@ function showRegisterModal() {
         // Manejo del envío del formulario
         document.getElementById('registerForm').addEventListener('submit', function(e) {
             e.preventDefault();
-            
+            showGlobalLoader();
             const formData = new FormData(e.target);
             const data = {
                 name: formData.get('name'),
@@ -54,9 +54,40 @@ function showRegisterModal() {
                 console.log('Registro exitoso:', data);
                 alert('Registro exitoso! Revisa tu correo electrónico para el PIN.');
                 hideRegisterModal();
+            }).finally(() => {
+                hideGlobalLoader();
             })
         });
 
+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 registro...</p>
+    `;
+    document.body.appendChild(globalLoaderElement);
+}
+
+function showGlobalLoader() {
+    if (!globalLoaderElement) createGlobalLoader();
+    globalLoaderElement.style.display = 'flex';
+    setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.opacity = '1'; }, 10);
+}
+
+function hideGlobalLoader() {
+    if (globalLoaderElement) {
+        globalLoaderElement.style.opacity = '0';
+        setTimeout(() => { if (globalLoaderElement) globalLoaderElement.style.display = 'none'; }, 300);
+    }
+}
+
 document.addEventListener('DOMContentLoaded', function() {
+    createGlobalLoader();
+    hideGlobalLoader();
     showRegisterModal();
-});
+});
+

+ 2 - 1
routes/chat.py

@@ -7,6 +7,7 @@ from services.logging_service import log_llm_response
 from auth.security import get_current_user
 import logging
 from fastapi import APIRouter
+from config.messages import SuccessResponse
 logger = logging.getLogger(__name__)
 
 chat_router = APIRouter()
@@ -21,7 +22,7 @@ async def chat_completions(request_data: ChatCompletionRequest, request: Request
     try:
         openai_response = await generate_completion(request_data.messages, session_identifier, current_user.nombre, current_user.email)
         log_llm_response(request_data.user, openai_response)
-        return JSONResponse({"response": openai_response})
+        return JSONResponse({"response": openai_response, "message": SuccessResponse.CHAT_RESPONSE_SUCCESS})
     except HTTPException as e:
         raise e
     except Exception as e:

+ 15 - 16
routes/orders.py

@@ -14,6 +14,7 @@ from logging import getLogger
 from threading import Thread
 from services.data_service import ProductDataService, UserDataService
 from config.mails import PRINTER_DISCONNECTED_MAIL
+from config.messages import ErrorResponse, SuccessResponse, UserResponse
 
 logger = getLogger(__name__)
 user_data_service = UserDataService()
@@ -31,23 +32,22 @@ async def printer_order(order: OrderWeb):
         logger.error("Printer is not connected.")
         email_thread = Thread(
             target=send_email,
-            args=(PRINTER_DISCONNECTED_MAIL["subject"], PRINTER_DISCONNECTED_MAIL["body"], ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]),
+            args=(PRINTER_DISCONNECTED_MAIL["subject"], PRINTER_DISCONNECTED_MAIL["body"], ["erwinjacimino2003@gmail.com"]),
             daemon=True
         )
         email_thread.start()
         logger.error("Email sent to admin about printer issue.")
-        return JSONResponse(status_code=424, content={"message": "Printer is not connected."})
+        return JSONResponse(status_code=424, content={"message": ErrorResponse.PRINTER_DISCONNECTED})
 
-    
     items = order.items
     table = order.table
-    
+
     if not items or not table:
-        return JSONResponse(status_code=400, content={"message": "Items and table are required."})
-    
+        return JSONResponse(status_code=400, content={"message": ErrorResponse.MISSING_FIELDS})
+
     if not isinstance(table, int):
-        return JSONResponse(status_code=400, content={"message": "Table must be an integer."})
-    
+        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_TABLE_TYPE})
+
     # Add products to Fudo
     product_errors = []
     for item in items:
@@ -55,28 +55,27 @@ async def printer_order(order: OrderWeb):
         # 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}
+            content={"message": ErrorResponse.PRODUCT_ADD_ERROR, "errors": product_errors}
         )
-    
+
     user = user_data_service.get_by_id(order.customerId)
     if not user:
-        return JSONResponse(status_code=404, content={"message": "User not found."})
+        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=order.customerId)})
     products = product_data_service.get_products([item.id for item in items])
     # Print order
     printer_orders.append(Order(
         user=user.nombre if user else "Unknown User",
         items=[Item(product.name, product.price, item.quantity) for product, item in zip(products, items)]
     ))
-    
-    
+
     # Log order
     log_order(user.nombre, order.table, order_date=order.orderDate, items=[product.name for product in products])
-    
-    return JSONResponse({"message": "Order processed successfully"})
+
+    return JSONResponse({"message": SuccessResponse.ORDER_SUCCESS})
 
 def order_thread():
     """Thread to process orders"""

+ 3 - 1
routes/products.py

@@ -4,6 +4,8 @@ from auth.security import get_current_user
 from services.data_service import ProductDataService
 from logging import getLogger
 from fastapi import APIRouter, Depends
+from config.messages import SuccessResponse
+
 logger = getLogger(__name__)
 
 product_data_service = ProductDataService()
@@ -16,4 +18,4 @@ async def get_products(current_user = Depends(get_current_user)):
     logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
     logger.info("Fetching all products")
     all_products = list(map(lambda p: p.model_dump(), product_data_service.get_all()))
-    return JSONResponse({"products": all_products})
+    return JSONResponse({"products": all_products, "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})

+ 27 - 13
routes/users.py

@@ -1,4 +1,5 @@
 from logging import getLogger
+from math import log
 from fastapi import APIRouter
 from fastapi.responses import JSONResponse
 from httpx import RequestError
@@ -13,6 +14,7 @@ from auth.security import generate_token
 from services.email_service import send_email
 from config.mails import REGISTER_MAIL
 from config.settings import APPNAME
+from config.messages import ErrorResponse, SuccessResponse, UserResponse
 fernet = Fernet(PIN_KEY.encode())
 logger = getLogger(__name__)
 user_data_service = UserDataService()
@@ -31,9 +33,9 @@ async def exists_user(request: UserIDRequest):
         """Check if user exists"""
         user = user_data_service.get_by_id(request.id)
         if user:
-            return JSONResponse(status_code=200, content={"exists": True})
+            return JSONResponse(status_code=200, content={"exists": True, "message": UserResponse.USER_EXISTS})
         else:
-            return JSONResponse(status_code=404, content={"exists": False, "data": {"message": "User does not exist."}})
+            return JSONResponse(status_code=404, content={"exists": False, "message": UserResponse.USER_DOES_NOT_EXIST})
 
 @user_router.post("/register")
 async def register_user(request: RegisterUserRequest):
@@ -41,16 +43,16 @@ async def register_user(request: RegisterUserRequest):
     pin = unique_pin_generate()
     userID = user_data_service.create(request.name, request.email, request.rut, pin)
     if userID == -1:
-        return JSONResponse(status_code=400, content={"message": "User already exists."})
+        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
     user = user_data_service.get_by_id(userID)
     if not user:
-        return JSONResponse(status_code=500, content={"message": "Error creating user."})
+        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
     send_email(
         REGISTER_MAIL["subject"],
         REGISTER_MAIL["body"],
         [user.email], name=user.nombre, app_name=APPNAME, pin=pin
     )
-    return JSONResponse(status_code=201, content={"message": "User created successfully.", "data": {
+    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
         **user.model_dump(exclude={"pin_hash"}),
         "token": generate_token(user.email)
     }})
@@ -58,11 +60,21 @@ async def register_user(request: RegisterUserRequest):
 @user_router.post("/login")
 async def login_user(request: LoginRequest):
     """Login user with email and PIN"""
+    redis_client = redis.Redis(host='localhost', port=6379, db=0)
     logger.debug(f"Login attempt for email: {request.email}")
+
+    is_blocked = redis_client.get(f"blocked:{request.email}")
+
+    if is_blocked:
+        logger.warning(f"Login attempt for blocked user: {request.email}, locked: {is_blocked}")
+        return JSONResponse(status_code=403, content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{int(str(redis_client.ttl(f'blocked:{request.email}'))) // 60} minutos")})
+    else:
+        logger.info(f"{redis_client.get(f'blocked:{request.email}')}")
+    
     user = user_data_service.login(request.email, request.pin)
     if user:
         # Successful login, return user data and token
-        return JSONResponse(status_code=200, content={"message": "Login successful.", "data": {
+        return JSONResponse(status_code=200, content={"message": SuccessResponse.LOGIN_SUCCESS, "data": {
             "id": user.id,
             "name": user.nombre,
             "email": user.email,
@@ -72,31 +84,33 @@ async def login_user(request: LoginRequest):
         }})
     else:
         # Failed login: increment attempts in Redis
-        redis_client = redis.Redis(host='localhost', port=6379, db=0)
         redis_client.incr(f"login_attempts:{request.email}")
         redis_client.expire(f"login_attempts:{request.email}", 3600)
         redis_attempts = redis_client.get(f"login_attempts:{request.email}")
         attempts = int(redis_attempts) if redis_attempts else 0 # type: ignore
-        if attempts and int(attempts) > 5:
+        if attempts and int(attempts) >= 5:
             # Too many attempts, block login
-            return JSONResponse(status_code=429, content={"message": "Too many login attempts. Please try again later."})
+            redis_client.set(f"blocked:{request.email}", "true")
+            redis_client.expire(f"blocked:{request.email}", 3600)
+            logger.warning(f"Too many login attempts for {request.email}. User blocked.")
+            return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
         else:
             # Set flag for last failed login
             redis_client.set(f"last_failed_login:{request.email}", "true")
-            redis_client.expire(f"last_failed_login:{request.email}", 3600)
+            redis_client.expire(f"last_failed_login:{request.email}", 1800)  # 30 minutes
             logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
             logger.error("Invalid email or PIN.")
         # Return unauthorized with attempts remaining
-        return JSONResponse(status_code=401, content={"message": "Invalid email or PIN.", "attempts_remaining": 5 - attempts if attempts else 5})
+        return JSONResponse(status_code=401, content={"message": ErrorResponse.INVALID_CREDENTIALS, "attempts_remaining": 5 - attempts if attempts else 5})
 
 @user_router.delete("/delete")
 async def delete_user(request: UserIDRequest):
     """Delete a user by ID"""
     user = user_data_service.delete(request.id)
     if user:
-        return JSONResponse(status_code=200, content={"message": "User deleted successfully.", "data": user})
+        return JSONResponse(status_code=200, content={"message": SuccessResponse.USER_DELETED_SUCCESS, "data": user})
     else:
-        return JSONResponse(status_code=404, content={"message": "User not found."})
+        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
 
 @user_router.get("/all")
 async def get_all_users():