Procházet zdrojové kódy

Add store management routes and in-time middleware; enhance user and product models with comments

Erwin Jacimino před 6 měsíci
rodič
revize
5d9b25d6fb

+ 7 - 1
app.py

@@ -3,7 +3,8 @@ from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from config.settings import DEVELOPMENT, SECRET_KEY
-from models import user
+from middleware.in_time import InTimeMiddleware
+from routes import store
 from routes import sales
 from services.email_service import initialize_email_sender
 from middleware.no_cache import NoCacheMiddleware
@@ -62,6 +63,9 @@ def create_app() -> FastAPI:
         logger.info("Adding no-cache middleware")
         app.add_middleware(NoCacheMiddleware)
         
+        # Add InTimeMiddleware
+        logger.info("Adding in-time middleware")
+        app.add_middleware(InTimeMiddleware)
         # disable docs
         app.docs_url = None
         app.redoc_url = None
@@ -126,6 +130,8 @@ def setup_routes(app: FastAPI):
             dependencies=[Depends(get_current_user)]
         )
 
+        logger.info("Seeting store management routes")
+        app.include_router(store.store_router, prefix="/api/store", tags=["Store Management"])
         # Verification routes
         logger.info("Setting up verification routes")
         app.include_router(users.verify_router, prefix="/verify", tags=["Verification"])

+ 2 - 0
auth/security.py

@@ -90,6 +90,8 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
     
     try:
         token = credentials.credentials
+        if (token.startswith("Bearer ")):
+            token = token.replace("Bearer ", "", 1)
         logger.debug(f"Decoding token: {token[:20]}...")
         
         payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

+ 1 - 0
config/settings.py

@@ -12,6 +12,7 @@ APPNAME = "Pedidos Express"
 MAIL = os.getenv("MAIL_DIRECTION","")
 MAIL_PASSWORD = os.getenv("MAIL_PASSWORD","")
 LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
+IS_OPEN_STORE = True
 if not os.path.exists(LOGS_FOLDER):
     os.makedirs(LOGS_FOLDER)
 LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()

+ 82 - 0
middleware/in_time.py

@@ -0,0 +1,82 @@
+"""
+Middleware para agregar headers de no-cache a las respuestas
+"""
+from fastapi.responses import HTMLResponse
+from fastapi.security import HTTPAuthorizationCredentials
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request
+from starlette.responses import Response
+from auth.security import get_current_user
+from config import settings
+import datetime
+
+
+start_time = datetime.time(10, 0, 0)
+end_time = datetime.time(23, 30, 0)
+
+class InTimeMiddleware(BaseHTTPMiddleware):
+    """
+    Middleware que revisa si la peticion se hizo dentro del horario de funcionamiento del local
+    10:00 a 23:30 horario de Chile
+    """
+    
+    async def dispatch(self, request: Request, call_next):
+        authorization = request.headers.get("Authorization")
+        if not authorization:
+          if settings.IS_OPEN_STORE:
+            return await call_next(request)
+          else:
+            return HTMLResponse(""" 
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Local Cerrando :(</title>
+  <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
+</head>
+<body>
+      <!-- Header -->
+    <div class="bg-[#101419] h-screen flex justify-center items-center flex- px-6 py-8 text-white text-center relative">
+
+      <!-- Icono principal -->
+      <div class="text-6xl mb-3">😫</div>
+
+      <!-- Título -->
+      <h2 class="text-2xl font-bold mb-2">¡El local se encuentra cerrado!</h2>
+    </div>
+</body>
+</html>
+                                """, status_code=420)
+            
+        time = datetime.datetime.now().time()
+        user = await get_current_user(
+                HTTPAuthorizationCredentials(scheme="Bearer", credentials=authorization)
+            )
+            
+        if ((time >= start_time and time <= end_time) and settings.IS_OPEN_STORE) or (user.permissions or 0) >= 1:
+            response = await call_next(request)
+            return response
+        else:
+            return HTMLResponse(""" 
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Local Cerrando :(</title>
+  <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
+</head>
+<body>
+      <!-- Header -->
+    <div class="bg-[#101419] h-screen flex justify-center items-center flex- px-6 py-8 text-white text-center relative">
+
+      <!-- Icono principal -->
+      <div class="text-6xl mb-3">😫</div>
+
+      <!-- Título -->
+      <h2 class="text-2xl font-bold mb-2">¡El local se encuentra cerrado!</h2>
+    </div>
+</body>
+</html>
+                                """, status_code=420)

