App QR para Mesas — Manual de Usuario

Sistema: ZEUMAX (ZEUMAX)
Módulo: app-qr — Experiencia de pedido sin contacto para comensales
Tecnología: Vite + React 19 + Tailwind CSS 3.4 + React Router DOM 7 + Zustand + Fetch API nativa
Paleta visual: Brand naranja #F5A623, Navy #2C3E50, Success #16A34A, Muted #7F8C8D
Fuentes: Rubik, Inter
Versión del documento: 1.0


Interfaz visual / Vistas principales

A continuación se muestran las capturas de pantalla principales de la App QR para Mesas.


Introducción

La App QR para Mesas es la experiencia de pedido sin contacto diseñada para clientes que se encuentran dentro del restaurante. Desde su mesa, el comensal escanea un código QR único impreso en la mesa o en un soporte de cartón, lo cual abre instantáneamente la carta digital completa del restaurante en su navegador móvil. No necesita descargar ninguna aplicación nativa, no requiere registro previo, ni correo electrónico, ni contraseña. Es una experiencia completamente anónima, fluida y optimizada para dispositivos móviles.

Al escanear el código QR, la aplicación detecta automáticamente a qué mesa pertenece el comensal, a qué sucursal del restaurante se encuentra, y autentica la sesión mediante un token de seguridad único por mesa. Todo esto ocurre en cuestión de segundos, en una pantalla de bienvenida minimalista que conecta al cliente con su mesa y le presenta el menú completo del restaurante, organizado por categorías, con imágenes reales de los productos, precios claros, y opciones de personalización como variaciones, extras (addons) y notas especiales para la cocina.

La app está construida como una PWA (Progressive Web App) optimizada, lo que significa que funciona en cualquier navegador móvil moderno (Chrome, Safari, Firefox, Edge) con una apariencia y sensibilidad cercana a una app nativa: transiciones suaves gestionadas con Framer Motion, iconos nítidos de Lucide React, componentes accesibles de Headless UI, y una paleta cromática cálida que refleja la identidad visual de ZEUMAX: el naranja energético del brand como color principal, el navy oscuro para textos y fondos en modo oscuro, el verde success para indicadores de confirmación, y grises muted para información secundaria.


Requisitos para usar la App QR

Para disfrutar de la experiencia de pedido sin contacto, el comensal solo necesita:

Requisito Descripción
Móvil con cámara Cualquier smartphone con cámara funcional y capacidad para leer códigos QR (la mayoría de cámaras nativas modernas lo hacen automáticamente).
Conexión a internet Wi-Fi del restaurante o datos móviles. La app es ligera y funciona incluso con conexiones modestas.
Código QR en la mesa Cada mesa tiene un QR único impreso que contiene los parámetros de identificación de la mesa y la sucursal.

No es necesario registrarse. No hay login, no hay formularios, no hay captchas. La app es anónima por diseño: el comensal escanea y empieza a pedir inmediatamente.


Flujo General: Cómo Funciona

