# Arquitectura frontend > **Propósito:** explicar cómo está construido el frontend. Este documento, junto con [UI system](./ui-system.md) y [UI patterns](./ui-patterns.md), 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: ```text 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//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 ```text 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](./ui-system.md). ```text AppMain └── RdxPageFrame ├── header ├── content │ └── RdxPageContainer │ └── RdxPageContent └── footer opcional ``` ```text 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`: ```text 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: ```text 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-system.md), 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: ```ts 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.