+ 1 - 0
models/items.py

@@ -5,6 +5,7 @@ class Item(BaseModel):
     name: str
     price: float
     quantity: int
+    comment: str
 
 class Order(BaseModel):
     """Order model matching the database schema"""

+ 1 - 0
models/sales.py

@@ -13,6 +13,7 @@ class ItemWeb(BaseModel):
     id: int
     price: int
     quantity: int
+    comment: str
     promotion: Optional[Promotion] = None
 
 

+ 6 - 0
models/user.py

@@ -10,6 +10,12 @@ class RegisterUserRequest(BaseModel):
     email: str
     rut: str
 
+class ForceRegisterUserRequest(BaseModel):
+    name: str
+    email: str
+    rut: str
+    pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")
+
 class UserRewardRequest(BaseModel):
     tableNumber: int
 

+ 56 - 3
public/main/index.html

@@ -149,7 +149,7 @@
       <header class="p-4 border-b border-gray-200">
         <h3 class="text-lg font-bold text-[#101419]">Tu pedido</h3>
       </header>
-
+      
       <div id="cartItems" class="flex-1 overflow-y-auto p-4 space-y-2 max-h-[25vh]"></div>
       <p id="emptyCartText" class="hidden text-center text-gray-400 mt-4">Tu carrito está vacío.</p>
 
@@ -250,7 +250,7 @@
   <!-- MODALS -->
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
-     class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+     class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-40 p-4">
   <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
     <div class="text-center">
       <h2 id="loginTitle" class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
@@ -416,7 +416,7 @@
     </div>
   </div>
 </div>
-<div id="successRewardModal" class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+<div id="successRewardModal" class="fixed hidden inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
    <div class="bg-white w-full max-w-md rounded-xl shadow-xl overflow-hidden">
        <!-- Header -->
        <div class="bg-[#101419] px-6 py-8 text-white text-center relative">
@@ -494,6 +494,59 @@
        </div>
    </div>
 </div>
+<div id="commentModal" class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+
+  <div class="bg-white w-full max-w-md rounded-xl shadow-xl overflow-hidden">
+    
+    <div class="hidden bg-gray-50 px-6 py-4 border-b border-gray-200">
+      <div class="flex items-center justify-between">
+        <h2 id="titleComment" class="text-xl font-bold text-[#101419]">Titulo</h2>
+        <button id="closeCommentModal" type="button" class="text-gray-400 hover:text-gray-600 text-2xl font-bold transition-colors">
+          &times;
+        </button>
+      </div>
+    </div>
+    <!-- 2 posibles modals: un campo con textarea y boton de enviar o un campo desplegable con opciones -->
+    <form id="selectCommentTypeForm" class="hidden">
+      <div class="p-6 space-y-4">
+        <p class="text-sm text-[#58728d]">
+          Elige un sabor
+        </p>
+        <div id="selectOptions" class="flex flex-col gap-2">
+            
+        </div>
+      </div>
+    </form>
+    <form id="commentForm" class="hidden">
+      <div class="p-6 space-y-4">
+        <p class="text-sm text-[#58728d]">
+          ¿Alguna instrucción para la cocina? (Ej: sin cebolla, muy picante, alergias, etc.)
+        </p>
+        <div>
+          <label for="commentTextarea" class="hidden text-sm font-medium text-gray-700 mb-2">
+            Tu comentario
+          </label>
+          <textarea id="commentTextarea"
+                    name="comment"
+                    rows="4"
+                    class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                    placeholder="Escribe tu comentario aquí..."></textarea>
+        </div>
+      </div>
+
+      <div class="border-t border-gray-200 bg-gray-50 px-6 py-4 flex gap-3">
+        <button id="cancelCommentBtn" type="button" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200">
+          Cancelar
+        </button>
+        <button id="submitCommentBtn" type="submit" class="flex-1 bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200">
+          Guardar
+        </button>
+      </div>
+    </form>
+
+  </div>
+</div>
+
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
   <script>
     // toast simple

