FactuGES_ERP/docs/frontend/frontend-architecture.md

8.8 KiB

Arquitectura frontend

Propósito: explicar cómo está construido el frontend. Este documento, junto con UI system y UI patterns, forma la fuente de verdad (SSOT) de frontend/UI.

1. Stack

El frontend usa React 19, TypeScript estricto, React Router, TanStack Query, React Hook Form, Axios, Tailwind CSS v4, Shadcn/UI y @repo/rdx-ui.

2. Arquitectura por capas

El flujo habitual de una feature es:

API → DTO → adapter → Entity → query hook → controller → page controller → Page → UI
  • shared/api: transporte y contratos HTTP.
  • shared/adapters: transforma DTOs en entidades de frontend.
  • shared/entities: modelos consumidos por la aplicación.
  • shared/hooks: queries, mutations y query keys de TanStack Query.
  • controllers: orquestación funcional, formularios, navegación y efectos.
  • page controllers: componen el estado completo de una pantalla.
  • Page: conecta controller y componentes; debe tener poco estado propio.
  • ui: renderiza props preparadas y emite eventos.

La UI no debe contener reglas de dominio, fetch manual ni traducciones entre DTO y entidad. Los controllers no deben convertirse en servicios de dominio.

3. Módulos frontend

Cada módulo publica sus rutas y su cliente desde modules/<module>/src/web. Las features se organizan por caso de uso (list, create, update, detail) y comparten transporte, entidades y hooks mediante shared cuando existe reutilización real.

Las dependencias deben apuntar hacia contratos compartidos, no hacia páginas concretas. Un componente de dominio puede envolver una foundation visual si aporta defaults o vocabulario propios, pero no debe recrear el layout de página.

4. Routing y layouts

Las rutas declaran su shell en handle.layout:

  • auth: login y recuperación, bajo AuthLayout.
  • app-sidebar: dashboard, listados, settings y navegación general.
  • app-fullscreen: tareas focalizadas de create/update/detail y Account cuando la ruta así lo declara.

apps/web/src/routes/app-routes.tsx agrupa las rutas por ese valor y las monta bajo el layout correspondiente. protected y requireCompany expresan guards independientes del shell. La elección debe seguir las rutas productivas del área; no se infiere únicamente por el nombre de la pantalla.

5. Shells de aplicación

Application shell
├── AppSidebarLayout
│   ├── AppSidebar
│   ├── AppTopbar
│   └── AppMain
├── AppFullscreenLayout
│   └── AppMain
└── AuthLayout
  • AppSidebarLayout proporciona navegación y topbar.
  • AppFullscreenLayout ocupa el viewport para una tarea focalizada.
  • AuthLayout centra las pantallas públicas de identidad.
  • AppMain entrega el espacio disponible. No es propietario del scroll de una página.

Dentro de AppMain, una pantalla usa RdxPageFrame; el sistema visual y sus reglas se documentan en UI system.

AppMain
└── RdxPageFrame
    ├── header
    ├── content
    │   └── RdxPageContainer
    │       └── RdxPageContent
    └── footer opcional   
RdxPageFrame
→ estructura y scroll

RdxPageContainer
→ ancho y padding

RdxPageContent
→ composición de los hijos principales

Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales. En Create/Update, Detail/Show y LIST, la composición canónica es RdxPageFrame → RdxPageContainer → RdxPageContent: errores, alertas, secciones semánticas y contenido funcional son hijos directos de RdxPageContent; los dialogs son hermanos del frame; RdxPanel se reserva para secciones semánticas reales.

6. Identity y sesión

El runtime actual se compone en apps/web/src/app.tsx:

DataSourceProvider
└── IdentityAuthSessionProvider
    └── router

El login usa POST /identity/auth/login. Tras autenticar:

  1. persiste accessToken y refreshToken;
  2. elimina cualquier empresa activa anterior;
  3. obtiene la sesión con GET /identity/auth/session;
  4. carga GET /companies/available;
  5. resuelve la empresa activa y el destino autenticado.

Al restaurar una sesión, el provider valida el token, hidrata identidad y empresas y limpia ambos estados si la restauración falla. El frontend nuevo usa @erp/identity/client; no debe introducir dependencias nuevas sobre el runtime legacy de auth.

