Saltar a contenido

Arquitectura del frontend

El frontend es una aplicación Next.js (App Router) que sirve tres audiencias desde un mismo proyecto: el comensal (flujo QR → menú → comanda → pago), el personal de sala/administración y una zona de demos. Esta página explica cómo está organizada la app, cómo habla con el backend (capa de API tipada), dónde vive el estado de cliente y cómo se sincroniza en tiempo real mediante SSE.

No es el Next.js que ya conoces

El proyecto usa una versión de Next con breaking changes respecto a versiones anteriores: APIs, convenciones y estructura de ficheros pueden diferir. Antes de escribir código, consulta la guía correspondiente en node_modules/next/dist/docs/ y atiende a los avisos de deprecation (ver frontend/AGENTS.md).

Zonas de la aplicación (app/)

El App Router organiza las rutas por audiencia:

Zona Ruta Propósito
Landing app/(landing)/ Página de marketing pública (grupo de ruta sin segmento de URL), con components/ y sections/.
Comensal app/menu/[restaurantId]/ Carta del restaurante (MenuClient.tsx).
Comensal (QR) app/menu/[restaurantId]/table/[sessionToken]/ Punto de entrada al escanear el QR de mesa: resuelve el session_token y abre el chat/comanda de la sesión.
Comensal app/checkout/ Funnel de cuenta y pago.
Comensal app/order/[orderId]/ Detalle/seguimiento de un pedido.
Cliente (auth) app/auth/ login, register y callback de OAuth del comensal.
Admin app/admin/ Panel de gestión: login, auth, onboarding, restaurants, super.
Demo app/demo/ Pantallas de demostración (p. ej. error-system).
Internas app/api/ Rutas de servidor (incluye app/api/admin/).

El árbol raíz incluye además layout.tsx, page.tsx, error.tsx, not-found.tsx y globals.css.

Distinción cliente / sala

La zona del comensal se sincroniza por el canal de sesión (token de mesa). La zona admin incluye el dashboard de sala, que usa un canal SSE distinto autenticado con el token de administrador. Ver Control de sala.

Capa de API

Toda la comunicación HTTP con el backend pasa por una capa tipada en frontend/lib/api/.

Cliente del comensal: client.ts

frontend/lib/api/client.ts exporta el objeto api, un wrapper sobre fetch organizado por dominios: auth, customers, menu, tables, recommendations, sessions, cart, comanda, tab, bill, review, chat, managerCall y admin. Características:

  • Maneja autenticación opcional por bearer token (requireAuth), guardado en sessionStorage vía setAccessToken / getAccessToken.
  • Normaliza errores en APIClientError (con status y data) y ofrece safeFetch que devuelve un APIResponse<T> ({ success, data | error }).
  • Convierte 204 No Content en un objeto vacío y serializa el body a JSON automáticamente.

Endpoints orders deprecados

Las operaciones de pedido legadas en api.orders (create, createDraft, get, updateStatus) lanzan error a propósito. Usa los endpoints de comanda (api.comanda.*): GET /sessions/{token}/draft, POST /sessions/{token}/lines, POST /sessions/{token}/submit, etc. Ver Comanda.

Cliente base y cliente de administración: baseClient.ts / adminClient.ts

  • frontend/lib/api/baseClient.ts define la clase abstracta BaseApiClient, que centraliza la construcción de URL, cabeceras, request<T>, requestBlob (descargas) y safeFetch. Las subclases implementan getAuthHeaders() y getCacheMode().
  • frontend/lib/api/adminClient.ts define AdminApiClient extends BaseApiClient (instancia exportada adminApiClient). Inyecta el token desde useAdminAuthStore, fuerza cache: 'no-store' y reporta errores con reportApiError.

El cliente admin compone módulos de endpoints por dominio (de lib/api/endpoints/admin/) en namespaces de solo lectura, entre otros:

  • products, categories, menus, chat, promotions, knowledge, restaurants
  • recommendations, flows, users, audit, analytics
  • tables, rooms, batches, managerCalls, errors, orders
  • manager (Control de sala): getSessions, getKitchenLoad, deliver, courtesyRound, setKitchenOverload.