+ 44 - 18
public/main/js/app.js

@@ -3,12 +3,13 @@ import { getOnlineUserCount, getUserList } 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 { updateProgress, claimReward } from './utils/progressBar.js';
+import { updateProgress, claimReward, getProgress } from './utils/progressBar.js';
 import { showError } from './utils/error.js';
 import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
 import { hideGUI, showGUI } from './utils/gui.js';
 import { smartSearch } from './utils/searching.js';
 import { getUserData } from './service/user.js';
+import { getComment, COMMENT_TYPES } from './utils/get_comment.js';
 
 // --- Variables Globales ---
 
@@ -46,14 +47,7 @@ const productPriority = {
     ]
 }
 
-// --- Chat History ---
-let chatHistory = [
-    { 
-        user: "name",
-        time: "00:00",
-        content: "Hola Mundo" 
-    }
-];
+
 
 const userColors = [
   // Rojos
@@ -80,6 +74,16 @@ const userColors = [
   "text-neutral-500", "text-neutral-600", "text-stone-500", "text-stone-600"
 ];
 
+const productsWithVariety = {
+    "480": [
+        "Frambuesa",
+        "Mango",
+        "Maracuyá",
+        "Piña",
+        "Tradicional"
+    ]
+}
+
 
 //--- Elementos del DOM ---
 
@@ -337,6 +341,11 @@ function initializeChat() {
     });
 }
 
+export function beforeUnloadHandler() {
+    if (userToken) {
+        logout();
+    }
+}
 /**
  * Setup basic event listeners
  */
@@ -357,11 +366,7 @@ function setupBasicListeners() {
         }
     });
 
-    window.addEventListener('beforeunload', function (event) {
-        // Show a confirmation dialog
-        event.preventDefault();
-        event.returnValue = 'Estas saliendo de la aplicacion, estas seguro?';
-    });
+    window.addEventListener('beforeunload', beforeUnloadHandler);
 }
 
 /**
@@ -845,15 +850,32 @@ async function renderProductsWithAnimation(products, groupInCategories = true, s
 /**
  * Add product to cart with visual feedback
  */
