Completed
La Urbana
Desarrollo full-stack · Angular + Node.js + Strapi CMS + PWA + Impresión térmica automatizada
La Urbana es una plataforma digital completa para la gestión de pedidos online de un restaurante, desarrollada íntegramente desde cero. El sistema abarca desde la experiencia del cliente en el navegador hasta la impresión física del ticket en cocina, sin intervención humana en el proceso. Se trata de un ecosistema de tres piezas que trabajan en perfecta sincronía: una Single Page Application (SPA) en Angular como frontend, una API REST con Strapi CMS como backend headless, y un microservicio de impresión en Node.js que cierra el ciclo operativo del restaurante.

Arquitectura General
El proyecto sigue una arquitectura desacoplada de tres capas: Cliente Angular PWA ←→ Strapi API REST ←→ Base de datos Microservicio Node.js Print - Impresora térmica local
Esta separación de responsabilidades permite escalar, mantener y desplegar cada capa de forma independiente. El frontend no sabe nada de la base de datos; el microservicio de impresión no sabe nada del frontend. Todo se comunica a través de contratos HTTP bien definidos.
Navegación y Routing
La aplicación implementa un sistema de rutas con Angular Router que cubre todo el flujo de usuario:
- /home — Landing page con propuesta de valor del restaurante
- /categorias/:categoria — Exploración de productos por categoría con parámetro dinámico en URL
- /carta — Vista completa de la carta del restaurante
- /checkout — Proceso de compra con autenticación integrada
- /finaliza — Confirmación de pedido y persistencia en backend
- /privacidad y /condiciones — Páginas legales
El routing incluye redirección automática a la categoría por defecto (`entrantes`) cuando se accede a `/categorias` sin parámetro, y validación de que el parámetro de categoría existe realmente en la base de datos antes de renderizar productos. Si la categoría no existe, redirige automáticamente evitando estados rotos.
Gestión de Categorías y Productos
El componente `CategoriasComponent` es el núcleo de la experiencia de compra. Combina dos observables de forma encadenada usando el operador `switchMap` de RxJS: primero obtiene todas las categorías disponibles, y dentro de esa suscripción, reacciona al parámetro de URL para cargar los productos correspondientes. Este patrón reactivo garantiza que el cambio de categoría en la URL siempre dispara una nueva carga de productos sin necesidad de recargar la página.
Los productos se muestran con imagen, nombre, descripción, precio y los iconos de alérgenos correspondientes. La lógica de alérgenos es especialmente cuidada: el sistema evalúa 14 tipos de alérgenos distintos (gluten, lácteos, huevos, pescados, crustáceos, moluscos, cacahuetes, soja, frutos de cáscara, apio, mostaza, sésamo, sulfitos y altramuces) y muestra únicamente los iconos de los que están presentes en cada producto. Al hacer clic en el icono de información de un producto, se despliega un modal con el detalle completo de sus alérgenos.
Carrito de Compra con Cifrado AES
Uno de los elementos más destacados del proyecto es la implementación de un carrito de compra persistente en `localStorage` con cifrado AES-256 mediante la librería CryptoJS. El `CryptoService` implementa PBKDF2 con 100 iteraciones y SHA1 como hasher para derivar la clave e IV a partir de una clave maestra, cifrando el contenido del carrito en UTF-16LE antes de almacenarlo.
Esto significa que aunque un usuario inspeccione el `localStorage` de su navegador, encontrará únicamente texto cifrado ilegible. La decisión de cifrar el carrito en cliente responde a un requisito de integridad: se dificulta la manipulación manual de precios o productos por parte del usuario.
El carrito soporta:
- Añadir productos con feedback visual inmediato (toast animado)
- Eliminar productos individualmente
- Cálculo del precio total en tiempo real mediante `Array.reduce`
- Persistencia entre sesiones (el carrito sobrevive a cerrar el navegador)
- Recuperación del último pedido realizado para repetirlo con un clic
Sistema de Autenticación JWT
La autenticación está integrada directamente en el flujo de compra, sin páginas separadas de login. El componente `CategoriasComponent` incluye un modal con dos modos: **login** y **registro**, alternables sin recargar la página.
El flujo de autenticación: 1. El usuario introduce credenciales 2. Se envía a `POST /auth/local` (Strapi nativo) 3. Se recibe un JWT que se almacena en `localStorage` bajo la clave `laurbana` 4. El token se usa en todas las llamadas autenticadas posteriores como Bearer token 5. Al cerrar sesión, se elimina el token del localStorage
El registro valida en cliente todos los campos requeridos (nombre, email con regex RFC 5322, contraseña, dirección, localidad y teléfono) antes de enviar la petición, mostrando mensajes de error específicos por campo. El usuario registrado queda automáticamente autenticado tras el registro.
Los usuarios autenticados pueden además **editar sus datos de perfil** (dirección, localidad, teléfono) directamente desde el modal, con feedback de éxito o error y cierre automático del modal tras la operación.
Historial de Pedidos y Recuperación
Para usuarios autenticados, el sistema consulta automáticamente el historial de pedidos al cargar la página. Recupera el pedido más reciente ordenando los carritos por `createdAt` de forma descendente, y ofrece al usuario la opción de repetir su último pedido con un solo clic, cargando todos los productos directamente en el carrito actual.
Esta funcionalidad implica una cadena de tres llamadas API encadenadas: obtener datos del usuario → buscar sus carritos → obtener los productos de cada carrito → obtener el detalle de cada producto. Todo gestionado con suscripciones RxJS y limpieza correcta en `ngOnDestroy` para evitar memory leaks.
Vista de Carta Completa
El componente `CartaComponent` ofrece una vista diferente: la carta completa del restaurante organizada por categorías, pensada para consulta sin necesidad de añadir al carrito. Las categorías se filtran por el atributo `show` (solo se muestran las marcadas como visibles) y se ordenan por el atributo `order`. Los productos dentro de cada categoría se ordenan alfabéticamente. Esta vista también muestra los iconos de alérgenos de cada producto.
Proceso de Checkout y Finalización
El `CheckoutComponent` gestiona la revisión final del pedido. Muestra todos los productos del carrito con sus precios, el total, y los datos del usuario autenticado. Si el usuario no está autenticado, muestra el formulario de login/registro antes de permitir continuar.
Al confirmar el pago, se activa una animación en el botón y tras 1.5 segundos se navega a `/finaliza`. El `FinalizarComponent` simula la espera de confirmación de la pasarela de pago (también con un timeout de 1.5s) y luego persiste el pedido en el backend: crea un nuevo carrito en la base de datos y asocia cada producto del carrito local a ese carrito mediante la tabla `productoscarritos`. Al destruirse el componente, si la compra fue exitosa, limpia el carrito del localStorage.
Animaciones con Angular Animations
Varios componentes implementan animaciones de entrada/salida usando el módulo `@angular/animations`. La animación `flyInOut` hace que los elementos entren deslizándose desde la izquierda y salgan hacia la derecha, con una duración de 100ms. Esto se aplica a elementos del carrito y notificaciones, dando fluidez a la interfaz sin depender de librerías externas de animación.
Progressive Web App (PWA)
La aplicación está configurada como PWA completa:
- Service Worker registrado con `@angular/service-worker`, activado en producción
- Web App Manifest (`manifest.webmanifest`) con iconos para todas las resoluciones (72x72 hasta 512x512), incluyendo iconos específicos para iOS (apple-touch-icon en múltiples tamaños)
- Notificaciones Push implementadas con la API Web Push. El `HomeComponent` solicita permiso al usuario y se suscribe usando una clave VAPID pública, permitiendo enviar notificaciones push desde el servidor incluso cuando la app no está abierta
- La app es instalable en dispositivos móviles y de escritorio como aplicación nativa
Gestión de Suscripciones RxJS
Un detalle de calidad del código es la gestión correcta de suscripciones. Todos los componentes que realizan llamadas HTTP usan un objeto `Subscription` agregado donde se añaden todas las suscripciones individuales. En `ngOnDestroy` se llama a `subscriptions.unsubscribe()` para cancelar todas de una vez, evitando memory leaks en una SPA donde los componentes se crean y destruyen frecuentemente.
Menú Responsivo con Detección de Clics Externos
El `MenuComponent` implementa un menú hamburguesa para móvil con un comportamiento especialmente cuidado: usa `@HostListener('document:click')` para detectar clics en cualquier parte del documento. Si el clic ocurre fuera del elemento del menú y el menú está abierto, lo cierra automáticamente. Esto replica el comportamiento nativo esperado por los usuarios sin necesidad de librerías externas.
El menú también es consciente de la ruta actual gracias a la suscripción a `router.events`, lo que permite aplicar estilos activos a los enlaces de navegación.
Backend — Strapi CMS Headless
El backend se implementó con Strapi, un CMS headless open-source que genera automáticamente una API REST completa a partir de los modelos de datos definidos. Esto permitió centrarse en la lógica de negocio y el frontend sin escribir código de servidor desde cero.
Los modelos de datos principales son:
- Categorias: nombre, show (boolean), order (integer)
- Productos: nombre, descripción, precio, imagen (media), categoría (relación), y 14 campos boolean de alérgenos
- Carritos: usuario (relación), estado, timestamps automáticos
- ProductosCarritos: tabla pivote entre carritos y productos con cantidad
La API usa el sistema de filtros y populate de Strapi v4 para queries complejas como obtener productos de una categoría con su imagen, o recuperar un carrito con todos sus productos y datos de usuario en una sola llamada.
La autenticación se gestiona con el plugin nativo de Strapi (JWT + bcrypt), que proporciona endpoints de registro, login y gestión de usuarios sin configuración adicional.
Microservicio de Impresión — Node.js
El microservicio de impresión es quizás la pieza más innovadora del ecosistema. Es un servidor **Express** ligero que corre en la red local del restaurante y cierra el ciclo operativo: cuando llega un pedido, el ticket se imprime físicamente en cocina de forma automática.
Generación del Ticket
El ticket se genera como HTML con estilos CSS optimizados para papel térmico de 80mm de ancho, el estándar en impresoras de punto de venta. El CSS define:
- Ancho fijo de 70mm con márgenes de 5mm
- Fuente PT Sans para legibilidad en papel térmico
- Tabla de productos con columnas de nombre (47% de ancho) y precio (alineado a la derecha)
- Separadores con bordes sólidos y punteados para cabecera y total
- Altura dinámica del PDF: se calcula en función del número de productos (`(n_productos * 11) + 90 mm`), evitando espacios en blanco o cortes en el ticket
El ticket incluye el logo del restaurante (cargado desde la URL de producción), datos del cliente (nombre, dirección, localidad, teléfono), listado de productos con precios individuales y total calculado.
Resolución Paralela de Productos
Un detalle técnico destacable es la obtención de los detalles de cada producto del pedido. En lugar de hacer las llamadas en serie (lo que sería lento para pedidos con muchos productos), se usa `Promise.all()` con un array de promesas creadas dinámicamente, una por producto. Todas las llamadas se lanzan en paralelo y se espera a que todas resuelvan antes de generar el ticket. Esto minimiza el tiempo entre la recepción del pedido y la impresión del ticket.
Integración con Impresora
La librería `pdf-to-printer` permite enviar el PDF generado directamente a cualquier impresora instalada en el sistema Windows, especificando el nombre de la impresora y opciones de escala. El archivo PDF temporal se elimina tras la impresión (o tras un error) para no acumular archivos en disco.
El servidor expone también métodos para listar las impresoras disponibles y obtener la impresora por defecto, facilitando la configuración inicial.
Impacto y métricas
- La aplicación está desplegada y en uso real en el restaurante La Urbana. El flujo completo — desde que el cliente navega la carta hasta que el ticket se imprime en cocina — funciona de forma autónoma sin intervención del personal para gestionar los pedidos digitales.
- La arquitectura SPA elimina las recargas de página completa. La navegación entre categorías, la adición de productos al carrito y el proceso de checkout son instantáneos desde la perspectiva del usuario. Las animaciones de entrada/salida y los toasts de feedback hacen la experiencia perceptiblemente más pulida que una web tradicional.
- Gracias a la configuración PWA completa, los clientes habituales pueden instalar La Urbana en su móvil o escritorio como si fuera una app nativa, con icono en pantalla de inicio y experiencia de pantalla completa. Las notificaciones push permiten al restaurante comunicarse con los usuarios suscritos incluso cuando la app no está abierta.
- El microservicio de impresión elimina la necesidad de que el personal de sala transcriba los pedidos digitales a papel. El ticket se imprime automáticamente en el momento en que el cliente confirma el pedido, con todos los datos necesarios para la preparación y entrega: nombre del cliente, dirección, teléfono y listado de productos con precios.
Galería
Galería de La Urbana. Usa las flechas o desliza para cambiar de imagen. Pulsa Enter o haz clic para ampliar.