Tipos: generados a mano y por codegen

  • Tipos del contrato OpenAPI: se generan con npm run codegen (script openapi-typescript), que lee el snapshot del contrato (backend/tests/contract/openapi_snapshot.json, configurable vía OPENAPI_SNAPSHOT) y escribe lib/api/generated/openapi.d.ts. Los helpers derivados viven en lib/api/generated/helpers.ts (p. ej. TabView, BillResponse, PayBillResponse, ReviewResponse).
  • Tipos SSE escritos a mano: las superficies SSE no están en OpenAPI, por lo que se mantienen manualmente en frontend/lib/api/sse-types.ts. Su fuente de verdad es backend/tests/contract/sse_events_inventory.md. Ver Eventos SSE.

Evolucionar el contrato

Cuando cambie el contrato del backend, regenera los tipos con npm run codegen y cablea el frontend en el mismo cambio. Ver Evolucionar el contrato.

Configuración de la URL base

frontend/lib/config/api.ts centraliza el cálculo de la URL base de la API mediante getApiBaseUrl() (y la constante API_CONFIG.baseUrl). El orden de resolución es:

  1. Variable de entorno de build NEXT_PUBLIC_API_URL, si existe.
  2. Detección en runtime (solo en navegador): host conocido de demo → API de demo; en otro caso, mismo origen + /api/v1.
  3. Por defecto en desarrollo: http://localhost:8000/api/v1.

Ver Configuración.

Estado de cliente (stores Zustand)

El estado se gestiona con Zustand, un store por dominio en frontend/store/:

  • Comensal: cartStore, chatStore, sessionStore, batchStore, tabStore, billStore.
  • Admin: adminAuthStore, adminRestaurantStore, adminUserStore, adminUIStore.
  • Transversales: authStore, uiStore (con helper toast).

store/index.ts reexporta los stores de uso más común; tabStore y billStore se importan directamente desde su módulo. Los hooks de tiempo real despachan los eventos SSE a estos stores (carrito, chat, batch, tab, bill, sesión).

Tiempo real (hooks SSE)

El frontend consume dos streams SSE distintos, ambos con el sobre { type, data, timestamp }. Ambos hooks reconectan con backoff exponencial (máx. 30 s) y fuerzan reconexión en visibilitychange / online / pageshow, porque los navegadores móviles cierran EventSource al pasar a segundo plano.

Canal del comensal: useSessionSync

frontend/lib/hooks/useSessionSync.ts conecta a GET /sessions/{token}/events (vía EventSource) y reparte los eventos a los stores correspondientes. Tipos de evento que maneja, entre otros:

  • Sesión/carrito/chat: connected, cart_updated, chat_message, chat_streaming, typing_started, typing_stopped, session_closed, guest_count_updated, heartbeat.
  • Ciclo de vida del batch: batch_submitted, batch_ai_result, batch_manager_result, batch_approved, batch_in_progress, batch_served, batch_ready, batch_updated.
  • Llamada al responsable: manager_call_created, manager_call_responded.

Al reconectar (onopen en una reconexión) hace una llamada REST de catch-up (api.comanda.getActive) para recuperar eventos perdidos mientras estuvo desconectado, ya que la cola del backend es por suscriptor y no reenvía histórico.

Canal de sala: useManagerSessionSync

frontend/lib/hooks/useManagerSessionSync.ts alimenta el dashboard de sala. Conecta a GET /admin/manager/restaurants/{id}/events y, como EventSource no puede enviar cabeceras de autenticación, el token de admin viaja como query param (?token=<admin jwt>, tomado de useAdminAuthStore). El hook:

  1. Siembra el estado con una llamada REST (adminApiClient.manager.getSessions).
  2. Fusiona los frames sala_snapshot, table_state_changed y silent_table_alert (tipados en sse-types.ts).
  3. Devuelve sessions, status (connecting | connected | disconnected), lastUpdated, silentAlert, loading y un refetch manual.

Referencias