-window.addToCart = function(productId, buttonElement = null) {
+
+window.addComment = async function(productId) {
+    const cartItem = cart.find(p => p.id === productId);
+    if (!cartItem) return;
+    const comment = await getComment(COMMENT_TYPES.LIBRE, [cartItem.comment]).catch(e => console.error(e));
+    if (!comment) return;
+    cartItem.comment += ` ${comment}`;
+    console.log(cartItem);
+}
+
+window.addToCart = async function(productId, buttonElement = null) {
+    let comment = "";
+    if (productsWithVariety[String(productId)]) {
+        comment = await getComment(COMMENT_TYPES.DESPLEGABLE, productsWithVariety[String(productId)]);
+    }
+
     const product = Allproducts.find(p => p.id === productId);
     if (!product) return;
     
     const cartItem = cart.find(item => item.id === productId);
     if (cartItem) {
         cartItem.quantity++;
+        cartItem.comment += `${comment}, `;
+
     } else {
-        cart.push({ ...product, quantity: 1 });
+        cart.push({ ...product, quantity: 1, comment });
     }
 
     // Visual feedback for button
@@ -951,6 +973,9 @@ function updateCartDisplay() {
                         <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
                     </div>
                     <div class="flex items-center gap-1 sm:gap-2">
+                        <button class="comment-button text-gray-500 hover:text-gray-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors" onclick="addComment(${item.id})">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-message-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M12.01 18.594l-4.01 2.406v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v5.5" /><path d="M16 19h6" /><path d="M19 16v6" /></svg>
+                        </button>
                         <button onclick="addToCart(${item.id})" class="plus-button text-green-500 hover:text-green-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">+</button>
                         <button onclick="removeFromCart(${item.id})" class="minus-button text-yellow-500 hover:text-yellow-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">-</button>
                         <button onclick="removeFromCart(${item.id}, true)" class="remove-button text-red-500 hover:text-red-400 text-base sm:text-lg p-1 rounded-full hover:bg-gray-700 transition-colors">
@@ -987,7 +1012,8 @@ async function processOrder() {
             items: cart.map(item => ({ 
                 id: item.id, 
                 price: item.price, 
-                quantity: item.quantity 
+                quantity: item.quantity,
+                comment: item.comment ?? ""
             })),
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
@@ -995,7 +1021,7 @@ async function processOrder() {
         
         const data = await sendOrder(orderData, userToken);
         
-        if (data && data.new_progress) {
+        if (data && data.new_progress && data.new_progress !== getProgress()) {
             updateProgress(data.new_progress);
         }
         

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

@@ -1,4 +1,5 @@
 
+import { beforeUnloadHandler } from "../app.js";
 import { showError } from "../utils/error.js";
 
 async function login(email,pin){
@@ -10,6 +11,10 @@ async function login(email,pin){
     body: JSON.stringify({ email, pin })
 }
   );
+  if (response.status == 420) {
+    window.removeEventListener("beforeunload", beforeUnloadHandler);
+    window.location.replace("/");
+  }
   if (response.status == 404) {
     const errorData = await response.json().catch(() => ({ message: "El usuario no fue encontrado." }));
     throw new Error(errorData.message);

+ 20 - 17
public/main/js/service/product.js

@@ -1,3 +1,17 @@
+import { beforeUnloadHandler } from "../app.js";
+
+ async function getJSONData(response) {
+  if (response.status === 420) {
+    window.removeEventListener("beforeunload", beforeUnloadHandler);
+    window.location.replace("/");
+  }
+  if (!response.ok) {
+    const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
+    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
+  }
+  const data = await response.json();
+  return data;
+}
 
 async function sendOrder(order, token) {
   try {
@@ -9,12 +23,7 @@ async function sendOrder(order, token) {
       },
       body: JSON.stringify(order)
     });
-    if (!response.ok) {
-      const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-    }
-    const data = await response.json();
-    return data;
+    return await getJSONData(response);
   } catch (error) {
     console.error("Error al enviar la orden:", error);
     throw error;
@@ -31,12 +40,8 @@ export async function freeBeer(token, tableNumber) {
       },
       body: JSON.stringify({ tableNumber })
     });
-    if (!response.ok) {
-      const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-    }
-    const data = await response.json();
-    return data;
+    return await getJSONData(response);
+
   } catch (error) {
     console.error("Error al obtener cerveza gratis:", error);
     throw error;
@@ -50,11 +55,8 @@ async function getProducts(token){
       "Authorization": `Bearer ${token}`
     }
   });
-  if (!response.ok) {
-    const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-  }
-  const data = await response.json();
+  
+  const data = await getJSONData(response);
   data.products.map(product => {
     if (product.promotion) {
       product.price = product.promotion.price;
@@ -63,4 +65,5 @@ async function getProducts(token){
   return data.products;
 }
 
+
 export {sendOrder, getProducts}

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

@@ -1,3 +1,4 @@
+import { beforeUnloadHandler } from "../app.js";
 import { showError } from "../utils/error.js";
 
 
@@ -9,6 +10,10 @@ export async function getUserData(token) {
         'Authorization': `Bearer ${token}` 
       }
     });
+    if (response.status === 420) {
+      window.removeEventListener("beforeunload", beforeUnloadHandler);
+      window.location.replace("/");
+    }
     if (response.status === 401) {
       showError("No autorizado. Verifica tu sesión.");
       throw new Error("Unauthorized access");
@@ -45,6 +50,10 @@ async function fetchUserSales(userId, token) {
             'Authorization': `Bearer ${token}`
         }
     });
+    if (response.status === 420) {
+      window.removeEventListener("beforeunload", beforeUnloadHandler);
+      window.location.replace("/");
+    }
     if (response.status === 404) {
       showError("No se encontraron ventas para este usuario.");
       return [];

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

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

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


+ 8 - 1
public/main/js/utils/progressBar.js

@@ -25,13 +25,20 @@ export function updateProgress(value) {
         rewardBtn.className = 'text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-full font-medium transition-all duration-200 animate-bounce';
         
         // Efecto de confeti (simulado con animación)
-        document.querySelector('.bg-gradient-to-r.from-amber-50').className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-200 animate-pulse';
+        const confetti = document.querySelector('.bg-gradient-to-r.from-amber-50')
+        if (confetti) {
+            confetti.className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-200 animate-pulse';
+        }
         
     } else if (currentProgress >= 75) {
         progressBar.className = 'h-full bg-gradient-to-r from-orange-400 to-red-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
     }
 }
 
+export function getProgress() {
+    return currentProgress;
+}
+
 export async function claimReward(token, userTable) {
             const userData = await getUserData(token);
             const progress = userData.rewardProgress || 0;

+ 4 - 4
routes/orders.py

@@ -231,10 +231,10 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
     # Print order
     try:
-        pizza_items = [ Item(name=item.name, price=product.price, quantity=item.quantity) for product, item in zip(items, products) if CAT_ITEMS.get(item.type) == Locations.PIZZAS]#type: ignore
-        burger_items = [ Item(name=item.name, price=product.price, quantity=item.quantity) for product, item in zip(items, products) if CAT_ITEMS.get(item.type) == Locations.BURGUER]#type: ignore
-        bar_items = [ Item(name=item.name, price=product.price, quantity=item.quantity) for product, item in zip(items, products) if CAT_ITEMS.get(item.type) == Locations.BAR]#type: ignore
-        coctelery_items = [ Item(name=item.name, price=product.price, quantity=item.quantity) for product, item in zip(items, products) if CAT_ITEMS.get(item.type) == Locations.COCTELERY]#type: ignore
+        pizza_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.PIZZAS]#type: ignore
+        burger_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BURGUER]#type: ignore
+        bar_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BAR]#type: ignore
+        coctelery_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.COCTELERY]#type: ignore
         
 
         if pizza_items:

+ 33 - 33
routes/products.py

@@ -120,38 +120,6 @@ async def get_free_beer(table_id: int, current_user:User = Depends(get_current_u
     return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id="free_beer")}, status_code=404)
 # MODERATE RISK OPERATIONS - Requires permissions >= 1 (Manager level or above)
 
-@product_router.patch("/{product_id}/edit")
-async def edit_product(product_id: int, product: ProductEditRequest, current_user = Depends(get_current_user)):
-    """
-    Edit an existing product - Requires manager permissions (level >= 1)
-    
-    Args:
-        product (ProductEditRequest): Product data to update
-        current_user: Authenticated user (dependency injection)
-        
-    Returns:
-        JSONResponse: Updated product data or permission denied message
-    """
-    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
-    logger.info(f"Editing product: {product_id}")
-    
-    # Check if user has sufficient permissions (manager level or above)
-    if user_data_service.permissions(current_user.id) > 0:
-
-        # Update product with provided data (excluding unset fields)
-        product_data_service.update(product_id, **product.model_dump(exclude_unset=True))
-        
-        # Retrieve updated product to return in response
-        edited_product = product_data_service.get_by_id(product_id)
-        if not edited_product:
-            return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
-        
-        logger.info(f"Product {product_id} edited successfully")
-        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS, "product": edited_product.model_dump()})
-    
-    # Return 403 if user lacks permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
-
 @product_router.post("/create")
 async def create_product(product: ProductCreateRequest, current_user = Depends(get_current_user)):
     """
@@ -232,4 +200,36 @@ async def delete_product(product_id: int, current_user = Depends(get_current_use
         return JSONResponse({"message": SuccessResponse.PRODUCT_DELETE_SUCCESS})
     
     # Return 403 if user lacks admin permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+
+@product_router.patch("/{product_id}/edit")
+async def edit_product(product_id: int, product: ProductEditRequest, current_user = Depends(get_current_user)):
+    """
+    Edit an existing product - Requires manager permissions (level >= 1)
+    
+    Args:
+        product (ProductEditRequest): Product data to update
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Updated product data or permission denied message
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info(f"Editing product: {product_id}")
+    
+    # Check if user has sufficient permissions (manager level or above)
+    if user_data_service.permissions(current_user.id) > 1:
+
+        # Update product with provided data (excluding unset fields)
+        product_data_service.update(product_id, **product.model_dump(exclude_unset=True))
+        
+        # Retrieve updated product to return in response
+        edited_product = product_data_service.get_by_id(product_id)
+        if not edited_product:
+            return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
+        
+        logger.info(f"Product {product_id} edited successfully")
+        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS, "product": edited_product.model_dump()})
+    
+    # Return 403 if user lacks permissions
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)