7. Selección de empresa

  • Sin empresas: navega a /no-companies; no existe activeCompanyId y el logout permanece disponible.
  • Una empresa: se selecciona automáticamente.
  • Varias empresas: navega a /company-selection hasta que el usuario elige.
  • Empresa persistida no disponible: vuelve a aplicar las tres reglas anteriores.

El switcher autenticado consume availableCompanies, activeCompany y changeActiveCompany() desde useIdentityAuthSession(). No obtiene tokens ni repite la consulta directamente.

8. Contexto tenant y X-Company-Id

Las requests tenant-scoped incluyen X-Company-Id cuando existe una empresa activa. No lo incluyen:

POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET  /identity/auth/session
GET  /companies/available

Los guards de compañía resuelven la navegación; los componentes visuales no deben inspeccionar headers HTTP ni decidir tenancy.

9. Refresh de Axios

Ante un 401 autenticado, el interceptor:

  1. inicia POST /identity/auth/refresh si existe refresh token;
  2. comparte una única refreshPromise entre requests concurrentes;
  3. persiste los tokens renovados;
  4. reintenta una sola vez la request original mediante _retry;
  5. limpia sesión y navega a /login si el refresh falla.

Login, refresh, logout, session y companies available están excluidos del bucle de refresh y del header tenant. Una request tenant reintentada conserva el contexto de empresa.

10. Navegación y contexto

La navegación derivada de una operación pertenece al controller. Las páginas conectan callbacks y las vistas puras no importan React Router. El retorno desde editores debe reutilizar el mecanismo de returnTo/navegación existente en vez de reconstruir rutas localmente.

El estado remoto permanece en TanStack Query. No se duplica en estado local salvo estado efímero de presentación. Queries, invalidaciones y mutations viven en hooks compartidos del dominio.

11. Reglas de contribución arquitectónica

  • Mantener TypeScript estricto y import type en cabecera.
  • Naming y código en inglés; documentación y comentarios arquitectónicos en español.
  • Preferir composición a frameworks CRUD o controllers genéricos.
  • No introducir acceso a red en UI ni reglas de negocio en Pages.
  • Mantener routing, sesión, tenant y sistema visual como responsabilidades separadas.
  • Cambios de API visual pública deben sincronizar implementación, UI system, UI Kit y tests.

Caché empresarial y cambios de sesión

Los fragmentos de claves de cada recurso se componen en consultas, invalidaciones y escrituras:

import { companyQueryKey } from "@erp/core/query";
import { useCompanyQueryScope } from "@erp/core/hooks";

const scope = useCompanyQueryScope();
const key = companyQueryKey(scope, LIST_CUSTOMERS_QUERY_KEY(criteria));

La clave efectiva contiene empresa y sesión local, además del fragmento del recurso. Los fragmentos/prefijos de las factories no son claves empresariales completas por sí solos. La paginación usa keepPreviousCompanyData: nunca conserva placeholder de otra empresa/sesión. Las estrategias de caché reciben el scope capturado por el hook; un callback tardío nunca debe reconstruirlo consultando la empresa activa nueva. Las claves enumeradas por la caché ya están compuestas y no deben envolverse de nuevo. Audit conserva sus filtros empresariales explícitos y añade el mismo aislamiento de sesión. Cuenta y onboarding usan identidad de sesión sin empresa; son operaciones globales legítimas, no consultas empresariales.

IdentityAuthSessionProvider centraliza selección/cambio, cancelación antes de cambiar header, limpieza y activación. selectActiveCompany y changeActiveCompany son asíncronas y usan el mismo recorrido. Cierre/reemplazo de cuenta destruye caché e incrementa la generación local de sesión; restore/refresh tardíos no pueden restablecer una sesión cerrada. No se modifican claims JWT.

CompanyPageOutlet remonta solo el contenido empresarial al cambiar empresa/sesión: elimina formularios, IDs y selección anterior sin reiniciar sidebar, tema ni toasts. El selector superior no mantiene una segunda estrategia de limpieza. QueryClient, axios/datasource y router son estables durante el montaje de App mediante inicialización diferida en useState.

No basta limpiar un listado ni esconder permisos en React. Backend valida auth/tenant/permiso y persistencia scoped; frontend evita reutilizar resultados de otra empresa/cuenta.