La experiencia de la App QR sigue un flujo lineal e intuitivo de cuatro pasos, diseñado para que cualquier persona, sin importar su nivel de familiaridad con la tecnología, pueda pedir desde su mesa sin fricción:


   Escaneo QR   SplashPage       MenuPage         Carrito     CheckoutPage 
   (cámara)           (conexión)        (carta digital)        (revisión)        (pedir cuenta 

  1. Escaneo del QR: El comensal apunta la cámara de su móvil al código QR de la mesa. El navegador abre la URL embebida en el QR.
  2. SplashPage (/): La app se conecta con la mesa, valida los parámetros y redirige automáticamente al menú.
  3. MenuPage (/menu): El comensal navega la carta, explora categorías, toca productos para ver detalles, selecciona variaciones, marca extras, ajusta cantidades y añade todo al carrito.
  4. Carrito y Checkout (/checkout): El comensal revisa su selección, confirma el pedido, y desde la pantalla de checkout puede ver todos los pedidos acumulados de su mesa y pedir la cuenta cuando desee finalizar.

Descripción Visual por Página

SplashPage — Pantalla de Bienvenida (/)

Ruta: /
Componente: SplashPage.jsx
Propósito: Conectar al comensal con su mesa y validar el QR escaneado.

Cómo se ve

La SplashPage ocupa la totalidad de la pantalla del móvil. El fondo es de color blanco limpio en modo claro, o un navy oscuro #2C3E50 en modo oscuro. En el centro exacto de la pantalla, se encuentra el logo de la app — en la versión actual se muestra el texto "ZEUMAX QR" (branding heredado del sistema legacy), aunque el sistema objetivo es ZEUMAX. Debajo del logo, un spinner de carga animado gira suavemente, diseñado con un tono naranja brand #F5A623 que transmite energía y actividad.

Bajo el spinner, un texto en fuente Inter de tamaño medio y color muted #7F8C8D dice:

"Conectando con tu mesa..."

La tipografía es espaciada, amigable, sin sensación de urgencia. La animación del spinner está gestionada por Framer Motion, lo que le da una sensación orgánica y fluida, no mecánica.

Qué hace (técnica en lenguaje humano)

Mientras el comensal ve esta pantalla serena, la aplicación está trabajando intensamente en segundo plano:

  • Lee los parámetros del QR: La URL del QR contiene variables como ?table=...&branch=...&token=.... La app extrae tableId, branchId y branchTableToken.
  • Guarda la sesión de forma persistente: Mediante Zustand con persistencia en localStorage (clave qr-table), la app memoriza: tableId, tableName, branchId, branchTableToken. Esto significa que si el comensal recarga la página accidentalmente, no necesita reescanear el QR: la app ya sabe en qué mesa está.
  • Redirige automáticamente: Si todos los parámetros son válidos, la app navega instantáneamente a /menu sin que el usuario tenga que tocar nada.

Manejo de errores

Si el comensal escaneó un QR dañado, incompleto, o si falta algún parámetro obligatorio, la pantalla deja de mostrar el spinner y presenta:

  • Un icono de alerta (de la librería Lucide React) en color rojo suave.
  • Un mensaje de error claro: "No pudimos conectar con tu mesa. El código QR parece inválido o incompleto."
  • Un botón naranja brand ancho y redondeado: "Reescanear QR" — que al tocarse permite al usuario intentar con un nuevo escaneo.

Nota de seguridad: El token branchTableToken es único por mesa y actúa como una llave de sesión temporal. Sin él, no se puede realizar pedidos.

Nota para el usuario final

No necesitas registrarte ni iniciar sesión. No hay pantalla de login, no hay que crear contraseñas, no hay que dar tu correo. La experiencia es completamente anónima. Solo escanea y pide.


Ruta: /menu
Componente: MenuPage.jsx
Propósito: Mostrar la carta completa del restaurante, permitir exploración por categorías, selección de productos con personalización, y acumulación en el carrito.

Cómo se ve

La MenuPage es la página principal de la experiencia y está diseñada para maximizar la exploración visual y minimizar la fricción de pedido.

Header fijo en la parte superior:
- Un header compacto y fijo que permanece visible al hacer scroll. En el centro, aparece el nombre del restaurante en fuente Rubik, tamaño grande, peso semibold, color navy #2C3E50.
- A la izquierda, puede haber un icono de menú hamburguesa o simplemente el logo del restaurante como elemento decorativo.
- El header tiene un fondo blanco o semi-transparente con efecto blur sutil al hacer scroll, manteniendo la sensación de profundidad.

CategoryTabs — Tabs de categorías horizontal scrollable:
- Justo debajo del header, una fila de tabs redondeados estilo píldora, dispuestos horizontalmente. Se deslizan con el dedo (scroll horizontal táctil).
- Cada tab tiene un icono representativo (de la colección Lucide React: un cuchillo/tenedor para entrantes, una taza para bebidas, un trozo de carne para carnes, etc.) y el nombre de la categoría debajo o al lado.
- El tab activo se resalta con fondo naranja brand #F5A623 y texto blanco. Los tabs inactivos tienen fondo gris muy claro y texto muted #7F8C8D.
- Al cambiar de categoría, la transición es suave gracias a Framer Motion: los productos de la nueva categoría aparecen con una animación de fade-in sutil.

Grid de ProductCards:
- El cuerpo principal es un grid de tarjetas cuadradas (generalmente 2 columnas en móvil), con espaciado uniforme entre ellas.
- Cada ProductCard muestra:
- Una imagen real del producto ocupando la mitad superior de la tarjeta, con bordes redondeados superiores, object-fit cover, y un efecto de hover sutil al tocar (ligero escalado o brillo).
- El nombre del producto en fuente Rubik, semibold, navy oscuro, truncado con ellipsis si es muy largo.
- El precio grande y prominente en naranja brand #F5A623, fuente Inter, peso bold, con símbolo de moneda.
- Un badge en la esquina superior derecha si aplica: un badge amarillo/dorado que dice "Popular" (con icono de llama o estrella) o un badge verde #16A34A que dice "Nuevo" (con icono de sparkle). Los badges son pequeños píldoras redondeadas con texto blanco.
- El fondo del grid es blanco o gris muy claro #F9FAFB, dando respiro visual entre las tarjetas.

Botón flotante de carrito (FAB — Floating Action Button):
- En la esquina inferior derecha de la pantalla, un botón circular grande flota sobre el contenido.
- El círculo es de color naranja brand #F5A623, con un icono de carrito de compras (de Lucide React) en blanco en el centro.
- Sobre el círculo, en la esquina superior izquierda, hay un badge rojo circular con número blanco en bold que indica la cantidad total de items en el carrito. Si el carrito está vacío, el badge desaparece.
- Al tocar el FAB, el carrito se abre como un drawer lateral desde la derecha.


BottomSheet de Detalle de Producto

Cuando el comensal toca cualquier ProductCard, la pantalla se oscurece sutilmente desde el fondo (un overlay negro al 40% de opacidad), y desde la parte inferior de la pantalla emerge un BottomSheet blanco, redondeado en sus esquinas superiores (radio de borde grande), ocupando aproximadamente el 85% de la altura de la pantalla.

La apertura del BottomSheet es una animación suave de deslizamiento hacia arriba gestionada por Framer Motion, con una curva de easing natural que simula el comportamiento de una hoja física.

Dentro del BottomSheet, de arriba a abajo:

  1. Barra de agarre: Una pequeña barra gris horizontal centrada en la parte superior del sheet, indicando que se puede arrastrar hacia abajo para cerrar.

  2. Imagen grande del producto: Ocupa aproximadamente el 30% superior del sheet. Es la misma imagen del producto pero en alta resolución, con bordes redondeados internos. Si no hay imagen, muestra un placeholder gris con un icono de imagen de Lucide React.

  3. Nombre y descripción:

  4. Nombre del producto en Rubik, tamaño grande (h3), peso bold, navy #2C3E50.
  5. Descripción debajo, en Inter, tamaño pequeño, color muted #7F8C8D, líneas múltiples permitidas, con espaciado legible.

  6. VariationSelector (Selector de variaciones):

  7. Si el producto tiene variaciones (por ejemplo: "Pequeño / Mediano / Grande" o "Término medio / Término bien cocido"), aparece una sección con título "Elige una opción" en semibold.
  8. Las opciones se presentan como radio buttons estilizados: círculos pequeños a la izquierda, nombre de la variación al centro, y precio adicional (si aplica) a la derecha en muted o naranja según sea gratis o de pago.
  9. Al seleccionar una opción, el círculo se rellena con naranja brand #F5A623 y el texto se pone en bold. Las demás opciones se atenúan sutilmente.
  10. Solo se puede elegir una variación (comportamiento de radio button exclusivo).

  11. AddonSelector (Selector de extras/acompafiamientos):

  12. Si el producto tiene extras disponibles (por ejemplo: "Queso extra +$1.50", "Bacon +$2.00", "Aguacate +$1.00"), aparece una sección con título "Añadir extras".
  13. Cada extra es una fila con checkbox estilizado a la izquierda (cuadrado redondeado que se marca con una palomita blanca sobre fondo naranja cuando está activo), el nombre del extra en el centro, y el precio adicional a la derecha.
  14. Se pueden marcar múltiples extras simultáneamente (comportamiento de checkbox múltiple).
  15. Cada vez que se marca o desmarca un extra, el precio total se recalcula instantáneamente.

  16. QuantitySelector (Selector de cantidad):

  17. Una fila compacta centrada con tres elementos: un botón circular gris claro con icono - (menos), un número grande central en bold (inicialmente 1), y un botón circular naranja #F5A623 con icono + (más).
  18. La cantidad mínima es 1. Al llegar a 1, el botón de menos se deshabilita visualmente (se atenúa).
  19. Al aumentar la cantidad, el número central hace una pequeña animación de escala (bounce sutil de Framer Motion).

  20. Notas (textarea):

  21. Un campo de texto textarea sin borde visible, con fondo gris muy claro #F9FAFB, bordes redondeados, y placeholder gris que dice: "¿Alguna nota para la cocina? (alergias, preferencias, sin cebolla...)"
  22. El texto se escribe con el teclado nativo del móvil. El campo crece verticalmente si el texto es largo (auto-expand).

  23. PriceDisplay dinámico:

  24. Un precio total grande y prominente en la parte inferior del BottomSheet, justo encima del botón de acción. Está en naranja brand #F5A623, fuente Inter, peso bold, tamaño grande (h2 aproximadamente), con símbolo de moneda.
  25. Este precio cambia en tiempo real a medida que el usuario: selecciona variaciones, marca addons, ajusta cantidad. No hay necesidad de recalcular manualmente: la suma es instantánea y visible.

  26. Botón "Añadir al carrito":

  27. Un botón ancho de ancho completo (full-width dentro del sheet con padding horizontal), altura generosa (48-56px), fondo naranja brand #F5A623, texto blanco en bold, fuente Inter, bordes redondeados grandes (estilo píldora).
  28. El texto dice: "Añadir al carrito — $XX.XX" donde el precio es el total dinámico.
  29. Al tocar el botón, se siente una respuesta táctil: el botón se comprime ligeramente (efecto de active state), y el BottomSheet se desliza hacia abajo cerrándose con animación suave.
  30. Al cerrarse, el FAB del carrito actualiza su badge (el número rojo sobre el círculo naranja) con la nueva cantidad total de items. Puede aparecer una animación de confirmación sutil en el FAB (un pequeño bounce o ripple).

Flujo paso a paso en la MenuPage

  1. El comensal toca una tarjeta de producto en el grid.
  2. El BottomSheet emerge desde abajo con animación fluida, oscureciendo el fondo.
  3. El comensal selecciona una variación (radio button) si hay opciones disponibles.
  4. El comensal marca los extras que desea (checkboxes) — el precio total se actualiza en vivo.
  5. El comensal ajusta la cantidad con los botones + y -.
  6. Opcionalmente, el comensal escribe una nota para la cocina.
  7. El comensal toca el botón naranja "Añadir al carrito".
  8. El BottomSheet se cierra deslizándose hacia abajo.
  9. El FAB del carrito muestra ahora el nuevo número de items.
  10. El comensal puede seguir explorando la carta y añadiendo más productos, o tocar el FAB para revisar el carrito.

Carrito — CartDrawer

Al tocar el botón flotante naranja del carrito en la esquina inferior derecha, un drawer lateral se desliza desde el lado derecho de la pantalla, ocupando aproximadamente el 85% del ancho en móvil. El fondo detrás se oscurece al 40%. La apertura es animada con Framer Motion (deslizamiento desde derecha con easing natural).

Dentro del CartDrawer:

  • Header del drawer: Un título "Tu pedido" en Rubik bold, con un icono de X en la esquina superior derecha para cerrar. Cerrar puede hacerse tocando la X, tocando el fondo oscuro, o deslizando hacia la derecha.

  • Lista de CartItems: Cada item del carrito es una fila horizontal compacta que contiene:

  • Una miniatura cuadrada de la imagen del producto (esquinas redondeadas, tamaño ~48px).
  • El nombre del producto en Inter semibold, truncado si es largo, en una línea.
  • Debajo del nombre, las variaciones y extras seleccionados en texto pequeño muted #7F8C8D (por ejemplo: "Mediano, Queso extra, Bacon").
  • La cantidad (ej: "x2") en texto bold pequeño.
  • El precio total del item (cantidad × precio unitario con variaciones y extras) alineado a la derecha, en naranja brand #F5A623.
  • Un botón X pequeño y sutil al lado derecho para eliminar el item del carrito. Al tocarlo, el item se desvanece con una animación de salida y es removido.

  • CartSummary (Resumen del carrito): Fijado en la parte inferior del drawer, con fondo blanco y una línea divisoria sutil gris en la parte superior:

  • Subtotal: Texto a la izquierda, valor a la derecha, en Inter regular.
  • Propina: Si aplica, una línea con texto "Propina sugerida" y valor.
  • Total: Una línea destacada con texto "Total" en bold grande, y el monto total en naranja brand #F5A623, tamaño grande, bold.

  • Botón "Ver cuenta":

  • Un botón ancho, full-width, altura generosa, fondo navy #2C3E50 o naranja brand #F5A623 según el estado, texto blanco bold, bordes redondeados.
  • Texto: "Ver cuenta" o "Confirmar pedido".
  • Al tocarlo, el drawer se cierra y la app navega a /checkout.

Lógica de matching: El carrito usa una lógica inteligente de matching por variaciones y addons. Si el comensal añade el mismo producto dos veces con exactamente las mismas variaciones y extras, el carrito los agrupa en un solo item aumentando la cantidad, en lugar de crear duplicados. Si las variaciones o extras difieren, se crean items separados.


Datos y fallback

La MenuPage carga los productos y categorías desde el backend mediante la Fetch API nativa (no usa Axios ni React Query). El endpoint base es VITE_BASE_URL, con headers que incluyen Accept, Content-Type, X-localization (para idioma), y branch-id (para filtrar productos de la sucursal correcta).

Si por alguna razón hay problemas de red (Wi-Fi intermitente, servidor no responde), la app tiene un fallback de datos mock (mockData.js) que muestra productos de ejemplo para que el comensal pueda seguir navegando mientras la conexión se recupera. Esto evita pantallas de error o bloqueos.


CheckoutPage — Cuenta de Mesa (/checkout)

Ruta: /checkout
Componente: CheckoutPage.jsx
Propósito: Mostrar todos los pedidos de la mesa, permitir revisión final y pedir la cuenta al finalizar la comida.

Cómo se ve

La CheckoutPage es la página de cierre de la experiencia. No es solo el carrito actual: es la cuenta completa de la mesa.

Header:
- Un header compacto con un botón de flecha atrás (<) a la izquierda, que regresa a /menu.
- En el centro, el título: "Cuenta - Mesa X" donde X es el número de la mesa (tomado del tableStore). Fuente Rubik, bold, navy oscuro.
- El header puede tener un fondo blanco o un sutil degradado de naranja muy claro a blanco, manteniendo la coherencia visual.

Lista de órdenes pendientes:
- El cuerpo principal muestra una lista vertical de todos los pedidos que han sido realizados desde esa mesa, no solo los del carrito actual. Esto incluye pedidos que ya fueron enviados a la cocina en iteraciones anteriores.
- Cada orden se muestra como una tarjeta horizontal o fila con:
- Imagen miniatura del producto (esquinas redondeadas, ~48px).
- Nombre del producto en Inter semibold, navy oscuro.
- Cantidad (ej: "x2") en texto bold.
- Precio total alineado a la derecha, en naranja brand #F5A623.
- Los items están separados por líneas divisorias muy sutiles gris claro.
- Si un item tiene variaciones o extras, se muestran en texto pequeño muted debajo del nombre.

Total acumulado:
- Al final de la lista, una sección de resumen con fondo ligeramente diferente (gris muy claro o un card blanco con sombra sutil) que contiene:
- Subtotal: todos los items sumados.
- Impuestos: si aplica (según configuración del backend).
- Total grande: el monto final en naranja brand #F5A623, tamaño muy grande (h2), fuente Inter, bold, con símbolo de moneda.

Botón principal:
- Un botón ancho, full-width, altura generosa (56-64px), fondo naranja brand #F5A623, texto blanco bold, bordes redondeados grandes, situado justo debajo del total o fijo en la parte inferior de la pantalla.
- Texto: "Pedir la cuenta" o "Pagar en efectivo".
- Al tocarlo, aparece un Modal de confirmación centrado en pantalla con fondo oscuro al 50%.

Modal de confirmación:
- Un card blanco centrado, con bordes redondeados grandes, padding generoso.
- Un icono de alerta o información en color naranja brand o azul info.
- Texto: "¿Confirmar que deseas pedir la cuenta?" en Inter, tamaño medio, centrado.
- Dos botones horizontales:
- "No, cancelar": fondo gris claro, texto oscuro, bordes redondeados.
- "Sí, pedir cuenta": fondo naranja brand #F5A623, texto blanco bold, bordes redondeados.
- Al confirmar, la app envía la solicitud al backend y muestra un estado de éxito o redirige a una pantalla de confirmación.

Estados de pago:
- Los pedidos pueden tener estado "pending" (pendiente de pago) o "paid" (pagado). Los items pendientes se muestran con indicadores visuales sutiles; los pagados pueden aparecer atenuados o con un badge verde #16A34A que dice "Pagado".

EmptyState:
- Si la mesa no tiene pedidos aún (el comensal llegó a checkout sin haber pedido nada), la pantalla no es un listado vacío crudo. En su lugar, muestra:
- Un icono grande de un plato vacío o una taza (de Lucide React, en color muted #7F8C8D, tamaño 64px o más).
- Un texto amigable: "Aún no has pedido nada" en Inter, tamaño grande, muted.
- Un texto secundario: "Explora el menú y añade productos a tu pedido" en tamaño pequeño, muted más claro.
- Un botón naranja "Ir al menú" que redirige a /menu.

Qué hace (técnica)

  • Consulta el historial de la mesa: La CheckoutPage llama al endpoint GET /api/v1/table/order/list enviando el tableId y branchTableToken en los headers. El backend responde con todas las órdenes asociadas a esa mesa.
  • Muestra el acumulado: Suma todos los items de todas las órdenes pendientes y calcula el total.
  • Permite pedir la cuenta: El botón "Pedir la cuenta" no es un pago online directo. Es una solicitud al restaurante para que un camarero se acerque a la mesa y procese el pago en efectivo (o con datáfono del restaurante). Esto es una diferencia clave con la web de delivery.
  • No integra pasarela de pago: La app QR de mesa no tiene integración con Stripe, PayPal, ni otras pasarelas. El pago es presencial en el restaurante.

Nota: Los pedidos se realizan en tiempo real durante la experiencia en el menú (cada vez que se añade al carrito y se confirma, se envía a la cocina), pero la cuenta se pide al final de la comida, cuando el comensal está listo para irse.


Diferencias Clave con la Web Cliente (costumer-web)

Aunque comparten la misma paleta visual y la marca ZEUMAX, la App QR para Mesas y la Web Cliente (costumer-web) son dos productos diferentes diseñados para contextos distintos: uno para comensales dentro del restaurante, otro para clientes que piden delivery desde sus hogares.

Característica App QR para Mesas (app-qr) Web Cliente (costumer-web)
Login/Registro No requiere login. Experiencia anónima. Requiere registro e inicio de sesión.
Modo de servicio Comedor en el restaurante. Delivery a domicilio.
Wallet/Pagos No tiene wallet integrado. Tiene wallet y recargas.
Tracking de pedido No tiene tracking en tiempo real. Tracking con Socket.IO y mapa.
Checkout/Pago "Pedir la cuenta" (llama al camarero). Pago online con Stripe/PayPal.
Número de pantallas 3 pantallas (/, /menu, /checkout). 20+ páginas (home, restaurantes, carrito, checkout, órdenes, perfil, wallet, etc.).
HTTP Client Fetch API nativo. Axios.
Gestión de estado Zustand (4 stores, persistidos en localStorage). Redux (store global más complejo).
Conexión en tiempo real No usa Socket.IO. Usa Socket.IO para tracking y notificaciones.
Consulta de datos No usa React Query. Usa React Query (TanStack Query).
Librería de UI Headless UI + Framer Motion + Lucide React. Swiper, Formik, Toast, Radix UI, etc.
Caché de datos mockData.js como fallback offline. Caché de React Query.
Propósito Pedido sin contacto desde la mesa. Pedido a domicilio con seguimiento completo.

Coherencia visual: A pesar de las diferencias técnicas, ambos productos mantienen la misma identidad visual: paleta naranja #F5A623, navy #2C3E50, success #16A34A, fuentes Rubik e Inter. El comensal que luego use la app de delivery reconocerá inmediatamente la marca.


Estado y Persistencia

La App QR para Mesas está diseñada para ser resiliente a recargas accidentales y para que el comensal no pierda su progreso si la pantalla se apaga o si cambia de aplicación momentáneamente.

Datos de la mesa (persistencia en localStorage)

Los parámetros de la mesa se almacenan en el navegador mediante Zustand con persistencia bajo la clave qr-table:

Dato almacenado Clave Descripción
tableId qr-table Identificador único de la mesa dentro del restaurante.
tableName qr-table Nombre o número legible de la mesa (ej: "Mesa 5", "Terraza 2").
branchId qr-table Identificador de la sucursal del restaurante.
branchTableToken qr-table Token de seguridad único para validar la sesión de esa mesa.

¿Qué significa esto para el usuario? Si el comensal recarga la página accidentalmente, si el navegador se cierra, o si el móvil se bloquea y vuelve a abrir la app, no necesita reescanear el QR. La app recuerda en qué mesa está sentado y retoma la experiencia exactamente donde la dejó. La sesión persiste en localStorage hasta que el comensal cierra explícitamente la cuenta o el restaurante invalida el token.

Carrito (persistencia en localStorage)

El contenido del carrito también se persiste bajo la clave qr-cart:

  • Items seleccionados: Productos, variaciones, extras, cantidades, notas y precios totales.
  • Lógica de matching: Si un producto con las mismas variaciones y extras ya existe en el carrito, se agrupa incrementando la cantidad en lugar de duplicarse.

¿Qué significa esto para el usuario? Si el comensal minimiza el navegador para atender una llamada, luego vuelve y recarga la página, su carrito sigue intacto. Puede añadir productos, cerrar la app, volver horas más tarde (si la sesión del token sigue válida), y su carrito seguirá ahí.

Modo oscuro (persistencia en localStorage)

La preferencia de tema se guarda bajo la clave qr-theme:

  • Modo claro: Fondos blancos, textos navy, tarjetas limpias.
  • Modo oscuro: Fondos navy #2C3E50, textos claros, tarjetas con fondo gris oscuro, badges manteniendo su color para legibilidad.

¿Qué significa esto para el usuario? Si el comensal prefiere el modo oscuro y lo activa desde el menú o la configuración del navegador, la app recordará esa preferencia en futuras visitas (siempre que sea la misma sesión de mesa).

Stores de Zustand (resumen técnico)

Store Clave de persistencia Datos gestionados
useThemeStore qr-theme Modo light/dark del tema visual.
useTableStore qr-table Identificación de la mesa y sucursal (tableId, tableName, branchId, branchTableToken).
useCartStore qr-cart Items del carrito con lógica de agrupación por variaciones y addons.
useOrderStore qr-orders Órdenes locales con estado (pending, paid).

Notas Técnicas para el Usuario

Aunque el manual está escrito para el comensal final, es útil entender algunos aspectos técnicos de la app que impactan directamente en la experiencia de uso:

Es una PWA, no una app nativa

La App QR para Mesas no se descarga desde la App Store ni Google Play. Es una Progressive Web App (aplicación web progresiva) que funciona directamente en el navegador móvil del comensal. Esto significa:

  • Funciona en cualquier navegador móvil moderno: Chrome, Safari, Firefox, Edge, Brave, Samsung Internet.
  • No ocupa espacio de almacenamiento en el móvil.
  • Se actualiza automáticamente cada vez que el restaurante publica mejoras (no hay que actualizar manualmente).
  • En algunos navegadores, el usuario puede añadir un acceso directo a la pantalla de inicio, que se ve y siente como una app nativa.

El restaurante controla la disponibilidad

La app no decide qué productos mostrar: el backend del restaurante (gestionado por el sistema ZEUMAX) controla:

  • Qué productos están disponibles o agotados.
  • Qué categorías están activas.
  • Qué variaciones y extras tiene cada producto.
  • Los precios actualizados.
  • Las imágenes de los productos.

Si un producto desaparece del menú, es porque el restaurante lo desactivó desde su panel de administración. Si un precio cambia, se actualiza en tiempo real para todos los comensales que escaneen el QR.

Las órdenes van directamente a la cocina y al manager

Cuando el comensal añade un producto al carrito y confirma el pedido (o cuando el pedido se envía desde el drawer), la app realiza una petición POST al endpoint /api/v1/table/order/place. Esta orden viaja instantáneamente a:

  • El Orden Receiver del manager: Un panel de gestión donde el personal del restaurante ve todas las órdenes entrantes por mesa.
  • La cocina: Dependiendo de la configuración del restaurante, las órdenes pueden imprimirse automáticamente en una impresora de tickets en la cocina o aparecer en una pantalla de despacho (KDS — Kitchen Display System).

Latencia: La app usa Fetch API nativa, lo que significa que las peticiones son rápidas y directas. No hay intermediarios ni reintentos complejos: si la red falla, el comensal verá un mensaje de error amigable y podrá intentar de nuevo.


API y Endpoints (Referencia Rápida)

Para completitud técnica del manual, estos son los endpoints que la App QR consume desde el backend:

Método Endpoint Descripción
GET /api/v1/categories Lista de categorías del menú activas para la sucursal.
GET /api/v1/categories/products/{id} Productos de una categoría específica.
GET /api/v1/products/latest Productos recién añadidos al menú (para badge "Nuevo").
GET /api/v1/products/popular Productos más vendidos (para badge "Popular").
GET /api/v1/products/details/{id} Detalle completo de un producto: variaciones, extras, descripción, imagen.
POST /api/v1/table/order/place Enviar un nuevo pedido desde la mesa al backend.
GET /api/v1/table/order/list Obtener todos los pedidos de la mesa actual (usado en checkout).

Headers comunes en todas las peticiones:
- Accept: application/json
- Content-Type: application/json
- X-localization: es (o el idioma activo)
- branch-id: {branchId} (identificador de la sucursal)

Error handling: La app usa una clase ApiError personalizada que captura el código HTTP (status) y el cuerpo de respuesta (data), mostrando mensajes de error amigables al comensal sin exponer detalles técnicos internos.


Paleta Visual y Tipografía

Elemento Color Uso
Brand (Naranja) #F5A623 Botones principales, precios, badge de carrito, acentos, FAB.
Navy #2C3E50 Textos principales, fondos en modo oscuro, headers.
Success (Verde) #16A34A Estados "pagado", badges "Nuevo", confirmaciones positivas.
Muted (Gris) #7F8C8D Textos secundarios, descripciones, placeholders, iconos inactivos.
Fondo claro #FFFFFF / #F9FAFB Fondos de pantalla, tarjetas, drawer.
Fuente Uso
Rubik Títulos, nombres de productos, headers, textos de impacto.
Inter Precios, cuerpo de texto, descripciones, botones, labels.

Resumen de la Experiencia

La App QR para Mesas de ZEUMAX condensa la experiencia de pedido en restaurante en tres pantallas elegantes y altamente optimizadas:

  1. SplashPage (/): Escanea, conecta, redirige. Sin fricción, sin registro.
  2. MenuPage (/menu): Explora la carta, personaliza productos, acumula en el carrito. Bottom sheets fluidos, precios dinámicos, animaciones suaves.
  3. CheckoutPage (/checkout): Revisa toda la cuenta de la mesa, pide la cuenta al camarero. Sin pagos online, sin complicaciones.

Es una experiencia anónima, persistente, resiliente y visualmente coherente con la marca ZEUMAX, diseñada para que cualquier comensal, desde el más tecnófilo hasta el más tradicional, pueda pedir desde su mesa con la misma naturalidad con la que levantaría la mano para llamar al camarero. Solo que ahora, con un QR, lo hace más rápido.


Documento: 04-app-qr-mesa.md
Sistema: ZEUMAX (ZEUMAX)
Módulo: app-qr
Ruta del código fuente: D:\zeumax\zeumax\app-qr
Nota de branding: El código fuente actual contiene referencias a "ZEUMAX QR" en lib/constants.js (branding legacy). El sistema objetivo es ZEUMAX. Se recomienda actualizar el branding en una futura revisión de código.