+ 24 - 0
routes/store.py

@@ -0,0 +1,24 @@
+from fastapi import APIRouter, Depends, Query, status
+from fastapi.responses import JSONResponse
+from models.user import User
+from pydantic import BaseModel
+from auth.security import get_current_user
+from config import settings
+
+
+
+store_router = APIRouter()
+
+class AppStateBody(BaseModel):
+    state: bool
+
+@store_router.post("/state", response_class=JSONResponse)
+def set_store_state(state: AppStateBody, current_user: User = Depends(get_current_user)):
+    if (current_user.permissions or -1) >= 2:
+        settings.IS_OPEN_STORE = state.state
+    
+    return {"state": settings.IS_OPEN_STORE}
+
+@store_router.get("/state", response_class=JSONResponse)
+def get_store_state(_: User = Depends(get_current_user)):
+    return {"state": settings.IS_OPEN_STORE}

+ 31 - 2
routes/users.py

@@ -16,7 +16,7 @@ from auth.security import get_current_user
 from config.mails import REGISTER_MAIL, PIN_RECOVERY_MAIL, PIN_SUCCESSFULLY
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.settings import APPNAME, DEVELOPMENT, PIN_KEY
-from models.user import LoginRequest, PinRecoveryRequest, PinRecoveryValidateRequest, PinUserRequest, RegisterUserRequest, User, UserIDRequest, UserMail, UserRewardRequest
+from models.user import ForceRegisterUserRequest, LoginRequest, PinRecoveryRequest, PinRecoveryValidateRequest, PinUserRequest, RegisterUserRequest, User, UserIDRequest, UserMail, UserRewardRequest
 from services.data_service import BlacklistDataService, UserDataService
 from services.email_service import get_email_sender
 from services.print_service import print_ticket
@@ -142,6 +142,32 @@ async def create_user(request: PinUserRequest, q: str):
         "token": generate_token(user.email)
     }})
 
+
+@user_router.post("/force-register")
+async def force_register_user(request: ForceRegisterUserRequest, current_user: User = Depends(get_current_user)):
+    """Force register a new user"""
+    logger.info(f"Force register attempt for email: {request.email}")
+    if (current_user.permissions or -1) >= 1:
+        return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
+    
+    
+    if not request.pin or len(request.pin) != 4:
+        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
+    userID = user_data_service.create(request.name, request.email, request.rut, request.pin)
+    if userID == -1:
+        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+    user = user_data_service.get_by_id(userID)
+    if not user:
+        logger.error(f"User creation failed for {request.email}: user not found after creation")
+        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
+
+    logger.info(f"User created successfully: {request.email}")
+    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
+        **user.model_dump(exclude={"pin_hash"}),
+        "token": generate_token(user.email)
+    }})
+    
+
 @user_router.post("/login")
 async def login_user(request: LoginRequest, http_request: Request):
     """Login user with email and PIN"""
@@ -205,6 +231,7 @@ async def login_user(request: LoginRequest, http_request: Request):
                     "created_at": user.created_at,
                     "token": generate_token(user.email),
                     "reward_progress": user.reward_progress,
+                    "permissions": user.permissions
                 }
             })
         else:
@@ -245,7 +272,9 @@ async def login_user(request: LoginRequest, http_request: Request):
         return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
 
 @user_router.delete("/delete")
-async def delete_user(request: UserIDRequest):
+async def delete_user(request: UserIDRequest, current_user: User = Depends(get_current_user)):
+    if current_user.permissions != 2:
+        return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
     """Delete a user by ID"""
     user = user_data_service.delete(request.id)
     if user:

+ 1 - 1
services/print_service.py

@@ -38,7 +38,7 @@ def print_order(order: Order, location:Locations ):
         # Prepare the order data for printing
         order_data = {
             "table": order.table,
-            "items": [{"name": item.name, "price": item.price, "quantity": item.quantity} for item in order.items],
+            "items": [{"name": item.name, "price": item.price, "quantity": item.quantity, "comment": item.comment} for item in order.items],
             "customerName": order.customerName,
             "totalAmount": order.totalAmount,
             "orderDate": order.orderDate